2ed31869

Created Started Finished

Three rules: HsLambdaCase, HsAppOnlyMain, TestJsonSpecPerJsonType

Three rules, one commit each, from the easy end of `plan/rules.md`.

### `HsLambdaCase` (module level)

Both shapes the guide names: one equation that names its last argument and then
cases on it, and one equation per pattern with the function's name repeated in
each. hlint covers neither, so it stays a check of its own.

Reported at the function's name rather than its equations, because a
where-bound helper's equations reach to the end of its own where clause and so
contain the bindings written in it, and a finding drawn over another finding
reads as one thing to fix rather than as three.

Both shapes err towards silence: an equation that takes an earlier argument
apart, or names it differently from its neighbour, is doing something one
`\case` cannot say, and a last argument whose name is written anywhere else in
the equation is a name with a use.

**14 findings here**, all fixed in the same commit. 79 in nix-ci.

### `HsAppOnlyMain` (package level)

An executable's own source holds `main = theRealMain` and nothing else. Two
ways to break it: a declaration beside `main`, and a `main` that does the work
itself.

At the package level because the question is which component claims a module,
and a component is a package's answer rather than a module's. The declaration
list is what says what is there, so the only thing read out of the parse tree
is what `main`'s right-hand side does. A type signature is not counted: it
cannot be moved on its own and the binding it belongs to is reported already.

**0 findings here, 0 in nix-ci** — both write the one line already, which is
what puts it in M1 rather than M3.

### `TestJsonSpecPerJsonType` (project level)

A type with both a `ToJSON` and a `FromJSON` instance obliges `jsonSpec @T` in
the test suite of its package's gen package. Both classes rather than either,
because what `jsonSpec` asserts is a claim about the pair jointly.

That makes an obligation a *set* of classes, which makes this rule and
`TestGenValidSpecPerGenValid` the same engine, so the engine is now written
once in `Hopinion.Check.Obligation` and each rule is thirty lines of classes,
combinator and sentences. The rule id becomes a table column, so a row still
belongs to exactly one rule and no rule can read another's. That is the one
place a rule does not own its own table, and `plan/plan.md` now says so as the
family exception it is.

One behaviour change falls out of the class set: a finding is per type rather
than per instance, since one call meets the obligation of every instance of a
type.

**0 findings here** (the one suppression is persistent's generated `ToJSON`
/`FromJSON` for `Key`), 12 in nix-ci.

### Checks

`nix flake check` passes on each of the three commits.