# `Mutare.Poison.Hint`
[🔗](https://github.com/foxbenjaminfox/mutare/blob/v0.2.1/lib/mutare/poison/hint.ex#L1)

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 reports the error at the
macro *call* rather than at the mutation inside it, Mutare cannot identify 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.

# `culprits`

```elixir
@spec culprits(String.t()) :: [
  {{String.t(), atom()}, {String.t(), pos_integer()} | nil}
]
```

The culprit macro of each stacktrace in `output`, paired with where it was invoked:
`{{module_string, function_atom}, {file, line} | nil}`, in source order — one entry per
stacktrace whose innermost frame names a macro we can advise on.

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 (the frames come from
`Mutare.Sandbox.Command.Output.macro_expansion_stacks/1`, innermost first). The call site is
the frame's own — the file whose metamutant holds the poisoning mutant — which is why
`Mutare.Poison.macro_poison/4` reads this rather than `expanding_macros/1`.

    iex> Mutare.Poison.Hint.culprits("expanding macro: Size.megabytes/1\n    lib/a.ex:8: A.f/0\n")
    [{{"Size", :megabytes}, {"lib/a.ex", 8}}]

# `escalation_note`

```elixir
@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 printed after a run *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
`t: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`

```elixir
@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. The names of `culprits/1`, deduplicated.

    iex> Mutare.Poison.Hint.expanding_macros("expanding macro: Size.megabytes/1\n")
    [{"Size", :megabytes}]

# `for_compile_failure`

```elixir
@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`

```elixir
@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/4`), 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

# `route_tuple`

```elixir
@spec route_tuple(String.t(), atom()) :: String.t() | nil
```

The copy-pasteable `call_routes:` entry that leaves `module.fun`'s arguments as written,
as source text: `{Module, :fun, :raw}` for a call, `{Module, :fun, :skip}` for a head
Mutare analyzes structurally (`Kernel.in/2` — the only route it accepts); `nil` for a head
no route can name (a definition or compiler syntax such as `Kernel.def/2`), which only a
`# mutare:ignore` around the offending code can silence. The one place the tuple is spelt,
shared by the abort hint, the recovery notes, and the live reporter's `⚠` line.

    iex> Mutare.Poison.Hint.route_tuple("Ecto.Query", :from)
    "{Ecto.Query, :from, :raw}"
    iex> Mutare.Poison.Hint.route_tuple("Kernel", :in)
    "{Kernel, :in, :skip}"
    iex> Mutare.Poison.Hint.route_tuple("Kernel", :def)
    nil

---

*Consult [api-reference.md](api-reference.md) for complete listing*
