Identify compile-poisoning mutants from a failed metamutant compile.
Every mutated branch lives in the one build, so a single mutation that won't compile would sink the whole run. Built-in mutators are compile-safe by construction, but a custom mutator can emit something that doesn't (an unbound variable, an undefined local call, …). Rather than abort, the runner asks this module which mutant ids the compile error points at, drops them, and rebuilds.
We map each error's file:line to the mutant id(s) whose generated code
spans that line, via a Mutare.Manifest built on demand from the file's
rendered metamutant (see Manifest.ids_at_line/2). Only error diagnostics are
scanned, never warnings: a failed compile prints every warning the mutations
provoke (each footered with the same file:line shape), and mistaking those for
the error's location dropped valid mutants as false poison (see error_locations/1).
The manifest records the
full line range of every mutant's generated code — its selector clause body,
and for a lifted mutant the gated clause (when mutare_active === <id>) where its
guard/head-pattern code actually lives — so a poison is found whether the error
points at the clause, a later line of a multiline body, a lifted mutant clause, or
(as a coarse fallback) the surrounding case. Matching only the selector clause's
start line, as we used to, missed all but the first of those.
The manifest is built here, lazily, only for the file(s) a compile error
names — not eagerly for every mutated file during the scan. That eager build
was pure waste: a manifest is read only on a failed compile (rare — built-in
mutators are compile-safe), yet a full Sourceror.parse_string! of a large
metamutant is by far the most expensive step of the scan (a 1500-line source
whose metamutant is ~40k lines took minutes to re-parse). Deferring it to the
poison path removes that cost from every healthy run.
When line attribution maps nothing — the signature of an inline DSL macro that rejects
the spliced selector, where the compiler blames the macro-call line no manifest region
covers — the runner falls back to macro_poison/2, which attributes by the macro name
the compiler blamed (via Mutare.Manifest.ids_in_named_calls/2) instead of by line. Only
when both fail does the run abort.
Summary
Functions
Mutant ids implicated by compile_output, given %{file => metamutant_source}.
The macro-expansion fallback attribution: mutant ids that live inside a call to a macro the compiler blamed, grouped by that macro.
Functions
Mutant ids implicated by compile_output, given %{file => metamutant_source}.
Builds the per-file Mutare.Manifest lazily — only for the file(s) an error
names — and memoizes it across error locations, so a file faulting on several
lines is parsed once. Returns an empty set when nothing could be mapped (the
caller then aborts).
@spec macro_poison(String.t(), %{optional(String.t()) => String.t()}) :: [ {{String.t(), atom()}, MapSet.t()} ]
The macro-expansion fallback attribution: mutant ids that live inside a call to a macro the compiler blamed, grouped by that macro.
When a mutation splices a runtime selector case into an argument that a macro rewrites
at compile time (an Ecto.Query.from/2-style inline DSL, a macro needing a literal), the
macro raises while expanding and the compiler blames the macro-call line — one line
above the selector case the Mutare.Manifest knows about — so ids/2 finds nothing and
the run would abort. But the failure output names the culprit in an expanding macro: Mod.fun/arity frame (Hint.expanding_macros/1). This maps that name back to mutant ids
through the metamutant (Manifest.ids_in_named_calls/2): find every call of that name in
the rendered metamutant of the file(s) the error touches, and collect the ids inside its
span. Bare-name match (the call is usually an imported from(...), not Ecto.Query.from),
so two same-named macros are skipped together — conservative, and one of them did raise.
Attributing through the metamutant + manifest (not the schema's :sites) is deliberate: it
sees every reserved mutant, so it is correct under --line/--max-mutants — where :sites
is filtered but the metamutant still embeds (and can be poisoned by) an unselected mutant —
exactly like the line-based ids/2 it backs up.
Returns [{{module_string, fun_atom}, MapSet.t()}] — one entry per blamed macro that
matched at least one mutant — so the caller can drop the union and name each macro for the
narration and the {Module, :fun, :raw} suggestion.