Skip to content

feat(engine): add //nomutant directive for inline mutation suppression#279

Open
matuszeg wants to merge 9 commits intogo-gremlins:mainfrom
matuszeg:feat/nomutant-directive
Open

feat(engine): add //nomutant directive for inline mutation suppression#279
matuszeg wants to merge 9 commits intogo-gremlins:mainfrom
matuszeg:feat/nomutant-directive

Conversation

@matuszeg
Copy link
Copy Markdown

@matuszeg matuszeg commented Apr 28, 2026

Proposed changes

Implements inline //nomutant directives, requested in #180.

The directive supports four scopes:

  1. End-of-line — applies to that statement: a := b + c //nomutant
  2. Typed filter — applies only to listed mutator types: a := b + c //nomutant:arithmetic-base,invert-bitwise
  3. Block-scope//nomutant on its own line above a func or single statement; applies to that AST node and everything inside it.
  4. File-scope//nomutant immediately before the package clause; suppresses every mutation in the file.

Suppressed mutants are emitted with status SKIPPED (the same status diff-mode already uses for unchanged code), so the directive's effect remains visible in the report and users can audit which suppressions fired.

Closes #180

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation Update (if none of the other choices apply)

Checklist

  • I have read the CONTRIBUTING doc
  • Lint and unit tests pass locally with my changes (make all)
  • I have added tests that prove my fix is effective or that my feature works
  • I have added necessary documentation (if appropriate)
  • Any dependent changes have been merged and published in downstream modules

Further comments

A few design decisions worth flagging for review:

  • Reused mutator.Skipped rather than introducing a new status. Diff-mode already uses Skipped for "we know this won't run," and an inline directive is semantically the same kind of "explicit skip." Avoiding a new enum value keeps the report layer untouched. If reviewers prefer a distinct SkippedReason field on Mutator to disambiguate diff-skipped vs. directive-skipped, happy to add it.
  • Additive nesting: when a block-scoped directive is nested inside another, both apply — a mutation is suppressed if any enclosing scope suppresses its type. This was the more natural reading of //nomutant:arithmetic-base inside a //nomutant:invert-bitwise block (the user's two requests should compose, not have the inner replace the outer). Documented in docs/docs/usage/nomutant-directive.md.
  • Malformed directives (//nomutant: with no types, or //nomutant:bogus-type) are ignored with a warning, not panicked or treated as syntax errors. This makes it safe to add or rename directives without breaking builds.
  • Mutator type names for the typed filter come from the existing config-key derivation (configuration.MutantTypeEnabledKey minus the mutants.*.enabled framing), so there is one source of truth — adding a new mutator type automatically makes its config key valid in directives.

Self-mutation testing (gremlins unleash --dry-run on this repo) still passes with 94.47% mutator coverage. Manually inserting a //nomutant in internal/report/report.go:179 produces SKIPPED mutants in the report as expected.

Introduces a //nomutant comment directive supporting four scopes:
end-of-line, end-of-line typed, block-scope (above a func or single
statement), and file-scope (above the package clause). Suppressed
mutants are emitted with status SKIPPED so they remain visible in the
report.

Closes go-gremlins#180
Adds tests for nested directives, directive-vs-coverage precedence,
directive coexistence with diff-mode, and adjacent typed filters.
Switches block-scope nesting from "innermost wins" to additive
composition so inner directives augment rather than replace outer
ones. Documents the new nesting semantics.
…l flow

Adds tests for: partial typed filters splitting mutator types from a
single shared token (e.g. '<' produces both ConditionalsBoundary and
ConditionalsNegation), block-scope attachment to an if-statement
(verifying largest-span attachment picks the IfStmt, not a
sub-expression), and end-of-line directives suppressing every
applicable operator on a multi-token line.
@pull-request-size pull-request-size Bot added the s/XXL Size: Denotes a PR that changes 1000+ lines. label Apr 28, 2026
Adds the missing YAML front matter to nomutant-directive.md (matching
the docs/docs/usage/mutations/*.md convention) and rewraps the new
footnote in configuration.md to the 80-character line limit.
The YAML front matter "title" already provides the page heading when
rendered by mkdocs-material; keeping a body H1 alongside it produces
two top-level headings, which Codacy flags as a markdownlint
violation on new files.
Removes the YAML front matter and uses a plain "# Nomutant directive"
H1, matching the structure of usage/ci/docker.md and avoiding both
the multiple-top-level-headings warning and the MD001
heading-increment warning that arose from omitting an H1 entirely.
Adds YAML front matter with a title that exactly matches the body H1,
mirroring the structure used by every recently-added docs page under
docs/docs/usage/mutations/. The previous attempt at front matter
used a title string that differed from the body H1, which appears to
have been why markdownlint MD025 flagged it as "multiple top-level
headings" while the mutations pages with identical title/H1 text are
not flagged.
@matuszeg matuszeg marked this pull request as ready for review April 28, 2026 19:44
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 29, 2026

Codecov Report

❌ Patch coverage is 93.70079% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.87%. Comparing base (b48a4aa) to head (1ca72c6).

Files with missing lines Patch % Lines
internal/engine/directives.go 93.27% 4 Missing and 4 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #279      +/-   ##
==========================================
+ Coverage   87.35%   87.87%   +0.51%     
==========================================
  Files          25       26       +1     
  Lines        1360     1484     +124     
==========================================
+ Hits         1188     1304     +116     
- Misses        141      145       +4     
- Partials       31       35       +4     
Flag Coverage Δ
unittests 87.87% <93.70%> (+0.51%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…code

Codecov flagged four uncovered lines in internal/engine/directives.go.
Two of them were genuinely missing tests — the nil-receiver guard in
isSuppressed and the empty-name "continue" inside parseDirective's
comma-split loop — and are now exercised by new unit tests. The other
two were a *ast.Comment guard inside both collectTokenLines and
largestNodeStartingAtLine that turn out to be unreachable, because
the preceding *ast.CommentGroup guard already returns false from the
ast.Inspect callback and ast.Inspect therefore never descends into
the CommentGroup's *ast.Comment children. Removed both as dead code
and noted in a comment why no Comment guard is needed.
test(engine): cover remaining directives.go branches and remove dead code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

s/XXL Size: Denotes a PR that changes 1000+ lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement possibility to disable mutants via comments

1 participant