Mutare.Poison.Hint (mutare v0.1.2)

Copy Markdown View Source

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

escalation_note(escalations)

@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

expanding_macros(output)

@spec expanding_macros(String.t()) :: [{String.t(), atom()}]

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}]

for_compile_failure(output)

@spec for_compile_failure(String.t()) :: String.t() | nil

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.

macro_skip_note(macro_skipped)

@spec macro_skip_note([%{module: String.t(), macro: atom()}]) :: String.t() | 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.

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