Mutare.Ignore (mutare v0.1.0)

Copy Markdown View Source

Suppresses selected mutants with a source comment. Ignored mutants remain in the report but are excluded from the mutation score.

A trailing directive applies to its own line. A standalone directive applies to the next line of code, reading through any comment lines in between — so it works at either end of an explanatory comment block:

expression() # mutare:ignore
# mutare:ignore
expression()
# mutare:ignore[integer] checked by the boundary test;
# any non-empty replacement is behaviorally equivalent
expression()

A blank line ends the comment block: the directive then targets the blank line and suppresses nothing (warned — see below).

A plain # mutare:ignore covers exactly one line of code (see "Scoped directives" below for regions and whole files). For a multi-line expression, place it directly above the line that carries the mutated code — above the |> step containing it, not the pipe's first line:

digested_files
# mutare:ignore[atom] delivery order is discarded
|> Task.async_stream(&write/1, ordered: false)
|> Stream.run()

A directive misplaced onto an earlier line of the same expression suppresses nothing; the resulting warning points at the line that has the matching mutants.

A directive may include a family filter, a reason, or both:

# mutare:ignore                              suppress every mutant on the line
# mutare:ignore equivalent under int math    suppress all; record the reason
# mutare:ignore[arithmetic]                  suppress only arithmetic mutants
# mutare:ignore[arithmetic, relational]      suppress two families
# mutare:ignore[relational:>]                suppress only the `i > j` swap
# mutare:ignore[integer] off-by-one is fine  a filter and a reason together

Filter entries are mutator family names from Mutare.Mutators.families/0 or a custom mutator's Mutare.Mutator.name/0. Without a filter, the directive suppresses every mutant on the line.

Scoped directives: regions and whole files

Annotating a large low-value span line by line — a literal lookup table, a generated module — doesn't scale, so two scoped verbs suppress in bulk. Both take the same optional [...] filter and reason as # mutare:ignore:

# mutare:ignore-start spot-checked; exhaustively covered by the round-trip test
def encode(?A), do: ?B
def encode(?B), do: ?C
def encode(?C), do: ?D
# mutare:ignore-end

An # mutare:ignore-start opens a region and the next # mutare:ignore-end closes it; every mutant on the delimiter comments' lines and between them is suppressed (the span is inclusive of both delimiter lines, so a trailing -start or -end on a code line covers that line's mutants too). Text after # mutare:ignore-end is plain prose — the filter and reason belong on the -start.

# mutare:ignore-file, anywhere in a file (conventionally at the top), suppresses every matching mutant in that file:

# mutare:ignore-file generated by `mix gen.unicode_tables` — do not hand-edit

A broken region pairing is a hard Mutare.Ignore.SpecError, not a warning: an -end with no open -start, a second -start inside an open region (regions don't nest), or a -start never closed each abort the run — the mistake is provable, and both lenient readings fail the wrong way (silently suppressing to end-of-file, or silently suppressing nothing).

When several directives match one mutant, the most specific filter wins ([family:label] over [family] over unfiltered); on a tie, the narrowest scope (line over region over file), so the recorded reason is the most locally-written one. Scoped directives are held to the same ineffectiveness bar as line directives: an -file or region that suppresses nothing is warned (and aborts under --strict-ignores).

Use family:label to select one variant from a family. For example, [relational:<=] suppresses the <= replacement but not the other relational replacements. Labels are declared by each family and matched case-insensitively. If a mutant has several labels, any matching label suppresses it. Run mix mutare --list-mutators for the complete built-in list.

Text after the keyword or filter is stored as the ignore reason and shown in the report.

A recognizable equivalent mutant: the re-stated delegate guard

A pattern worth knowing when triaging guard_drop/pattern_guard survivors in wrapper-heavy code — a thin wrapper that re-states the guard of the function it delegates to:

def sign(data, salt) when is_binary(salt),
  do: Plug.Crypto.sign(data, salt)   # Plug.Crypto.sign/2 has the identical guard

Dropping the wrapper's guard is provably unobservable: a bad input still raises the same FunctionClauseError, one stack frame deeper, inside the delegate. Verify against the delegate's source (the guards must really be equivalent), then ignore with a reason naming it:

# mutare:ignore[guard_drop] Plug.Crypto.sign/2 re-checks is_binary(salt)
def sign(data, salt) when is_binary(salt), do: Plug.Crypto.sign(data, salt)

This shape recurs constantly in real codebases (any module wrapping a well-guarded library); recognizing it saves convincing yourself a survivor is "just uncovered" when it is actually unreachable.

When a directive errors or does nothing

An unknown label for a built-in family, including a disabled one, or an active custom family is an error. Unknown families, bare-family typos, empty filters, and malformed filters match nothing. Any directive that suppresses no mutant produces a warning; --strict-ignores turns that warning into a non-zero exit.

The mutare: comment namespace is reserved. A comment that starts with # mutare: but does not spell a recognized directive — a typo (# mutare:ingore), a stray space after the colon (# mutare: ignore), or a directive from a future Mutare version — is never silently inert: it produces the same scan-time warning, and --strict-ignores the same non-zero exit.

Only source comments are parsed. Text such as "# mutare:ignore" inside a string has no effect.