Check a repository against one rule Replaces #5, which GitHub will not let me reopen: I merged it by mistake and then took it back off master, so the commits are no longer there. Stacked on the merged #4. `hopinion check .` reads every module of every cabal component and reports what the rules have to say about it. One rule ships. 52 files, 4,019 lines. 110 tests. ## The invariants, each of which fails the run when broken **A rule says there is something to fix, or there is not.** No third answer. A checker whose silence can mean "I could not tell" is a checker whose silence means nothing. So a module that did not parse is a failure of the run rather than a module with nothing wrong in it, and a module whose source is a preprocessor's input is counted as unread rather than passing quietly. **A rule is a value, not an instance of a class.** No generic code is polymorphic in a check type, so a record of functions is the right encoding. Adding a rule is a module and a line in `Hopinion.Rule.Registry`, plus a directory under `hopinion-gen/test_resources/Rule` named after it, which `Hopinion.RuleSpec` turns into a golden test per file with no test code to write. **Facts come from the token stream and the parse tree, never from exact-print annotations.** That placement answers "where must this be reprinted", not "what is this about", and the two differ exactly where it would matter. **Every failure is a type, and one function turns it into words.** `Failure`, `RuleSetError`, `DiscoveryError` and `ReportError` each say what happened rather than carrying the sentence about it, and each has exactly one renderer. Three of `Failure`'s constructors do carry rendered text, deliberately and with the reason written down: what they are about is an absolute path, which under Nix is a store path nobody reading a report can navigate to, so there is nothing for a consumer to do with it but print the sentence. **Every path is a typed path.** `path` and `path-io` throughout, with no `FilePath` in any signature. What a person writes on the command line is parsed at the boundary: a repository root is a `SomeBase Dir`, so it is resolved once against the working directory, and the module command's argument is a `Path Rel File`, so an absolute one is refused by the option parser rather than thrown from the middle of a run. Existence is asked with `forgivingAbsence` rather than a `doesFileExist` before a read that can still race. **Nothing is built for a rule that does not exist yet.** One level, no fact store, no cross-process split, nothing reads what the compiler wrote down. The build holds that line rather than my judgement: `-Wunused-packages`, `-Wunused-top-binds` and weeder run as errors and removed the last of the machinery this rule turned out not to need. ## The rule `HsNoCustomShowRead`, from `haskell-style.md`: `Show` and `Read` are derived, never written out. An `InstanceFact` whose class is one of those and whose origin is a written instance declaration. ## This is deliberately not adoptable yet It finds 22 real instances on nix-ci. All 22 are the secret-hiding `Show` instances that `haskell-style.md` explicitly permits, and there is no way to tell hopinion so, because there is no suppression mechanism yet. That is the argument for the next PR rather than a gap I am hiding: the rule the plan already annotates with "escape hatch needed for secret data" is the one that makes suppressions unavoidable. ## Where to start reading `Hopinion.Rule` for what a rule is, then `Hopinion.Check.Hs.NoCustomShowRead` for what one looks like, then `Hopinion.Run` for how they are run. `Hopinion.Facts` and its neighbours are the vocabulary everything else is a function between. ## Review loop ``` nix flake check nix develop --command cabal test hopinion-test --test-options="--ai-executor" ``` ## What comes after | PR | adds | brought in by | |---|---|---| | #10 | comment attachment, scopes, suppressions, `hopinion.yaml` | `CommentBareTodo` | | #11 | the fact store and the package level | `HsGenValidInGenPackage` | | #12 | the compiler's artifacts, the process split, the Nix builders | `TestGenValidSpecPerGenValid` |