Replaces function return expressions with fixed constants.
It applies to each def and defp return path. When a clause tail is a case/cond/if/unless/with/try/receive, each branch's tail is its own return path. The value of a try after block is excluded because it is discarded. Each clause of an anonymous function is handled in the same way.
Each eligible return path produces two replacements based on its shape:
| tail shape | empty/zero | sentinel |
|---|---|---|
numeric (a + b, x * 2, div(a, b), -n) | 0 | 1 |
string concatenation (a <> b) | "" | "mutare" |
list expression (a ++ b, xs -- ys) | [] | [:mutare] |
| anything else (variable, call, tuple, map, | nil | :mutare |
:ok/:error atom, an opaque-macro result) |
A replacement equal to the original return is omitted.
Two separate rules keep this family from restating another's work, and they answer different
questions. A literal tail belongs to its node-level family outright, whatever the run
enables — see Exclusions. On every tail this family does claim, the transform then compares
values: when an enabled node-level family already produces the same scalar replacement at
that return expression, it keeps the node-level mutation and drops this family's duplicate.
So with AtomLiteral enabled, :foo → :mutare is reported as atom while :foo → nil
stays a return_value mutation; disable AtomLiteral and both replacements come back.
Exclusions
- Boolean expressions are handled by
Mutare.Mutators.Conditional. - Integer, float, string, list, and boolean literals are handed to their node-level families
outright — unlike the value comparison above, this holds even when that family is disabled.
On a literal tail the contrasting pair earns nothing:
nil/:mutarecollide with no replacement those families emit, so no comparison would catch them, and they are at best weaker restatements of one. Ado: falsetail mutated tonilis the clearest case — both are falsy, so everyrefute-style assertion lets it through, and only a strict comparison or afalsepattern match kills it. Bare atoms remain eligible. nilreturn expressions are not mutated.- A unit-returning function — one whose every return path, across all its clauses, is literally
:okornil— has no return positions at all. Its return carries no data, so a replacement there survives whenever the caller discards the value (the norm for a side-effect helper) and is killed only by a test asserting a static fact. Anonymous functions are classified the same way. The classification is syntactic, over the clauses visible in the source (a clause a macro generates beside hand-written ones of the same signature is invisible to it), and otherwise errs only toward missing a unit function: a call in tail position (Logger.info(x)), a variable bound to:ok, or an else-lesswith(its non-matching value is a return path) is not recognised, and one:okpath beside a{:error, _}path is still mutated — that:okcarries the success bit. A tail that never returns (raise/reraise/throw/exit, or the:erlangprimitives) is not a return path, so an:ok-or-raise function is unit; the raising tail itself keeps its return mutants. quoteblocks are compile-time code and are not mutated as a whole.
The empty and sentinel variants can be selected independently in an ignore directive, for example # mutare:ignore[return_value:empty]. This family is enabled by default.
Summary
Functions
Returns clean-meta constant replacements for a clause return expression.
Functions
Returns clean-meta constant replacements for a clause return expression.
Eligible expressions receive an empty or zero value and a non-empty sentinel,
excluding any replacement equal to the original. Returns [] for expressions
handled by another value family, boolean expressions, nil, and quoted code.