b3736ecb

Created Started Cancelled

Let ListLit be skipped by the call its list is an argument of

`ListLit` had no way to be told that a list is a message.

A list literal whose elements are the pieces of a message is a list nobody
asserts the elements of, so every one of them is a mutant that can only ever be
killed by pinning wording. Until now the only lever was turning the operator
off for the whole repository: `ListLit` read no config at all, and the one
targeted key in the family — `ConstEmptyList`'s `skip-strings` — tests whether
the *element* type is `Char`, so it does not see a list *of* strings.

This came out of adopting a house rule that says string concatenation is
written `mconcat [a, b]` rather than `a <> b`. That turns every concatenation
into a `ListLit` site, and the operator stops discriminating.

### What this adds

```yaml
operators:
  ListLit:
    skip-calls-to:
      - logInfo
      - fail
```

**Any enclosing call counts, not only the immediate one.** That is the point
rather than a generalisation for its own sake: the call a message's list
literal is the immediate argument of is always `mconcat` or `unwords`, never
the function that says what the message is *for*. In

```haskell
fail $ mconcat ["unknown result: ", x]
```

it is `fail` that makes the list a message, and it is two applications out. A
`skip-calls-to` that only matched the immediate callee could only ever be
spelled `[mconcat, unwords, unlines]`, which is the blunt repo-wide disable
wearing a hat.

So the walker now carries the calls an expression sits inside, innermost first,
pushed when it descends into an argument. `$` is seen through on both the paths
it can take: the `HsApp` chain the typechecker expands it into, whose head is
`$` rather than the function, and the `OpApp` it can still be.

Name matching is the existing `nameMatchCandidates`, so an entry may be bare or
fully qualified by either the defining module or a module it is imported
through, exactly as for `ElideCall` and `SwitchFunctionArguments`.

### What it does not reach

`skip-calls-to` finds a message only where it is an **argument**. Not a return
value (`renderFooParseError` *is* its message), not one wrapped in a
constructor (`Left $ mconcat [...]` — `headFunctionName` is `Nothing` for
constructors by design), and not a case-alternative or if-branch result. Those
still want a test or a per-binding annotation, which seems right: an argument
to `fail` is a message by construction, and the others are not.

### Test

`Example.ListLitCallsLib` exercises both forms, and `nix/mutation.yaml` names
its `report`. The example's own mutation check is the assertion, and it is a
real one rather than a passing one:

| `mutation-sydtest-mutation-example` | mutations | uncovered | result |
|---|---|---|---|
| without the config entry | 310 | 2 | FAIL |
| with it | 308 | 0 | PASS |

The two it removes are exactly `reportPlain` (direct application) and
`reportDollar` (through `$`). The new golden manifest reads `0 mutations in 0
groups`, and no other module's manifest changed — so the walker change alters
nothing else.

`report` answers with a length rather than its argument so that `ElideCall`
cannot apply; were it the identity, eliding the call would be an equivalent
mutant and the module would be testing that instead.

### Checks

`mutation-sydtest-mutation-example`, `mutation-manifest-example` and
`mutation-sydtest-mutation-example-cross` pass. `mutation-opt-env-conf`,
`mutation-safe-coloured-text` and `mutation-sydtest-mutation-example-diff` fail
identically on this branch and on its base — as does `cabal test
sydtest-mutation-plugin`, on a `CompletionResult`/`String` type error in the
`opt-env-conf-test` dependency. None of those are touched here.
PR #138