Honour #[mutants::skip] on block expressions#618
Open
sandersaares wants to merge 4 commits into
Open
Conversation
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>
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>
Contributor
Author
|
Side-finding: the |
- 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_blockoverride that short-circuits whenattrs_excludedmatches, mirroring the existing per-expression handlers. It works in:#[mutants::skip] { ... }let x = #[mutants::skip] { ... };#[mutants::skip] 'outer: { ... }…because
synattaches the outer attribute toExprBlock.attrsin 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_attributesandproc_macro_hygienefeatures, 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_skipto recognisemutants::skipinsidecfg_attrwhen the cfg predicate is a function-style form (any(...),all(...),not(...)) or aname = "value"form. Previouslyparse_nested_metaerrored out on those, silently dropping the skip directive.The recommended workaround is:
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 theunexpected_cfgslint.cargo-mutantsstill sees the innermutants::skipand applies the suppression.This caveat applies to the existing expression-level skip attrs too;
book/src/attrs.mdis updated with a short note and inline example documenting the workaround.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.src/visit/test/skip_attr_cfg_attr.rs) coveringany(),not(all()), andtarget_os = "linux"predicates insidecfg_attr(...).testdata/skip_attr_block/) exercising the visitor end-to-end withcargo mutants --check. Asserts the expected 22 mutants are produced, all passcargo 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 usingcfg_attr(any(), ...).NEWS.md: Unreleased entry.🤖 Generated with the help of GitHub Copilot CLI.