Skip to content

Honour #[mutants::skip] on block expressions#618

Open
sandersaares wants to merge 4 commits into
sourcefrog:mainfrom
sandersaares:skip-attr-block
Open

Honour #[mutants::skip] on block expressions#618
sandersaares wants to merge 4 commits into
sourcefrog:mainfrom
sandersaares:skip-attr-block

Conversation

@sandersaares
Copy link
Copy Markdown
Contributor

@sandersaares sandersaares commented May 28, 2026

What

Honor #[mutants::skip] on plain block expressions { ... } inside function bodies.

The visitor already checks #[mutants::skip] at most scopes — fn, impl, trait, mod, file, and several expression kinds (call, method-call, match, struct literal, unary). Block expressions were omitted, so writing #[mutants::skip] { ... } inside a function body silently mutated the contents anyway.

This PR adds a visit_expr_block override that short-circuits when attrs_excluded matches, mirroring the existing per-expression handlers. It works in:

  • statement position: #[mutants::skip] { ... }
  • expression position: let x = #[mutants::skip] { ... };
  • labeled blocks: #[mutants::skip] 'outer: { ... }

…because syn attaches the outer attribute to ExprBlock.attrs in all three forms.

Stable Rust workaround: cfg_attr(any(), mutants::skip)

Stable Rust gates custom proc-macro attributes on expression positions behind the unstable stmt_expr_attributes and proc_macro_hygiene features, so the direct form #[mutants::skip] { ... } only compiles on nightly.

To make the feature actually usable on stable, this PR also extends attr_is_mutants_skip to recognise mutants::skip inside cfg_attr when the cfg predicate is a function-style form (any(...), all(...), not(...)) or a name = "value" form. Previously parse_nested_meta errored out on those, silently dropping the skip directive.

The recommended workaround is:

fn frobnicate(x: i32) -> i32 {
    #[cfg_attr(any(), mutants::skip)]
    {
        x + 1
    }
}

any() is built into rustc and evaluates to false at compile time, so the inner attribute is never expanded — and unlike a made-up cfg name (mutants, never, etc.) it does not trigger the unexpected_cfgs lint. cargo-mutants still sees the inner mutants::skip and applies the suppression.

This caveat applies to the existing expression-level skip attrs too; book/src/attrs.md is updated with a short note and inline example documenting the workaround.

Tests

  • Unit tests (src/visit/test/skip_attr_expr_block.rs) covering all four forms (statement-position, expression-position, all-genres-within, labeled) and verifying that nested mutants of every supported genre (binary, unary, match arms, match guards) are suppressed inside an annotated block while sibling code outside it is still mutated.
  • cfg_attr predicate tests (src/visit/test/skip_attr_cfg_attr.rs) covering any(), not(all()), and target_os = "linux" predicates inside cfg_attr(...).
  • Integration testdata (testdata/skip_attr_block/) exercising the visitor end-to-end with cargo mutants --check. Asserts the expected 22 mutants are produced, all pass cargo check, and the ones inside annotated blocks are suppressed. Uses #[cfg_attr(any(), mutants::skip)] so the tree compiles on stable.

Docs

  • book/src/attrs.md: block expressions added to the Scope list; stable-Rust caveat + inline example using cfg_attr(any(), ...).
  • NEWS.md: Unreleased entry.

🤖 Generated with the help of GitHub Copilot CLI.

sandersaares and others added 2 commits May 28, 2026 04:54
The visitor checks for `#[mutants::skip]` at most scopes — fn, impl,
trait, mod, file, and several expression kinds (call, method-call,
match, struct literal, unary). Plain block expressions `{ ... }` were
omitted, so writing `#[mutants::skip] { ... }` inside a function body
silently mutated the contents anyway.

Add a `visit_expr_block` override that short-circuits when
`attrs_excluded` matches, mirroring the existing per-expression
handlers. This works in both statement position
(`#[mutants::skip] { ... }`) and expression position
(`let x = #[mutants::skip] { ... };`), as well as on labeled blocks,
because syn attaches the outer attribute to `ExprBlock.attrs` in all
three forms.

New unit tests under `src/visit/test/skip_attr_expr_block.rs` cover
each of those forms and verify that nested mutants of every supported
genre (binary, unary, match arms, match guards) are suppressed inside
the annotated block, while sibling code outside the block is still
mutated.

Documentation updated:
- `book/src/attrs.md` lists block expressions in the Scope section.
- `NEWS.md` adds an Unreleased entry under the existing section.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds testdata/skip_attr_block tree exercising the new visit_expr_block
override against real cargo. Asserts the expected 22 mutants are produced
and all pass cargo check.

Stable Rust gates custom proc-macro attributes on expression positions
behind the unstable stmt_expr_attributes and proc_macro_hygiene features,
so the tree uses the cfg_attr(mutants, mutants::skip) wrapping form (same
trick as testdata/cfg_attr_mutants_skip). Cargo never sets cfg(mutants)
during normal builds, so the inner mutants::skip is parsed by cargo-mutants
but never expanded by rustc.

Updates book/src/attrs.md to document the stable Rust caveat and the
cfg_attr workaround.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sandersaares sandersaares marked this pull request as draft May 28, 2026 02:36
cargo-mutants's attr_is_mutants_skip walked the cfg_attr contents via
syn's parse_nested_meta. The callback only consumed plain idents, so
function-style cfg predicates like any(), all(...), and not(...) caused
parse_nested_meta to error out with `expected `,``, silently dropping
the mutants::skip suppression. `name = "value"` cfg forms had the same
problem.

Extend the callback to consume:
  * function-style predicates (any(...), all(...), not(...)) by parsing
    and discarding the parenthesised contents
  * name = value predicates by parsing and discarding the value

This unblocks `#[cfg_attr(any(), mutants::skip)]` as the recommended
stable-Rust workaround for skipping expression-position mutants:

  * `any()` is built into rustc and evaluates to false at compile time,
    so the inner attribute is never expanded
  * unlike a made-up cfg name (`mutants`, `never`, etc.) it does not
    trigger the `unexpected_cfgs` lint

Switches the testdata/skip_attr_block tree and the book docs from the
made-up `cfg_attr(mutants, ...)` form to `cfg_attr(any(), ...)`.

Adds unit tests for any(), not(all()), and `name = "value"` predicates
in src/visit/test/skip_attr_cfg_attr.rs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sandersaares sandersaares marked this pull request as ready for review May 28, 2026 02:43
@sandersaares
Copy link
Copy Markdown
Contributor Author

sandersaares commented May 28, 2026

Side-finding: the #[cfg_attr(any(), mutants::skip)] form does not actually require even a dev-dependency on the mutants crate because the macro is never invoked at compile time. This might be an interesting opportunity for people who like to keep a low dependency count.

- Promote the workaround note in the attrs book page from a "caveat"
  callout to a top-level "Hiding the attribute from rustc with
  cfg_attr(any(), ...)" section, explaining:
    * Two use cases: stable-Rust expression-position skips, and avoiding
      a dependency on the `mutants` crate altogether.
    * Why `any()` works (built-in, false with no operands) and avoids
      the `unexpected_cfgs` lint.
    * That the `mutants` crate dependency is not needed when only the
      `cfg_attr(any(), ...)` form is used, because rustc never expands
      the inner attribute.
- Align the SUMMARY (TOC) entry to the actual page title:
  "Skipping mutations with an attribute" rather than "Skipping
  functions with an attribute".

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant