c7b8d283

Say what a path is, and add the rule that holds us to it

Below #11 in the stack: #11 and #12 rebase onto this.

A `FilePath` is a `String`, so nothing about it says whether it holds a file or a directory, whether it is absolute or relative, or whether it is a path at all. Every function taking one documents what it accepts and then trusts its callers.

`HsNoFilePath` reports the type where a module's own signatures use it.

## Read off the tokens

Not the parse tree. The token stream is where a name the code wrote is already told apart from the same word elsewhere: a comment saying `-- a FilePath is a String`, and the literal `"a FilePath is a String"`, are each one token of their own, and a qualified name is one token holding both halves, so `import System.FilePath` does not look like the type either. `good.hs` contains all three and its golden is empty.

That is also why this is not a grep.

## Every finding is suppressible where it is

Each one is scoped to the declaration it is inside, so the report offers a suppression for that declaration and nothing wider:

```
[error HsNoFilePath]: The type FilePath. Say what the path is with Path from path.
     ╭──▶ hopinion-gen/test/Hopinion/AnnotationSpec.hs@28:16-28:24
  28 │ resourceDir :: FilePath
     •                ┬───────
     │ Hint: To suppress, write this directly above line 28 of ... : -- [allow:HsNoFilePath] <reason>
```

Converting at the edge is not reported at all. A library that wants a `String` gets `readFile (toFilePath file)`, and that is correct.

## The repository is clean under it

Twenty findings, every one of them in the test specs. The library needed no changes to pass, which is worth saying: it had no `FilePath` in its own signatures already, only conversions at boundaries. Its one word-comparison of a directory name is now a comparison of directories.

Fixing the specs dropped `directory` from `hopinion-gen`. `resourceDir` is a `Path Rel Dir`, the three copies of `rootAt` take one, and `listDirRel` answers with directories and files already told apart, so several listing assertions are now exact about which is which instead of comparing sorted strings.

## It needed a sydtest change

NorfairKing/sydtest#137, merged and released as sydtest 0.30.0.0, pinned here at `27be1a6d`. `scenarioDir` handed its callback a `FilePath`, which forced `FilePath` into the signature of anything called from inside one. It now hands over a `Path Rel File`, and `scenarioDirOfDirs` a `Path Rel Dir`, relative to the directory given, so the one thing a caller has to know about a scenario is the one thing the type now tells them and the name does not have to be split back out of a joined path.

## What is deliberately not here

The same rule over `import System.FilePath` and `import System.Directory`. An import is not a declaration, so the comment attachment pass has nowhere to attach a suppression above one, and the only thing left is a file-scoped suppression, which would exempt every other finding in that file. A rule whose findings cannot be suppressed where they are breaks the rule #10 is built on: a suppression the report offers must be one the parser accepts. That wants an `AttachedToImport` first, and it is its own change.

`filepath` therefore survives in two places, both genuine gaps in `path`: cabal hands module names back as `[String]`, so `joinPath` is needed at that boundary, and turning a directory's name into a file's name has no typed equivalent. Those two are exactly where a dependency-based rule would want a per-site suppression.

One thing to settle: this cites `haskell-style.md`, "Preferred libraries", which does not currently say it. The guide wants the line, or the rule is citing something that is not there.

## What it costs elsewhere

57 findings in nix-ci, 47 in sydtest, 10 in centjes. None of them is the exception the rule stays quiet about, so all three are real conversions rather than suppressions to write.

nix-ci turns the rule off in a new `hopinion.yaml` until those 57 are converted, which is NorfairKing/nix-ci#527. That is a decision, not a workaround: 57 suppressions would reach a passing check dishonestly, because a suppression says this one is deliberate and none of these is. Converting them is a change to review on its own.

## Review loop

```
nix flake check
nix develop --command cabal test hopinion-test --test-options="--ai-executor"
```

hlint-checkpre-commitreleaseshellweeder-checkconfiguredevShelldefaultx86_64-linux