59fb01be

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#136, pinned here. `scenarioDir` handed its callback a `FilePath`, which forced `FilePath` into the signature of anything called from inside one. It now hands over a `Path b File`, and `scenarioDirOfDirs` a `Path b Dir`, so the one thing a caller has to know about a scenario is the one thing the type now tells them.

## 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.

## Review loop

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

Suite timing

Time to Start Worker time Duration Time to finish Idle
Config 11s 1s 1s 13s 11s
Eval 31s 9s 9s 41s 17s
Build 40s 0s 48s 1m29s 47s
Suite 11s 11s 1m17s 1m29s 1m17s

Timeline

0s40s