Skip to content

date: 2026-06-25 tags: [mutation-testing, pest-mutate, footgun] status: graduated graduated_to: CLAUDE.md # Mutation Testing Policy

A targeted @pest-mutate-ignore silently no-ops on trailing prose or the wrong line

Symptom — the diff-scoped mutation gate on PR #783 stalled at 94.7% despite an @pest-mutate-ignore: <Mutator> on every equivalent mutant. The ignored mutants kept showing as survivors; no error, no warning.

Root cause — two separate placement traps, both in pest-mutate's parser:

  1. Exact-string match. NodeVisitor compares $ignore === $mutator::name() after explode(',') + trim. A directive like // @pest-mutate-ignore: RemoveArrayItem — checked defaults to false yields the string "RemoveArrayItem — checked defaults to false", which !== "RemoveArrayItem" → the ignore is parsed but never matches.
  2. Leading vs trailing. A leading comment is only honoured when PHP-Parser attaches it to the mutated node — which happens for whole statements, not array-item nodes. For a RemoveArrayItem (a casts() entry, a create([...]) key, a validation-rule element) the leading form no-ops; the directive must be a trailing same-line comment, which pest-mutate catches via its line-map (mutatorsToIgnoreByLine[node->getStartLine()], keyed on the comment's own line).

Fix — clean, correctly-placed directives across the packing files (commit d8f4097): mutator name(s) only, prose on a preceding line, trailing for array-item mutants. Took the gate 94.7% → 100%.

Guard — graduated into CLAUDE.md's Mutation Testing Policy (the @pest-mutate-ignore paragraph), which now states the exact-match rule and the leading-statement / trailing-array-item placement. The old "must be on its own preceding line" claim there was wrong for array-item mutants and has been corrected.