Turns one unrecoverable compile failure into advice the user can act on.
Almost every compile-poisoning mutation is recovered automatically — Mutare
finds the offending mutant, drops it, and rebuilds. One case can't be: a macro
that requires a compile-time literal argument (Size.megabytes(5) and the
like). Mutating the literal turns it into runtime code, the macro rejects it and
raises while the compiler is expanding it, and because the compiler blames the
macro call rather than the mutation inside it, Mutare can't tell which single
mutant to drop. The whole run aborts.
The way out is to leave that macro's arguments as written by routing it :raw
in .mutare.exs (the :call_routes option). This module recognises the situation
from the failed compile's output, identifies the macro(s) at fault, and produces
that advice — including a copy-pasteable snippet — so the error the user sees
explains how to get unblocked instead of just echoing the raw compiler error.
Returns nil for any other compile failure, where the raw error stands on its
own.
Summary
Functions
A copy-pasteable :call_routes suggestion for the block macros a successful run
had to escalate (skip wholesale) during compile-poison recovery, or nil when there
were none.
The macro(s) to advise skipping, as distinct {module_string, function_atom}
pairs read from the expanding macro: frames in output, in first-seen order.
[] when there are none.
A copy-pasteable remediation hint for the failed-compile output, or nil.
A copy-pasteable :call_routes suggestion for the inline DSL macros a successful run
had to skip via the macro-expansion fallback (Mutare.Poison.macro_poison/2), or nil
when there were none.
Functions
@spec escalation_note([Mutare.Run.escalation()]) :: String.t() | nil
A copy-pasteable :call_routes suggestion for the block macros a successful run
had to escalate (skip wholesale) during compile-poison recovery, or nil when there
were none.
Unlike for_compile_failure/1 — which fires on an unrecoverable abort — this is the
advice a run prints after it recovered: the metamutant compiled, but only because
Mutare guessed a DSL body could be mutated, hit poison, and skipped the block at
runtime. That recovery is rediscovered from scratch on every run (the dropped ids are
in-memory only), so we hand the user the durable, name-based fix. Each escalated macro
becomes a module-wildcard {:*, :name, :raw} route (the invocation's module is an
unknown DSL we don't resolve), skipping that macro name wherever it appears.
escalations is Mutare.Run's :recovery.escalated (a list of
Mutare.Run.escalation/0).
iex> Mutare.Poison.Hint.escalation_note([%{macro: :guarded, file: "lib/x.ex", line: 3, count: 2}])
...> |> String.contains?("{:*, :guarded, :raw}")
true
The macro(s) to advise skipping, as distinct {module_string, function_atom}
pairs read from the expanding macro: frames in output, in first-seen order.
[] when there are none.
Each compile error reports only its innermost macro — the one whose literal
argument was actually mutated. A literal-only macro nested inside another
(if Size.megabytes(5)) also lists the enclosing macros, but skipping those
would needlessly hide valid mutants, so only the culprit is kept.
iex> Mutare.Poison.Hint.expanding_macros("expanding macro: Size.megabytes/1\n")
[{"Size", :megabytes}]
A copy-pasteable remediation hint for the failed-compile output, or nil.
Recognises the one poison case Mutare can't recover on its own — a macro that
needs a compile-time literal argument — and returns advice on how to skip it.
Any other failure returns nil, leaving the raw compiler error to stand alone.
A copy-pasteable :call_routes suggestion for the inline DSL macros a successful run
had to skip via the macro-expansion fallback (Mutare.Poison.macro_poison/2), or nil
when there were none.
The sibling of escalation_note/1 for inline macros rather than block macros: a mutation
wouldn't compile inside a macro that rewrites its argument at compile time, so Mutare
dropped that macro's mutants and rebuilt. Because the compiler named the macro (an
expanding macro: frame), the module is known — so unlike the block case's {:*, …}
wildcard this suggests the precise {Module, :fun, :raw}. That recovery is rediscovered
(and its rebuilds repaid) on every run, so pinning it is the durable fix.
macro_skipped is Mutare.Run's :recovery.macro_skipped (a list of
%{module: module_string, macro: fun_atom}).
iex> Mutare.Poison.Hint.macro_skip_note([%{module: "Ecto.Query", macro: :from}])
...> |> String.contains?("{Ecto.Query, :from, :raw}")
true