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.