From be285437e1f2a767dbdd0faebb69ab487263ec7d Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 28 May 2026 04:54:10 +0300 Subject: [PATCH 1/9] Honour #[mutants::skip] on block expressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- NEWS.md | 2 + book/src/attrs.md | 6 +- src/visit.rs | 18 ++++ src/visit/test/skip_attr_expr_block.rs | 141 +++++++++++++++++++++++++ 4 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 src/visit/test/skip_attr_expr_block.rs diff --git a/NEWS.md b/NEWS.md index f89bcd81..18c2b304 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,8 @@ ## Unreleased +- New: `#[mutants::skip]` is now honoured on block expressions (`{ ... }`), in both statement and expression position. All mutants generated inside the annotated block are suppressed. + - New: mutate `NonZero` into `1`, and also `-1` when `T` is or may be signed. - Docs: Clarify that `#[mutants::skip]` is honoured at every scope where attributes can be placed, not only on functions: `impl` blocks, `trait` declarations, modules, files (as `#![mutants::skip]`), and expressions that can carry an outer attribute. Only the documentation was misleading; the behaviour is unchanged. diff --git a/book/src/attrs.md b/book/src/attrs.md index 63a4fe1f..e8cdc697 100644 --- a/book/src/attrs.md +++ b/book/src/attrs.md @@ -60,6 +60,6 @@ mod test { - **`mod` blocks** — applies to all items within the module. - **Files** (as an inner attribute `#![mutants::skip]`) — applies to the entire file. - **Expressions** that can syntactically carry an outer attribute, including - `match`, struct literal (`Foo { ... }`), call (`foo(...)`), method-call - (`x.foo(...)`), and unary expressions (`!x`, `-x`) — applies to the - expression and everything nested inside it. + block (`{ ... }`), `match`, struct literal (`Foo { ... }`), call + (`foo(...)`), method-call (`x.foo(...)`), and unary expressions (`!x`, + `-x`) — applies to the expression and everything nested inside it. diff --git a/src/visit.rs b/src/visit.rs index 2dfdaec4..d5bb2fd6 100644 --- a/src/visit.rs +++ b/src/visit.rs @@ -751,6 +751,23 @@ impl<'ast> Visit<'ast> for DiscoveryVisitor<'_> { syn::visit::visit_expr_struct(self, i); } + + /// Visit a block expression, e.g. `{ ... }` used as a statement or as the + /// right-hand side of `let x = { ... };`. + /// + /// An outer `#[mutants::skip]` attribute attached to the block (in either + /// position) suppresses mutants generated for every expression inside the + /// block. `Block` itself carries no attributes — they live on the enclosing + /// `ExprBlock`, which is what this handler inspects. + fn visit_expr_block(&mut self, i: &'ast syn::ExprBlock) { + let _span = trace_span!("expr_block", line = i.span().start().line).entered(); + trace!("visit block expression"); + if attrs_excluded(&i.attrs) { + trace!("block excluded by attrs"); + return; + } + syn::visit::visit_expr_block(self, i); + } } // Get the span of the block excluding the braces, or None if it is empty. @@ -984,6 +1001,7 @@ mod test { use super::*; mod skip_attr_cfg_attr; + mod skip_attr_expr_block; mod skip_attr_expr_call; mod skip_attr_expr_match; mod skip_attr_expr_method_call; diff --git a/src/visit/test/skip_attr_expr_block.rs b/src/visit/test/skip_attr_expr_block.rs new file mode 100644 index 00000000..de96177a --- /dev/null +++ b/src/visit/test/skip_attr_expr_block.rs @@ -0,0 +1,141 @@ +//! Tests that `#[mutants::skip]` on a block expression `{ ... }` suppresses +//! mutants generated inside that block, while sibling code in the same +//! function remains mutated. + +use indoc::indoc; +use test_log::test; + +use crate::Options; +use crate::visit::mutate_source_str; + +#[test] +fn skip_attr_on_statement_position_block_suppresses_nested_mutants() { + let mutants = mutate_source_str( + indoc! {r#" + fn driver(a: i32, b: i32, c: i32, d: i32) { + #[mutants::skip] + { + let _ = a + b; + } + let _ = c - d; + } + "#}, + &Options::default(), + ) + .unwrap(); + let names: Vec = mutants.iter().map(|m| m.name(false)).collect(); + + assert!( + !names.iter().any(|n| n.contains("replace + with")), + "`+` inside skipped block should not produce mutants: {names:?}" + ); + assert!( + names.iter().any(|n| n.contains("replace - with")), + "`-` in the unannotated sibling code should still produce mutants: {names:?}" + ); +} + +#[test] +fn skip_attr_on_expression_position_block_suppresses_nested_mutants() { + let mutants = mutate_source_str( + indoc! {r#" + fn driver(a: i32, b: i32, c: i32, d: i32) -> i32 { + let x = #[mutants::skip] { + a + b + }; + let y = { + c - d + }; + x | y + } + "#}, + &Options::default(), + ) + .unwrap(); + let names: Vec = mutants.iter().map(|m| m.name(false)).collect(); + + assert!( + !names.iter().any(|n| n.contains("replace + with")), + "`+` inside skipped block should not produce mutants: {names:?}" + ); + assert!( + names.iter().any(|n| n.contains("replace - with")), + "`-` in the unannotated sibling block should still produce mutants: {names:?}" + ); + assert!( + names.iter().any(|n| n.contains("replace | with")), + "`|` in the unannotated tail expression should still produce mutants: {names:?}" + ); +} + +#[test] +fn skip_attr_on_block_suppresses_all_genres_within() { + let mutants = mutate_source_str( + indoc! {r#" + fn pick(x: i32, y: i32) -> &'static str { + #[mutants::skip] + { + let _ = !true; + match x { + 0 => "zero", + n if n > y => "gt", + _ => "other", + } + } + } + "#}, + &Options::default(), + ) + .unwrap(); + let names: Vec = mutants.iter().map(|m| m.name(false)).collect(); + + assert!( + !names.iter().any(|n| n.contains("delete !")), + "unary mutants inside skipped block should be suppressed: {names:?}" + ); + assert!( + !names.iter().any(|n| n.contains("delete match arm")), + "match arm deletion mutants inside skipped block should be suppressed: {names:?}" + ); + assert!( + !names.iter().any(|n| n.contains("replace match guard")), + "match guard mutants inside skipped block should be suppressed: {names:?}" + ); + assert!( + !names.iter().any(|n| n.contains("replace > with")), + "binary mutants inside skipped block should be suppressed: {names:?}" + ); +} + +#[test] +fn skip_attr_on_labeled_block_suppresses_nested_mutants() { + let mutants = mutate_source_str( + indoc! {r#" + fn driver(a: i32, b: i32) -> i32 { + #[mutants::skip] + 'block: { + if a > b { + break 'block a + b; + } + a - b + } + } + "#}, + &Options::default(), + ) + .unwrap(); + let names: Vec = mutants.iter().map(|m| m.name(false)).collect(); + + assert!( + !names.iter().any(|n| n.contains("replace + with")), + "`+` inside skipped labeled block should not produce mutants: {names:?}" + ); + assert!( + !names.iter().any(|n| n.contains("replace - with")), + "`-` inside skipped labeled block should not produce mutants: {names:?}" + ); + assert!( + !names.iter().any(|n| n.contains("replace > with")), + "`>` inside skipped labeled block should not produce mutants: {names:?}" + ); +} From 08e6f28da48061b3579a71d680919d570d382555 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 28 May 2026 05:12:54 +0300 Subject: [PATCH 2/9] Add testdata integration test for #[mutants::skip] on block expressions 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> --- book/src/attrs.md | 15 +++++ testdata/skip_attr_block/Cargo_test.toml | 11 ++++ testdata/skip_attr_block/README.md | 35 ++++++++++++ testdata/skip_attr_block/src/lib.rs | 73 ++++++++++++++++++++++++ tests/main.rs | 64 +++++++++++++++++++++ 5 files changed, 198 insertions(+) create mode 100644 testdata/skip_attr_block/Cargo_test.toml create mode 100644 testdata/skip_attr_block/README.md create mode 100644 testdata/skip_attr_block/src/lib.rs diff --git a/book/src/attrs.md b/book/src/attrs.md index e8cdc697..a376553f 100644 --- a/book/src/attrs.md +++ b/book/src/attrs.md @@ -63,3 +63,18 @@ mod test { block (`{ ... }`), `match`, struct literal (`Foo { ... }`), call (`foo(...)`), method-call (`x.foo(...)`), and unary expressions (`!x`, `-x`) — applies to the expression and everything nested inside it. + +> **Caveat:** attributes on expressions are not supported on the stable +> Rust toolchain. As a workaround, you can use the form +> `#[cfg_attr(mutants, mutants::skip)]`, which "hides" the attribute from +> the compiler but keeps it visible to cargo-mutants. This works even on +> stable Rust. For example: +> +> ```rust,ignore +> fn frobnicate(x: i32) -> i32 { +> #[cfg_attr(mutants, mutants::skip)] +> { +> x + 1 +> } +> } +> ``` diff --git a/testdata/skip_attr_block/Cargo_test.toml b/testdata/skip_attr_block/Cargo_test.toml new file mode 100644 index 00000000..8e2fc260 --- /dev/null +++ b/testdata/skip_attr_block/Cargo_test.toml @@ -0,0 +1,11 @@ +[package] +name = "cargo-mutants-testdata-skip-attr-block" +version = "0.1.0" +edition = "2018" +publish = false + +# No dependency on the `mutants` crate is needed: the proc-macro is +# wrapped in `cfg_attr(any(), ...)` and is therefore never expanded. + +[lib] +doctest = false diff --git a/testdata/skip_attr_block/README.md b/testdata/skip_attr_block/README.md new file mode 100644 index 00000000..59412eae --- /dev/null +++ b/testdata/skip_attr_block/README.md @@ -0,0 +1,35 @@ +# skip_attr_block + +Exercises `#[mutants::skip]` placed on a block expression `{ ... }` inside a +function body. Each function in `src/lib.rs` annotates a block in a different +syntactic position and pairs it with un-annotated sibling code so that the +expected behaviour is unambiguous: + +- `statement_position` — `#[..] { ... }` used as a statement. +- `tail_block` — `#[..] { ... }` used as the function's tail expression. +- `labeled_block` — `#[..] 'lbl: { ... }` used as the function's tail + expression. +- `unannotated_sibling` — no skip attribute; mutants must still be produced. + +Inside each annotated block every mutant cargo-mutants would normally produce +must be suppressed; in `unannotated_sibling`, mutants must be produced as +usual. + +## Why `cfg_attr` wrapping? + +Stable Rust gates custom (proc-macro) attributes on expressions and block +expressions behind the unstable `stmt_expr_attributes` and +`proc_macro_hygiene` features, so the direct form `#[mutants::skip] { ... }` +does not compile on stable. This tree uses +`#[cfg_attr(mutants, mutants::skip)]` instead: the `mutants` cfg is never +set during normal builds or `cargo check --tests`, so rustc strips the +`cfg_attr` to nothing and never sees the inner `mutants::skip` proc-macro +attribute. cargo-mutants still recognises it via +`visit::attr_is_mutants_skip` and applies the suppression. This matches +the convention already used by `testdata/cfg_attr_mutants_skip`. + +The unit tests under `src/visit/test/skip_attr_expr_block.rs` cover the +direct `#[mutants::skip]` form (which only requires syn parsing). + +Because the inner proc-macro is never expanded, this tree does not need +to depend on the `mutants` crate at all. diff --git a/testdata/skip_attr_block/src/lib.rs b/testdata/skip_attr_block/src/lib.rs new file mode 100644 index 00000000..01a1961a --- /dev/null +++ b/testdata/skip_attr_block/src/lib.rs @@ -0,0 +1,73 @@ +//! Verify that `#[mutants::skip]` on a block expression `{ ... }` +//! suppresses every mutant generated inside that block, while mutants +//! in surrounding code are still produced as usual. +//! +//! On stable Rust, custom (proc-macro) attributes on expressions and +//! block expressions are gated behind the unstable `stmt_expr_attributes` +//! and `proc_macro_hygiene` features. To keep this testdata compilable on +//! stable, we use the `#[cfg_attr(mutants, mutants::skip)]` wrapping: the +//! `mutants` cfg is never set during normal builds or `cargo check +//! --tests`, so rustc strips the `cfg_attr` to nothing and never sees the +//! inner `mutants::skip` proc-macro attribute. cargo-mutants still +//! recognises it via `visit::attr_is_mutants_skip` and applies the +//! suppression. The unit tests under +//! `src/visit/test/skip_attr_expr_block.rs` additionally cover the direct +//! `#[mutants::skip]` form, which only needs to parse through syn. + +/// `#[mutants::skip]` on a block used as a statement. +/// +/// The `+=` and `+` operators inside the annotated block must produce no +/// mutants. The `+` and `-` on the tail expression must still be mutated +/// as usual. +pub fn statement_position(a: i32, b: i32, c: i32, d: i32) -> i32 { + let mut total = 0; + #[cfg_attr(mutants, mutants::skip)] + { + total += a + b; + } + total + (c - d) +} + +/// `#[mutants::skip]` on an unlabeled block used as the function's tail +/// expression. +/// +/// Every operator inside the annotated block — `>`, `+`, `-` — must +/// produce no mutants. +pub fn tail_block(a: i32, b: i32, c: i32) -> i32 { + let _ = 0; + #[cfg_attr(mutants, mutants::skip)] + { + if a > b { + a + b + } else { + a - c + } + } +} + +/// `#[mutants::skip]` on a labeled block used as the function's tail +/// expression. +/// +/// Every operator inside the annotated block — `>`, `+`, `-` — must +/// produce no mutants. +pub fn labeled_block(a: i32, b: i32, c: i32) -> i32 { + #[cfg_attr(mutants, mutants::skip)] + 'block: { + if a > b { + break 'block a + b; + } + a - c + } +} + +/// Sibling un-annotated block — mutants here must NOT be suppressed. +/// +/// Paired with `statement_position` etc. above, this confirms the +/// suppression is scoped to the annotated block and does not leak into +/// surrounding code. +pub fn unannotated_sibling(a: i32, b: i32) -> i32 { + let x = { a * b }; + let y = { a / b }; + x | y +} + diff --git a/tests/main.rs b/tests/main.rs index 376ba105..ce74a933 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -2629,6 +2629,70 @@ fn check_tree_with_mutants_skip() { ); } +/// `#[mutants::skip]` placed on a block expression inside a function body +/// must suppress every mutant generated for code inside that block. +/// +/// The `skip_attr_block` tree pairs each annotated block with un-annotated +/// surrounding code, so the absence of mutants from the annotated lines and +/// their presence on the un-annotated lines is the actual assertion. We use +/// `--check` so the listed mutants are also confirmed to compile on stable, +/// ruling out regressions where suppression accidentally rewrites the source. +/// +/// The tree uses the `#[cfg_attr(mutants, mutants::skip)]` form because +/// stable Rust does not accept custom proc-macro attributes on expressions; +/// see the testdata's README for details. The direct `#[mutants::skip]` form +/// is covered by unit tests in `src/visit/test/skip_attr_expr_block.rs`. +#[test] +fn check_tree_with_skip_attr_on_block_expressions() { + let tmp_src_dir = copy_of_testdata("skip_attr_block"); + run() + .arg("mutants") + .args(["--check", "--no-times", "--no-shuffle"]) + .current_dir(tmp_src_dir.path()) + .env_remove("RUST_BACKTRACE") + .assert() + .success() + .stdout(indoc! { r" + Found 22 mutants to test + ok Unmutated baseline + ok src/lib.rs:23:5: replace statement_position -> i32 with 0 + ok src/lib.rs:23:5: replace statement_position -> i32 with 1 + ok src/lib.rs:23:5: replace statement_position -> i32 with -1 + ok src/lib.rs:28:11: replace + with - in statement_position + ok src/lib.rs:28:11: replace + with * in statement_position + ok src/lib.rs:28:16: replace - with + in statement_position + ok src/lib.rs:28:16: replace - with / in statement_position + ok src/lib.rs:37:5: replace tail_block -> i32 with 0 + ok src/lib.rs:37:5: replace tail_block -> i32 with 1 + ok src/lib.rs:37:5: replace tail_block -> i32 with -1 + ok src/lib.rs:54:5: replace labeled_block -> i32 with 0 + ok src/lib.rs:54:5: replace labeled_block -> i32 with 1 + ok src/lib.rs:54:5: replace labeled_block -> i32 with -1 + ok src/lib.rs:69:5: replace unannotated_sibling -> i32 with 0 + ok src/lib.rs:69:5: replace unannotated_sibling -> i32 with 1 + ok src/lib.rs:69:5: replace unannotated_sibling -> i32 with -1 + ok src/lib.rs:69:17: replace * with + in unannotated_sibling + ok src/lib.rs:69:17: replace * with / in unannotated_sibling + ok src/lib.rs:70:17: replace / with % in unannotated_sibling + ok src/lib.rs:70:17: replace / with * in unannotated_sibling + ok src/lib.rs:71:7: replace | with & in unannotated_sibling + ok src/lib.rs:71:7: replace | with ^ in unannotated_sibling + 22 mutants tested: 22 succeeded + "}) + .stderr(""); + assert_eq!( + outcome_json_counts(&tmp_src_dir), + serde_json::json!({ + "caught": 0, + "missed": 0, + "success": 22, + "timeout": 0, + "unviable": 0, + "total_mutants": 22, + }) + ); +} + #[test] fn check_tree_where_build_fails() { let tmp_src_dir = copy_of_testdata("typecheck_fails"); From 875678832d9983ae70b6a053f59aa1490f109eea Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 28 May 2026 05:40:02 +0300 Subject: [PATCH 3/9] Recognise function-style cfg predicates inside cfg_attr(mutants::skip) 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> --- book/src/attrs.md | 8 +-- src/visit.rs | 14 +++++- src/visit/test/skip_attr_cfg_attr.rs | 74 ++++++++++++++++++++++++++++ testdata/skip_attr_block/README.md | 13 ++--- testdata/skip_attr_block/src/lib.rs | 12 ++--- 5 files changed, 105 insertions(+), 16 deletions(-) diff --git a/book/src/attrs.md b/book/src/attrs.md index a376553f..081079f3 100644 --- a/book/src/attrs.md +++ b/book/src/attrs.md @@ -66,13 +66,15 @@ mod test { > **Caveat:** attributes on expressions are not supported on the stable > Rust toolchain. As a workaround, you can use the form -> `#[cfg_attr(mutants, mutants::skip)]`, which "hides" the attribute from +> `#[cfg_attr(any(), mutants::skip)]`, which "hides" the attribute from > the compiler but keeps it visible to cargo-mutants. This works even on -> stable Rust. For example: +> stable Rust. The empty `any()` cfg predicate is built into rustc and +> always evaluates to false, so the inner attribute is never expanded. +> For example: > > ```rust,ignore > fn frobnicate(x: i32) -> i32 { -> #[cfg_attr(mutants, mutants::skip)] +> #[cfg_attr(any(), mutants::skip)] > { > x + 1 > } diff --git a/src/visit.rs b/src/visit.rs index d5bb2fd6..0f8f12bc 100644 --- a/src/visit.rs +++ b/src/visit.rs @@ -934,7 +934,9 @@ fn path_ends_with(path: &syn::Path, ident: &str) -> bool { /// True if the attribute contains `mutants::skip`. /// -/// This for example returns true for `#[mutants::skip]` or `#[cfg_attr(test, mutants::skip)]`. +/// This for example returns true for `#[mutants::skip]`, +/// `#[cfg_attr(test, mutants::skip)]`, or +/// `#[cfg_attr(any(), mutants::skip)]`. fn attr_is_mutants_skip(attr: &Attribute) -> bool { if path_is(attr.path(), &["mutants", "skip"]) { return true; @@ -946,6 +948,16 @@ fn attr_is_mutants_skip(attr: &Attribute) -> bool { if let Err(err) = attr.parse_nested_meta(|meta| { if path_is(&meta.path, &["mutants", "skip"]) { skip = true; + } else if meta.input.peek(syn::token::Paren) { + // Function-style cfg predicate like `any(...)`, `all(...)`, `not(...)`. + // We don't evaluate the predicate; just consume and discard its contents + // so parse_nested_meta can advance to the next item. + let content; + let _ = syn::parenthesized!(content in meta.input); + let _: proc_macro2::TokenStream = content.parse()?; + } else if meta.input.peek(syn::Token![=]) { + // `name = "value"` form (e.g. `target_os = "linux"`); consume the value. + let _: syn::Expr = meta.value()?.parse()?; } Ok(()) }) { diff --git a/src/visit/test/skip_attr_cfg_attr.rs b/src/visit/test/skip_attr_cfg_attr.rs index 439deda6..de45941b 100644 --- a/src/visit/test/skip_attr_cfg_attr.rs +++ b/src/visit/test/skip_attr_cfg_attr.rs @@ -71,3 +71,77 @@ fn cfg_attr_mutants_skip_on_mod_suppresses_inner_items() { "sibling function outside the mod should still produce mutants: {names:?}" ); } + +/// Function-style cfg predicates like `any(...)`, `all(...)`, and `not(...)` +/// inside `cfg_attr` must still be recognised as carrying a `mutants::skip` +/// directive. `cfg_attr(any(), ...)` is the recommended stable-Rust +/// workaround for skipping expression-position mutants because the empty +/// `any()` evaluates to false at compile time, so rustc never expands the +/// inner attribute. +#[test] +fn cfg_attr_with_any_predicate_is_recognised_as_mutants_skip() { + let mutants = mutate_source_str( + indoc! {r#" + #[cfg_attr(any(), mutants::skip)] + fn add(a: i32, b: i32) -> i32 { + a + b + } + + fn outside(a: i32, b: i32) -> i32 { + a * b + } + "#}, + &Options::default(), + ) + .unwrap(); + let names: Vec = mutants.iter().map(|m| m.name(false)).collect(); + + assert!( + !names.iter().any(|n| n.contains("add")), + "cfg_attr(any(), mutants::skip) should suppress mutants on the fn: {names:?}" + ); + assert!( + names.iter().any(|n| n.contains("outside")), + "sibling function should still produce mutants: {names:?}" + ); +} + +#[test] +fn cfg_attr_with_not_all_predicate_is_recognised_as_mutants_skip() { + let mutants = mutate_source_str( + indoc! {r#" + #[cfg_attr(not(all()), mutants::skip)] + fn add(a: i32, b: i32) -> i32 { + a + b + } + "#}, + &Options::default(), + ) + .unwrap(); + let names: Vec = mutants.iter().map(|m| m.name(false)).collect(); + + assert!( + !names.iter().any(|n| n.contains("add")), + "cfg_attr(not(all()), mutants::skip) should suppress mutants on the fn: {names:?}" + ); +} + +#[test] +fn cfg_attr_with_name_value_predicate_is_recognised_as_mutants_skip() { + let mutants = mutate_source_str( + indoc! {r#" + #[cfg_attr(target_os = "linux", mutants::skip)] + fn add(a: i32, b: i32) -> i32 { + a + b + } + "#}, + &Options::default(), + ) + .unwrap(); + let names: Vec = mutants.iter().map(|m| m.name(false)).collect(); + + assert!( + !names.iter().any(|n| n.contains("add")), + "cfg_attr(name = \"value\", mutants::skip) should suppress mutants on the fn: {names:?}" + ); +} diff --git a/testdata/skip_attr_block/README.md b/testdata/skip_attr_block/README.md index 59412eae..0432c481 100644 --- a/testdata/skip_attr_block/README.md +++ b/testdata/skip_attr_block/README.md @@ -21,12 +21,13 @@ Stable Rust gates custom (proc-macro) attributes on expressions and block expressions behind the unstable `stmt_expr_attributes` and `proc_macro_hygiene` features, so the direct form `#[mutants::skip] { ... }` does not compile on stable. This tree uses -`#[cfg_attr(mutants, mutants::skip)]` instead: the `mutants` cfg is never -set during normal builds or `cargo check --tests`, so rustc strips the -`cfg_attr` to nothing and never sees the inner `mutants::skip` proc-macro -attribute. cargo-mutants still recognises it via -`visit::attr_is_mutants_skip` and applies the suppression. This matches -the convention already used by `testdata/cfg_attr_mutants_skip`. +`#[cfg_attr(any(), mutants::skip)]` instead: the built-in `any()` cfg +predicate with no operands evaluates to false at compile time, so rustc +strips the whole `cfg_attr` and never sees the inner `mutants::skip` +proc-macro attribute. cargo-mutants still recognises it via +`visit::attr_is_mutants_skip` and applies the suppression. Using `any()` +rather than a made-up cfg name avoids the `unexpected_cfgs` lint that +rustc emits for unrecognised cfg flags. The unit tests under `src/visit/test/skip_attr_expr_block.rs` cover the direct `#[mutants::skip]` form (which only requires syn parsing). diff --git a/testdata/skip_attr_block/src/lib.rs b/testdata/skip_attr_block/src/lib.rs index 01a1961a..6a5a803a 100644 --- a/testdata/skip_attr_block/src/lib.rs +++ b/testdata/skip_attr_block/src/lib.rs @@ -5,9 +5,9 @@ //! On stable Rust, custom (proc-macro) attributes on expressions and //! block expressions are gated behind the unstable `stmt_expr_attributes` //! and `proc_macro_hygiene` features. To keep this testdata compilable on -//! stable, we use the `#[cfg_attr(mutants, mutants::skip)]` wrapping: the -//! `mutants` cfg is never set during normal builds or `cargo check -//! --tests`, so rustc strips the `cfg_attr` to nothing and never sees the +//! stable, we use the `#[cfg_attr(any(), mutants::skip)]` wrapping: the +//! built-in `any()` cfg predicate with no operands evaluates to false at +//! compile time, so rustc strips the whole `cfg_attr` and never sees the //! inner `mutants::skip` proc-macro attribute. cargo-mutants still //! recognises it via `visit::attr_is_mutants_skip` and applies the //! suppression. The unit tests under @@ -21,7 +21,7 @@ /// as usual. pub fn statement_position(a: i32, b: i32, c: i32, d: i32) -> i32 { let mut total = 0; - #[cfg_attr(mutants, mutants::skip)] + #[cfg_attr(any(), mutants::skip)] { total += a + b; } @@ -35,7 +35,7 @@ pub fn statement_position(a: i32, b: i32, c: i32, d: i32) -> i32 { /// produce no mutants. pub fn tail_block(a: i32, b: i32, c: i32) -> i32 { let _ = 0; - #[cfg_attr(mutants, mutants::skip)] + #[cfg_attr(any(), mutants::skip)] { if a > b { a + b @@ -51,7 +51,7 @@ pub fn tail_block(a: i32, b: i32, c: i32) -> i32 { /// Every operator inside the annotated block — `>`, `+`, `-` — must /// produce no mutants. pub fn labeled_block(a: i32, b: i32, c: i32) -> i32 { - #[cfg_attr(mutants, mutants::skip)] + #[cfg_attr(any(), mutants::skip)] 'block: { if a > b { break 'block a + b; From aa7c70cd780f566c44289d167dcaf5dc719677ac Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 28 May 2026 05:48:56 +0300 Subject: [PATCH 4/9] Document the cfg_attr(any(), ...) trick more thoroughly - 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> --- book/src/SUMMARY.md | 2 +- book/src/attrs.md | 54 +++++++++++++++++++++++++++++++-------------- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index f281855b..6d18a58d 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -9,7 +9,7 @@ - [Exit codes](exit-codes.md) - [The `mutants.out` directory](mutants-out.md) - [Skipping untestable code](skip.md) - - [Skipping functions with an attribute](attrs.md) + - [Skipping mutations with an attribute](attrs.md) - [Skipping function calls](skip_calls.md) - [Filtering files](skip_files.md) - [Filtering functions and mutants](filter_mutants.md) diff --git a/book/src/attrs.md b/book/src/attrs.md index 081079f3..3dba5543 100644 --- a/book/src/attrs.md +++ b/book/src/attrs.md @@ -64,19 +64,41 @@ mod test { (`foo(...)`), method-call (`x.foo(...)`), and unary expressions (`!x`, `-x`) — applies to the expression and everything nested inside it. -> **Caveat:** attributes on expressions are not supported on the stable -> Rust toolchain. As a workaround, you can use the form -> `#[cfg_attr(any(), mutants::skip)]`, which "hides" the attribute from -> the compiler but keeps it visible to cargo-mutants. This works even on -> stable Rust. The empty `any()` cfg predicate is built into rustc and -> always evaluates to false, so the inner attribute is never expanded. -> For example: -> -> ```rust,ignore -> fn frobnicate(x: i32) -> i32 { -> #[cfg_attr(any(), mutants::skip)] -> { -> x + 1 -> } -> } -> ``` +## Hiding the attribute from rustc with `cfg_attr(any(), ...)` + +Some uses of `#[mutants::skip]` are inconvenient or impossible to apply +directly: + +- Stable Rust does not accept custom proc-macro attributes on + expressions; placing `#[mutants::skip]` directly on a block or other + expression only compiles on nightly (it requires the unstable + `stmt_expr_attributes` and `proc_macro_hygiene` features). +- You may not want a crate-wide dependency on the `mutants` crate just + to suppress a few mutants. + +For both cases, wrap the attribute in a `cfg_attr` whose condition is +always false: + +```rust,ignore +fn frobnicate(x: i32) -> i32 { + #[cfg_attr(any(), mutants::skip)] + { + x + 1 + } +} +``` + +`any()` is built into rustc and, with no operands, always evaluates to +false. The compiler therefore strips the whole `cfg_attr` away and never +expands the inner `mutants::skip` proc-macro attribute. cargo-mutants +parses the source independently and still recognises the inner +`mutants::skip` directive and applies the suppression. + +Because the inner attribute is never expanded, code that only uses the +`cfg_attr(any(), ...)` form **does not need a dependency on the +`mutants` crate at all**. (A direct `#[mutants::skip]` does still need +the dependency so that rustc can resolve the attribute path.) + +Using `any()` rather than a made-up cfg name (such as `cfg(mutants)` or +`cfg(never)`) also avoids the `unexpected_cfgs` lint that rustc emits +for cfg names it does not know about. From ba4ecd335420a7a4c1e9faa8d2c1a2e76871074b Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Wed, 3 Jun 2026 08:43:39 +0300 Subject: [PATCH 5/9] Make skip-on-expressions testdata nightly-only via mutants_nightly cfg Custom proc-macro attributes on expressions (including block expressions) are gated behind unstable rustc features (stmt_expr_attributes and proc_macro_hygiene), so the direct `#[mutants::skip] { ... }` form does not compile on stable Rust. Be honest about this in the docs and add a real integration test that exercises the feature on nightly. Introduce a new `mutants_nightly` custom cfg as the opt-in for integration tests that depend on nightly-only Rust syntax: * Register the cfg in the top-level Cargo.toml's [lints.rust] so the `unexpected_cfgs` lint stays clean whether or not it is set. * Add testdata/skip_attr_block/, which uses the direct `#[mutants::skip]` form and gates the required feature attributes on `#![cfg_attr(mutants_nightly, feature(...))]`. * Add the integration test `check_tree_with_skip_attr_on_block_expressions` in tests/main.rs. It is `#[ignore]`d unless `mutants_nightly` is set, and forwards the cfg to the cargo-mutants subprocess (and therefore to the testdata's `cargo check --tests`) via RUSTFLAGS. * Document the convention in AGENTS.md so future nightly-only tests follow the same pattern. Documentation: * book/src/attrs.md: state honestly that expression-level `#[mutants::skip]` is currently nightly-only. Soften the pre-existing note about cargo-mutants ignoring the cfg_attr condition with a brief "This may change in future versions." * book/src/SUMMARY.md: align the TOC entry "Skipping mutations with an attribute" with the actual page title. * NEWS.md: mention the nightly requirement next to the block-expression entry. Implementation: * src/visit.rs: extend `attr_is_mutants_skip` to consume function-style cfg predicates (`any(...)`, `all(...)`, `not(...)`) and `name = "value"` predicates inside `cfg_attr`. Previously these caused parse_nested_meta to error out and silently drop the `mutants::skip` directive. The doc comment notes that this is an implementation detail and not a public guarantee. * src/visit/test/skip_attr_cfg_attr.rs: add unit tests pinning the behavior for all three predicate shapes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 31 ++++++++++ Cargo.toml | 13 +++++ NEWS.md | 2 +- book/src/attrs.md | 45 ++------------ src/visit.rs | 14 +++-- src/visit/test/skip_attr_cfg_attr.rs | 26 +++++---- testdata/skip_attr_block/Cargo_test.toml | 17 ++++-- testdata/skip_attr_block/README.md | 48 +++++++-------- testdata/skip_attr_block/src/lib.rs | 39 +++++++------ tests/main.rs | 74 ++++++++++++++---------- 10 files changed, 170 insertions(+), 139 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index da60b5c4..cd7c9105 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,6 +44,37 @@ Always use `copy_of_testdata()` to create a temporary copy before running tests. Describe the purpose of each testdata tree in its `Cargo.toml` or `README.md`. +### Nightly-only integration tests (`mutants_nightly`) + +Some integration tests exercise testdata trees that use nightly-only Rust syntax (for example, custom proc-macro attributes on expressions, which require the `stmt_expr_attributes` and `proc_macro_hygiene` feature gates). To keep the default test run usable on stable Rust, these tests are gated on a custom cfg named `mutants_nightly`: + +```rust +#[test] +#[cfg_attr( + not(mutants_nightly), + ignore = "requires --cfg=mutants_nightly and a nightly toolchain" +)] +fn check_tree_with_my_nightly_feature() { ... } +``` + +Their testdata enables the required nightly features under the same cfg: + +```rust +#![cfg_attr(mutants_nightly, feature(stmt_expr_attributes, proc_macro_hygiene))] +``` + +To run these tests: + +```bash +cargo +nightly nextest run --all-features \ + --config 'build.rustflags=["--cfg=mutants_nightly"]' \ + check_tree_with_my_nightly_feature +``` + +The integration test forwards the cfg to the cargo-mutants subprocess (and from there to the testdata's `cargo check --tests`) via `RUSTFLAGS="--cfg=mutants_nightly"` set on the subprocess env. + +`mutants_nightly` is registered as a known cfg in the top-level `Cargo.toml`'s `[lints.rust]` section and in the testdata tree's `Cargo_test.toml`, so the `unexpected_cfgs` lint doesn't warn when the cfg is absent. + ## Linting and formatting Run `cargo fmt` before committing. diff --git a/Cargo.toml b/Cargo.toml index 939a8e87..552cb797 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -131,6 +131,19 @@ lto = "thin" inherits = "test" debug = "none" +# Custom cfgs used by the test suite. Listing them here suppresses the +# `unexpected_cfgs` lint warning when the cfg is not set. +# +# `mutants_nightly` — opts into integration tests that exercise nightly-only +# syntax in their testdata trees (for example `#[mutants::skip]` placed +# directly on an expression). Set by running: +# +# cargo +nightly test --config 'build.rustflags=["--cfg=mutants_nightly"]' +# +# See AGENTS.md for details. +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(mutants_nightly)'] } + # Config for [workspace.metadata.release] pre-release-replacements = [ diff --git a/NEWS.md b/NEWS.md index 18c2b304..212ac35d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,7 +2,7 @@ ## Unreleased -- New: `#[mutants::skip]` is now honoured on block expressions (`{ ... }`), in both statement and expression position. All mutants generated inside the annotated block are suppressed. +- New: `#[mutants::skip]` is now honoured on block expressions (`{ ... }`), in both statement and expression position. All mutants generated inside the annotated block are suppressed. Note that custom proc-macro attributes on expressions require a nightly Rust toolchain (`stmt_expr_attributes` and `proc_macro_hygiene` feature gates). - New: mutate `NonZero` into `1`, and also `-1` when `T` is or may be signed. diff --git a/book/src/attrs.md b/book/src/attrs.md index 3dba5543..f75f604f 100644 --- a/book/src/attrs.md +++ b/book/src/attrs.md @@ -14,7 +14,7 @@ code. It only flags the item for cargo-mutants. **Note:** `cargo-mutants` does not evaluate the `cfg_attr` condition; the inner `mutants::skip` is always honoured regardless of whether the condition -would hold during compilation. +would hold during compilation. This may change in future versions. You may want to also add a comment explaining why the item is skipped. @@ -63,42 +63,7 @@ mod test { block (`{ ... }`), `match`, struct literal (`Foo { ... }`), call (`foo(...)`), method-call (`x.foo(...)`), and unary expressions (`!x`, `-x`) — applies to the expression and everything nested inside it. - -## Hiding the attribute from rustc with `cfg_attr(any(), ...)` - -Some uses of `#[mutants::skip]` are inconvenient or impossible to apply -directly: - -- Stable Rust does not accept custom proc-macro attributes on - expressions; placing `#[mutants::skip]` directly on a block or other - expression only compiles on nightly (it requires the unstable - `stmt_expr_attributes` and `proc_macro_hygiene` features). -- You may not want a crate-wide dependency on the `mutants` crate just - to suppress a few mutants. - -For both cases, wrap the attribute in a `cfg_attr` whose condition is -always false: - -```rust,ignore -fn frobnicate(x: i32) -> i32 { - #[cfg_attr(any(), mutants::skip)] - { - x + 1 - } -} -``` - -`any()` is built into rustc and, with no operands, always evaluates to -false. The compiler therefore strips the whole `cfg_attr` away and never -expands the inner `mutants::skip` proc-macro attribute. cargo-mutants -parses the source independently and still recognises the inner -`mutants::skip` directive and applies the suppression. - -Because the inner attribute is never expanded, code that only uses the -`cfg_attr(any(), ...)` form **does not need a dependency on the -`mutants` crate at all**. (A direct `#[mutants::skip]` does still need -the dependency so that rustc can resolve the attribute path.) - -Using `any()` rather than a made-up cfg name (such as `cfg(mutants)` or -`cfg(never)`) also avoids the `unexpected_cfgs` lint that rustc emits -for cfg names it does not know about. + Note that the `#[mutants::skip]` macro on expressions requires the + unstable `stmt_expr_attributes` and `proc_macro_hygiene` features, so + expression-level `#[mutants::skip]` is currently only usable on a + nightly Rust toolchain. diff --git a/src/visit.rs b/src/visit.rs index 0f8f12bc..f0635311 100644 --- a/src/visit.rs +++ b/src/visit.rs @@ -934,9 +934,13 @@ fn path_ends_with(path: &syn::Path, ident: &str) -> bool { /// True if the attribute contains `mutants::skip`. /// -/// This for example returns true for `#[mutants::skip]`, -/// `#[cfg_attr(test, mutants::skip)]`, or -/// `#[cfg_attr(any(), mutants::skip)]`. +/// This for example returns true for `#[mutants::skip]` or for +/// `#[cfg_attr(, mutants::skip)]` regardless of what `` is — +/// cargo-mutants does not evaluate the cfg condition, so any predicate +/// shape (a plain ident like `test`, a function-style predicate like +/// `any()`/`not(...)`, or a `name = "value"` form) is treated the same. +/// This is an implementation detail, not a public guarantee; see +/// `book/src/attrs.md` for what we actually promise to users. fn attr_is_mutants_skip(attr: &Attribute) -> bool { if path_is(attr.path(), &["mutants", "skip"]) { return true; @@ -950,8 +954,8 @@ fn attr_is_mutants_skip(attr: &Attribute) -> bool { skip = true; } else if meta.input.peek(syn::token::Paren) { // Function-style cfg predicate like `any(...)`, `all(...)`, `not(...)`. - // We don't evaluate the predicate; just consume and discard its contents - // so parse_nested_meta can advance to the next item. + // We don't evaluate the predicate; just consume and discard its + // contents so parse_nested_meta can advance to the next item. let content; let _ = syn::parenthesized!(content in meta.input); let _: proc_macro2::TokenStream = content.parse()?; diff --git a/src/visit/test/skip_attr_cfg_attr.rs b/src/visit/test/skip_attr_cfg_attr.rs index de45941b..bc930282 100644 --- a/src/visit/test/skip_attr_cfg_attr.rs +++ b/src/visit/test/skip_attr_cfg_attr.rs @@ -72,14 +72,16 @@ fn cfg_attr_mutants_skip_on_mod_suppresses_inner_items() { ); } -/// Function-style cfg predicates like `any(...)`, `all(...)`, and `not(...)` -/// inside `cfg_attr` must still be recognised as carrying a `mutants::skip` -/// directive. `cfg_attr(any(), ...)` is the recommended stable-Rust -/// workaround for skipping expression-position mutants because the empty -/// `any()` evaluates to false at compile time, so rustc never expands the -/// inner attribute. +// The tests below pin the implementation detail that `attr_is_mutants_skip` +// ignores the cfg condition for *every* shape of `cfg_attr` predicate, not +// just plain identifiers like `test`. This keeps the behavior consistent +// regardless of how the user spells the condition. It is not a public +// guarantee — `book/src/attrs.md` deliberately does not promise that the +// condition is ignored — but the consistency matters internally because a +// silently-dropped `mutants::skip` would be very surprising. + #[test] -fn cfg_attr_with_any_predicate_is_recognised_as_mutants_skip() { +fn cfg_attr_with_function_style_predicate_still_treats_mutants_skip_as_skip() { let mutants = mutate_source_str( indoc! {r#" #[cfg_attr(any(), mutants::skip)] @@ -98,7 +100,7 @@ fn cfg_attr_with_any_predicate_is_recognised_as_mutants_skip() { assert!( !names.iter().any(|n| n.contains("add")), - "cfg_attr(any(), mutants::skip) should suppress mutants on the fn: {names:?}" + "cfg_attr with a function-style predicate must still be recognised as carrying mutants::skip: {names:?}" ); assert!( names.iter().any(|n| n.contains("outside")), @@ -107,7 +109,7 @@ fn cfg_attr_with_any_predicate_is_recognised_as_mutants_skip() { } #[test] -fn cfg_attr_with_not_all_predicate_is_recognised_as_mutants_skip() { +fn cfg_attr_with_nested_function_style_predicate_still_treats_mutants_skip_as_skip() { let mutants = mutate_source_str( indoc! {r#" #[cfg_attr(not(all()), mutants::skip)] @@ -122,12 +124,12 @@ fn cfg_attr_with_not_all_predicate_is_recognised_as_mutants_skip() { assert!( !names.iter().any(|n| n.contains("add")), - "cfg_attr(not(all()), mutants::skip) should suppress mutants on the fn: {names:?}" + "cfg_attr with a nested function-style predicate must still be recognised as carrying mutants::skip: {names:?}" ); } #[test] -fn cfg_attr_with_name_value_predicate_is_recognised_as_mutants_skip() { +fn cfg_attr_with_name_value_predicate_still_treats_mutants_skip_as_skip() { let mutants = mutate_source_str( indoc! {r#" #[cfg_attr(target_os = "linux", mutants::skip)] @@ -142,6 +144,6 @@ fn cfg_attr_with_name_value_predicate_is_recognised_as_mutants_skip() { assert!( !names.iter().any(|n| n.contains("add")), - "cfg_attr(name = \"value\", mutants::skip) should suppress mutants on the fn: {names:?}" + "cfg_attr with a name = value predicate must still be recognised as carrying mutants::skip: {names:?}" ); } diff --git a/testdata/skip_attr_block/Cargo_test.toml b/testdata/skip_attr_block/Cargo_test.toml index 8e2fc260..832b22ca 100644 --- a/testdata/skip_attr_block/Cargo_test.toml +++ b/testdata/skip_attr_block/Cargo_test.toml @@ -1,11 +1,16 @@ [package] name = "cargo-mutants-testdata-skip-attr-block" -version = "0.1.0" -edition = "2018" +version = "0.0.0" +edition = "2021" publish = false -# No dependency on the `mutants` crate is needed: the proc-macro is -# wrapped in `cfg_attr(any(), ...)` and is therefore never expanded. +# Custom proc-macro attributes on expressions and block expressions are +# nightly-only on the Rust toolchain, so the source uses +# `#![cfg_attr(mutants_nightly, feature(...))]` to enable the required +# feature gates only when the test is opted in via the `mutants_nightly` +# cfg. See AGENTS.md and the crate-level docs in `src/lib.rs`. +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(mutants_nightly)'] } -[lib] -doctest = false +[dependencies] +mutants = "0.0.3" diff --git a/testdata/skip_attr_block/README.md b/testdata/skip_attr_block/README.md index 0432c481..dfa3b59d 100644 --- a/testdata/skip_attr_block/README.md +++ b/testdata/skip_attr_block/README.md @@ -1,36 +1,28 @@ # skip_attr_block -Exercises `#[mutants::skip]` placed on a block expression `{ ... }` inside a -function body. Each function in `src/lib.rs` annotates a block in a different -syntactic position and pairs it with un-annotated sibling code so that the -expected behaviour is unambiguous: +Exercises `#[mutants::skip]` placed directly on a block expression +`{ ... }` inside a function body. Each function in `src/lib.rs` annotates +a block in a different syntactic position and pairs it with un-annotated +sibling code so that the expected behavior is unambiguous: -- `statement_position` — `#[..] { ... }` used as a statement. -- `tail_block` — `#[..] { ... }` used as the function's tail expression. -- `labeled_block` — `#[..] 'lbl: { ... }` used as the function's tail +- `statement_position` — `#[mutants::skip] { ... }` used as a statement. +- `tail_block` — `#[mutants::skip] { ... }` used as the function's tail expression. -- `unannotated_sibling` — no skip attribute; mutants must still be produced. +- `labeled_block` — `#[mutants::skip] 'lbl: { ... }` used as the + function's tail expression. +- `unannotated_sibling` — no skip attribute; mutants must still be + produced. -Inside each annotated block every mutant cargo-mutants would normally produce -must be suppressed; in `unannotated_sibling`, mutants must be produced as -usual. +Inside each annotated block every mutant cargo-mutants would normally +produce must be suppressed; in `unannotated_sibling`, mutants must be +produced as usual. -## Why `cfg_attr` wrapping? +## Nightly-only -Stable Rust gates custom (proc-macro) attributes on expressions and block -expressions behind the unstable `stmt_expr_attributes` and -`proc_macro_hygiene` features, so the direct form `#[mutants::skip] { ... }` -does not compile on stable. This tree uses -`#[cfg_attr(any(), mutants::skip)]` instead: the built-in `any()` cfg -predicate with no operands evaluates to false at compile time, so rustc -strips the whole `cfg_attr` and never sees the inner `mutants::skip` -proc-macro attribute. cargo-mutants still recognises it via -`visit::attr_is_mutants_skip` and applies the suppression. Using `any()` -rather than a made-up cfg name avoids the `unexpected_cfgs` lint that -rustc emits for unrecognised cfg flags. +Custom proc-macro attributes on expressions and block expressions are +nightly-only (`stmt_expr_attributes` and `proc_macro_hygiene`). The +crate-level `#![cfg_attr(mutants_nightly, feature(...))]` enables those +features only when the `mutants_nightly` cfg is set, and the integration +test that consumes this tree is `#[ignore]`d unless the same cfg is set. -The unit tests under `src/visit/test/skip_attr_expr_block.rs` cover the -direct `#[mutants::skip]` form (which only requires syn parsing). - -Because the inner proc-macro is never expanded, this tree does not need -to depend on the `mutants` crate at all. +See `AGENTS.md` in the repository root for how to opt in. diff --git a/testdata/skip_attr_block/src/lib.rs b/testdata/skip_attr_block/src/lib.rs index 6a5a803a..423f44f8 100644 --- a/testdata/skip_attr_block/src/lib.rs +++ b/testdata/skip_attr_block/src/lib.rs @@ -1,18 +1,22 @@ -//! Verify that `#[mutants::skip]` on a block expression `{ ... }` -//! suppresses every mutant generated inside that block, while mutants -//! in surrounding code are still produced as usual. +//! Verify that `#[mutants::skip]` placed directly on a block expression +//! `{ ... }` inside a function body suppresses every mutant generated +//! inside that block, while mutants in surrounding code are still +//! produced as usual. //! -//! On stable Rust, custom (proc-macro) attributes on expressions and -//! block expressions are gated behind the unstable `stmt_expr_attributes` -//! and `proc_macro_hygiene` features. To keep this testdata compilable on -//! stable, we use the `#[cfg_attr(any(), mutants::skip)]` wrapping: the -//! built-in `any()` cfg predicate with no operands evaluates to false at -//! compile time, so rustc strips the whole `cfg_attr` and never sees the -//! inner `mutants::skip` proc-macro attribute. cargo-mutants still -//! recognises it via `visit::attr_is_mutants_skip` and applies the -//! suppression. The unit tests under -//! `src/visit/test/skip_attr_expr_block.rs` additionally cover the direct -//! `#[mutants::skip]` form, which only needs to parse through syn. +//! Custom proc-macro attributes on expressions and block expressions +//! require the unstable `stmt_expr_attributes` and `proc_macro_hygiene` +//! features, which are only available on nightly Rust. We therefore +//! gate the feature attributes on `cfg(mutants_nightly)`. The +//! integration test that consumes this tree is `#[ignore]`d unless +//! `mutants_nightly` is set, and the same cfg is forwarded to the +//! `cargo check --tests` subprocess that cargo-mutants runs against +//! this tree, so the feature gates kick in there too. See AGENTS.md +//! for the convention. + +#![cfg_attr( + mutants_nightly, + feature(stmt_expr_attributes, proc_macro_hygiene) +)] /// `#[mutants::skip]` on a block used as a statement. /// @@ -21,7 +25,7 @@ /// as usual. pub fn statement_position(a: i32, b: i32, c: i32, d: i32) -> i32 { let mut total = 0; - #[cfg_attr(any(), mutants::skip)] + #[mutants::skip] { total += a + b; } @@ -35,7 +39,7 @@ pub fn statement_position(a: i32, b: i32, c: i32, d: i32) -> i32 { /// produce no mutants. pub fn tail_block(a: i32, b: i32, c: i32) -> i32 { let _ = 0; - #[cfg_attr(any(), mutants::skip)] + #[mutants::skip] { if a > b { a + b @@ -51,7 +55,7 @@ pub fn tail_block(a: i32, b: i32, c: i32) -> i32 { /// Every operator inside the annotated block — `>`, `+`, `-` — must /// produce no mutants. pub fn labeled_block(a: i32, b: i32, c: i32) -> i32 { - #[cfg_attr(any(), mutants::skip)] + #[mutants::skip] 'block: { if a > b { break 'block a + b; @@ -70,4 +74,3 @@ pub fn unannotated_sibling(a: i32, b: i32) -> i32 { let y = { a / b }; x | y } - diff --git a/tests/main.rs b/tests/main.rs index ce74a933..4568d617 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -2635,48 +2635,64 @@ fn check_tree_with_mutants_skip() { /// The `skip_attr_block` tree pairs each annotated block with un-annotated /// surrounding code, so the absence of mutants from the annotated lines and /// their presence on the un-annotated lines is the actual assertion. We use -/// `--check` so the listed mutants are also confirmed to compile on stable, -/// ruling out regressions where suppression accidentally rewrites the source. +/// `--check` so the listed mutants are also confirmed to compile, ruling +/// out regressions where suppression accidentally rewrites the source. /// -/// The tree uses the `#[cfg_attr(mutants, mutants::skip)]` form because -/// stable Rust does not accept custom proc-macro attributes on expressions; -/// see the testdata's README for details. The direct `#[mutants::skip]` form -/// is covered by unit tests in `src/visit/test/skip_attr_expr_block.rs`. -#[test] +/// Custom proc-macro attributes on expressions are nightly-only on rustc, +/// so the testdata uses `#![cfg_attr(mutants_nightly, feature(...))]` to +/// enable the required feature gates and this test is `#[ignore]`d unless +/// `mutants_nightly` is set. To run it, build the test binary with: +/// +/// ```text +/// cargo +nightly test --config 'build.rustflags=["--cfg=mutants_nightly"]' \ +/// check_tree_with_skip_attr_on_block_expressions +/// ``` +/// +/// The cfg is forwarded to the cargo-mutants subprocess (and therefore to +/// the testdata's `cargo check --tests` invocation) via `RUSTFLAGS`. The +/// direct `#[mutants::skip]` form is additionally covered by unit tests in +/// `src/visit/test/skip_attr_expr_block.rs`, which only parse the source +/// through syn and so work on stable. +#[test] +#[cfg_attr( + not(mutants_nightly), + ignore = "requires --cfg=mutants_nightly and a nightly toolchain; see test docs" +)] fn check_tree_with_skip_attr_on_block_expressions() { let tmp_src_dir = copy_of_testdata("skip_attr_block"); run() .arg("mutants") .args(["--check", "--no-times", "--no-shuffle"]) .current_dir(tmp_src_dir.path()) + .env("RUSTFLAGS", "--cfg=mutants_nightly") .env_remove("RUST_BACKTRACE") .assert() .success() .stdout(indoc! { r" Found 22 mutants to test ok Unmutated baseline - ok src/lib.rs:23:5: replace statement_position -> i32 with 0 - ok src/lib.rs:23:5: replace statement_position -> i32 with 1 - ok src/lib.rs:23:5: replace statement_position -> i32 with -1 - ok src/lib.rs:28:11: replace + with - in statement_position - ok src/lib.rs:28:11: replace + with * in statement_position - ok src/lib.rs:28:16: replace - with + in statement_position - ok src/lib.rs:28:16: replace - with / in statement_position - ok src/lib.rs:37:5: replace tail_block -> i32 with 0 - ok src/lib.rs:37:5: replace tail_block -> i32 with 1 - ok src/lib.rs:37:5: replace tail_block -> i32 with -1 - ok src/lib.rs:54:5: replace labeled_block -> i32 with 0 - ok src/lib.rs:54:5: replace labeled_block -> i32 with 1 - ok src/lib.rs:54:5: replace labeled_block -> i32 with -1 - ok src/lib.rs:69:5: replace unannotated_sibling -> i32 with 0 - ok src/lib.rs:69:5: replace unannotated_sibling -> i32 with 1 - ok src/lib.rs:69:5: replace unannotated_sibling -> i32 with -1 - ok src/lib.rs:69:17: replace * with + in unannotated_sibling - ok src/lib.rs:69:17: replace * with / in unannotated_sibling - ok src/lib.rs:70:17: replace / with % in unannotated_sibling - ok src/lib.rs:70:17: replace / with * in unannotated_sibling - ok src/lib.rs:71:7: replace | with & in unannotated_sibling - ok src/lib.rs:71:7: replace | with ^ in unannotated_sibling + ok src/lib.rs:27:5: replace statement_position -> i32 with 0 + ok src/lib.rs:27:5: replace statement_position -> i32 with 1 + ok src/lib.rs:27:5: replace statement_position -> i32 with -1 + ok src/lib.rs:32:11: replace + with - in statement_position + ok src/lib.rs:32:11: replace + with * in statement_position + ok src/lib.rs:32:16: replace - with + in statement_position + ok src/lib.rs:32:16: replace - with / in statement_position + ok src/lib.rs:41:5: replace tail_block -> i32 with 0 + ok src/lib.rs:41:5: replace tail_block -> i32 with 1 + ok src/lib.rs:41:5: replace tail_block -> i32 with -1 + ok src/lib.rs:58:5: replace labeled_block -> i32 with 0 + ok src/lib.rs:58:5: replace labeled_block -> i32 with 1 + ok src/lib.rs:58:5: replace labeled_block -> i32 with -1 + ok src/lib.rs:73:5: replace unannotated_sibling -> i32 with 0 + ok src/lib.rs:73:5: replace unannotated_sibling -> i32 with 1 + ok src/lib.rs:73:5: replace unannotated_sibling -> i32 with -1 + ok src/lib.rs:73:17: replace * with + in unannotated_sibling + ok src/lib.rs:73:17: replace * with / in unannotated_sibling + ok src/lib.rs:74:17: replace / with % in unannotated_sibling + ok src/lib.rs:74:17: replace / with * in unannotated_sibling + ok src/lib.rs:75:7: replace | with & in unannotated_sibling + ok src/lib.rs:75:7: replace | with ^ in unannotated_sibling 22 mutants tested: 22 succeeded "}) .stderr(""); From e6777b663e4e83899e34bf27581f2793795bd959 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Wed, 3 Jun 2026 09:26:46 +0300 Subject: [PATCH 6/9] NEWS: refer to the mutants::skip attribute directly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 39bc21af..5b4c53e0 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,7 +2,7 @@ ## Unreleased -- New: `#[mutants::skip]` is now honoured on block expressions (`{ ... }`), in both statement and expression position. All mutants generated inside the annotated block are suppressed. Note that custom proc-macro attributes on expressions require a nightly Rust toolchain (`stmt_expr_attributes` and `proc_macro_hygiene` feature gates). +- New: `#[mutants::skip]` is now honoured on block expressions (`{ ... }`), in both statement and expression position. All mutants generated inside the annotated block are suppressed. Note that the `#[mutants::skip]` attribute on expressions requires a nightly Rust toolchain (`stmt_expr_attributes` and `proc_macro_hygiene` feature gates). - Fixed: Support the new TOML 1.1 syntax that's supported by Cargo in Rust 1.94. Thanks to @Coruscant11. From 862804cd5c20931424f6340ea2f9d282f3d4e0fb Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 4 Jun 2026 16:59:32 +0300 Subject: [PATCH 7/9] Add testdata integration test for #[mutants::skip] on non-block expressions The block-expression skip is end-to-end covered by the skip_attr_block testdata tree and check_tree_with_skip_attr_on_block_expressions; the remaining expression genres (call, method call, match, struct literal, unary) were only covered by parser-only unit tests in src/visit/test/skip_attr_expr_*.rs. Add a skip_attr_expressions testdata tree that pairs an annotated expression of each remaining genre with an un-annotated sibling of the same genre in the same function, plus the matching cargo-mutants --check integration test check_tree_with_skip_attr_on_expressions. Like the block test, both the tree and the test are nightly-only via the mutants_nightly cfg (stmt_expr_attributes + proc_macro_hygiene are required to attach a custom attribute to an expression on rustc). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../skip_attr_expressions/Cargo_test.toml | 16 +++ testdata/skip_attr_expressions/README.md | 32 ++++++ testdata/skip_attr_expressions/src/lib.rs | 104 ++++++++++++++++++ tests/main.rs | 82 ++++++++++++++ 4 files changed, 234 insertions(+) create mode 100644 testdata/skip_attr_expressions/Cargo_test.toml create mode 100644 testdata/skip_attr_expressions/README.md create mode 100644 testdata/skip_attr_expressions/src/lib.rs diff --git a/testdata/skip_attr_expressions/Cargo_test.toml b/testdata/skip_attr_expressions/Cargo_test.toml new file mode 100644 index 00000000..7a048625 --- /dev/null +++ b/testdata/skip_attr_expressions/Cargo_test.toml @@ -0,0 +1,16 @@ +[package] +name = "cargo-mutants-testdata-skip-attr-expressions" +version = "0.0.0" +edition = "2021" +publish = false + +# Custom proc-macro attributes on expressions and statements are +# nightly-only on the Rust toolchain, so the source uses +# `#![cfg_attr(mutants_nightly, feature(...))]` to enable the required +# feature gates only when the test is opted in via the `mutants_nightly` +# cfg. See AGENTS.md and the crate-level docs in `src/lib.rs`. +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(mutants_nightly)'] } + +[dependencies] +mutants = "0.0.3" diff --git a/testdata/skip_attr_expressions/README.md b/testdata/skip_attr_expressions/README.md new file mode 100644 index 00000000..f9a54b88 --- /dev/null +++ b/testdata/skip_attr_expressions/README.md @@ -0,0 +1,32 @@ +# skip_attr_expressions + +Exercises `#[mutants::skip]` placed directly on expressions other than +block expressions. Each function in `src/lib.rs` pairs an annotated +expression of a given genre with an un-annotated sibling expression of +the same genre so that the expected behavior is unambiguous: + +- `call_expr` — `#[mutants::skip]` on a call expression suppresses + mutants generated for its argument expressions. +- `method_call_expr` — same, on a method-call expression. +- `match_expr` — on a `match` expression, suppresses both arm-deletion + and guard-replacement mutants. +- `struct_expr` — on a struct literal that has a `..Default::default()` + base, suppresses the field-deletion mutants generated for the literal. +- `unary_expr` — on a unary `!` expression, suppresses the unary + deletion mutant. + +Inside each annotated expression every mutant cargo-mutants would +normally produce must be suppressed; in the un-annotated sibling in the +same function, mutants must be produced as usual. Block-expression skip +(`#[mutants::skip] { ... }`) is covered separately by the `skip_attr_block` +tree. + +## Nightly-only + +Custom proc-macro attributes on expressions and statements are +nightly-only (`stmt_expr_attributes` and `proc_macro_hygiene`). The +crate-level `#![cfg_attr(mutants_nightly, feature(...))]` enables those +features only when the `mutants_nightly` cfg is set, and the integration +test that consumes this tree is `#[ignore]`d unless the same cfg is set. + +See `AGENTS.md` in the repository root for how to opt in. diff --git a/testdata/skip_attr_expressions/src/lib.rs b/testdata/skip_attr_expressions/src/lib.rs new file mode 100644 index 00000000..d5105064 --- /dev/null +++ b/testdata/skip_attr_expressions/src/lib.rs @@ -0,0 +1,104 @@ +//! Verify that `#[mutants::skip]` placed directly on expressions other +//! than block expressions suppresses every mutant generated inside the +//! annotated expression, while sibling expressions of the same genre in +//! the same function are still mutated as usual. +//! +//! Custom proc-macro attributes on expressions and statements require +//! the unstable `stmt_expr_attributes` and `proc_macro_hygiene` +//! features, which are only available on nightly Rust. We therefore +//! gate the feature attributes on `cfg(mutants_nightly)`. The +//! integration test that consumes this tree is `#[ignore]`d unless +//! `mutants_nightly` is set, and the same cfg is forwarded to the +//! `cargo check --tests` subprocess that cargo-mutants runs against +//! this tree, so the feature gates kick in there too. See AGENTS.md +//! for the convention. +//! +//! Block-expression skip is covered separately by the `skip_attr_block` +//! testdata tree. + +#![cfg_attr( + mutants_nightly, + feature(stmt_expr_attributes, proc_macro_hygiene) +)] + +/// `#[mutants::skip]` on a call expression. +/// +/// The `+` operator inside the annotated call's argument must produce +/// no mutants. The `-` operator inside the un-annotated sibling call +/// must still be mutated as usual. +pub fn call_expr(a: i32, b: i32, c: i32, d: i32) { + #[mutants::skip] + helper(a + b); + helper(c - d); +} + +/// `#[mutants::skip]` on a method-call expression. +/// +/// The `+` operator inside the annotated method call's argument must +/// produce no mutants. The `-` operator inside the un-annotated sibling +/// method call must still be mutated as usual. +pub fn method_call_expr(s: &Holder, a: i32, b: i32, c: i32, d: i32) { + #[mutants::skip] + s.frob(a + b); + s.frob(c - d); +} + +/// `#[mutants::skip]` on a `match` expression. +/// +/// The annotated match must produce no arm-deletion, guard-replacement +/// or operator mutants. The un-annotated sibling match must still +/// produce all of those mutants. +pub fn match_expr(x: i32, y: i32) { + let _ = #[mutants::skip] + match x { + 0 => "zero", + n if n > y => "gt", + _ => "other", + }; + let _ = match x { + 0 => "zero", + n if n > y => "gt", + _ => "other", + }; +} + +/// `#[mutants::skip]` on a struct-literal expression. +/// +/// The annotated struct literal must produce no field-deletion mutants. +/// The un-annotated sibling literal must still produce them. +pub fn struct_expr() { + let _ = #[mutants::skip] + Settings { + enabled: true, + count: 1, + ..Default::default() + }; + let _ = Settings { + enabled: true, + count: 1, + ..Default::default() + }; +} + +/// `#[mutants::skip]` on a unary `!` expression. +/// +/// The annotated `!b` must produce no `delete !` mutant. The +/// un-annotated sibling `!b` must still produce one. +pub fn unary_expr(b: bool) { + let _ = #[mutants::skip] !b; + let _ = !b; +} + +#[derive(Default)] +pub struct Settings { + pub enabled: bool, + pub count: i32, +} + +pub struct Holder; + +impl Holder { + pub fn frob(&self, _x: i32) {} +} + +fn helper(_x: i32) {} diff --git a/tests/main.rs b/tests/main.rs index 4568d617..364fbefd 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -2709,6 +2709,88 @@ fn check_tree_with_skip_attr_on_block_expressions() { ); } +/// `#[mutants::skip]` placed directly on expressions other than block +/// expressions must suppress every mutant generated inside the annotated +/// expression, while sibling expressions of the same genre in the same +/// function are still mutated as usual. +/// +/// The `skip_attr_expressions` tree pairs an annotated expression of each +/// supported genre — call, method call, `match`, struct literal and unary — +/// with an un-annotated sibling expression of the same genre in the same +/// function. The absence of mutants on the annotated expression's lines and +/// their presence on the sibling's lines is the actual assertion. We use +/// `--check` so the listed mutants are also confirmed to compile, ruling +/// out regressions where suppression accidentally rewrites the source. +/// +/// Custom proc-macro attributes on expressions and statements are +/// nightly-only on rustc, so the testdata uses +/// `#![cfg_attr(mutants_nightly, feature(...))]` to enable the required +/// feature gates and this test is `#[ignore]`d unless `mutants_nightly` +/// is set. To run it, build the test binary with: +/// +/// ```text +/// cargo +nightly test --config 'build.rustflags=["--cfg=mutants_nightly"]' \ +/// check_tree_with_skip_attr_on_expressions +/// ``` +/// +/// The cfg is forwarded to the cargo-mutants subprocess (and therefore to +/// the testdata's `cargo check --tests` invocation) via `RUSTFLAGS`. The +/// direct `#[mutants::skip]` form is additionally covered by unit tests in +/// `src/visit/test/skip_attr_expr_*.rs`, which only parse the source +/// through syn and so work on stable. Block-expression skip has its own +/// dedicated integration test `check_tree_with_skip_attr_on_block_expressions`. +#[test] +#[cfg_attr( + not(mutants_nightly), + ignore = "requires --cfg=mutants_nightly and a nightly toolchain; see test docs" +)] +fn check_tree_with_skip_attr_on_expressions() { + let tmp_src_dir = copy_of_testdata("skip_attr_expressions"); + run() + .arg("mutants") + .args(["--check", "--no-times", "--no-shuffle"]) + .current_dir(tmp_src_dir.path()) + .env("RUSTFLAGS", "--cfg=mutants_nightly") + .env_remove("RUST_BACKTRACE") + .assert() + .success() + .stdout(indoc! { r" + Found 18 mutants to test + ok Unmutated baseline + ok src/lib.rs:30:5: replace call_expr with () + ok src/lib.rs:32:14: replace - with + in call_expr + ok src/lib.rs:32:14: replace - with / in call_expr + ok src/lib.rs:41:5: replace method_call_expr with () + ok src/lib.rs:43:14: replace - with + in method_call_expr + ok src/lib.rs:43:14: replace - with / in method_call_expr + ok src/lib.rs:52:5: replace match_expr with () + ok src/lib.rs:59:9: delete match arm 0 in match_expr + ok src/lib.rs:60:14: replace match guard n > y with true in match_expr + ok src/lib.rs:60:14: replace match guard n > y with false in match_expr + ok src/lib.rs:60:16: replace > with == in match_expr + ok src/lib.rs:60:16: replace > with < in match_expr + ok src/lib.rs:60:16: replace > with >= in match_expr + ok src/lib.rs:70:5: replace struct_expr with () + ok src/lib.rs:77:9: delete field enabled from struct Settings expression in struct_expr + ok src/lib.rs:78:9: delete field count from struct Settings expression in struct_expr + ok src/lib.rs:88:5: replace unary_expr with () + ok src/lib.rs:89:13: delete ! in unary_expr + 18 mutants tested: 18 succeeded + "}) + .stderr(""); + assert_eq!( + outcome_json_counts(&tmp_src_dir), + serde_json::json!({ + "caught": 0, + "missed": 0, + "success": 18, + "timeout": 0, + "unviable": 0, + "total_mutants": 18, + }) + ); +} + #[test] fn check_tree_where_build_fails() { let tmp_src_dir = copy_of_testdata("typecheck_fails"); From 4a1394d36d516c02359ebb9421a05e52401b6bb7 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sat, 6 Jun 2026 23:27:15 +0300 Subject: [PATCH 8/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- testdata/skip_attr_block/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testdata/skip_attr_block/README.md b/testdata/skip_attr_block/README.md index dfa3b59d..dea22cb7 100644 --- a/testdata/skip_attr_block/README.md +++ b/testdata/skip_attr_block/README.md @@ -8,7 +8,7 @@ sibling code so that the expected behavior is unambiguous: - `statement_position` — `#[mutants::skip] { ... }` used as a statement. - `tail_block` — `#[mutants::skip] { ... }` used as the function's tail expression. -- `labeled_block` — `#[mutants::skip] 'lbl: { ... }` used as the +- `labeled_block` — `#[mutants::skip] 'block: { ... }` used as the function's tail expression. - `unannotated_sibling` — no skip attribute; mutants must still be produced. From 817e7485ebbfba69900e97a723c514bf7c96e89a Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 7 Jun 2026 10:41:40 +0300 Subject: [PATCH 9/9] Drop duplicative skip-attr integration tests The two integration tests added in this branch (check_tree_with_skip_attr_on_block_expressions and check_tree_with_skip_attr_on_expressions) test the same mutation-generator behavior that's already covered by the unit tests in src/visit/test/skip_attr_expr_block.rs and skip_attr_cfg_attr.rs. The orthogonal concerns they appeared to add value for (mutated source compiles, exact stdout/JSON output, line+column spans) are already exercised by many other integration tests in the suite. The one genuinely novel concern (inner #![cfg_attr(..., feature(...))] preservation through mutation rewriting) is not skip-attribute-specific and is better left as a manual smoke test on testdata/nightly_only, matching the existing project convention documented there. Additionally, CI never sets --cfg=mutants_nightly, so both tests were always ignored in CI runs and protected against nothing in practice. Removes: - Both integration tests from tests/main.rs - testdata/skip_attr_block/ and testdata/skip_attr_expressions/ trees - The mutants_nightly cfg registration in Cargo.toml - The Nightly-only integration tests section in AGENTS.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 31 ---- Cargo.toml | 13 -- testdata/skip_attr_block/Cargo_test.toml | 16 -- testdata/skip_attr_block/README.md | 28 --- testdata/skip_attr_block/src/lib.rs | 76 -------- .../skip_attr_expressions/Cargo_test.toml | 16 -- testdata/skip_attr_expressions/README.md | 32 ---- testdata/skip_attr_expressions/src/lib.rs | 104 ----------- tests/main.rs | 162 ------------------ 9 files changed, 478 deletions(-) delete mode 100644 testdata/skip_attr_block/Cargo_test.toml delete mode 100644 testdata/skip_attr_block/README.md delete mode 100644 testdata/skip_attr_block/src/lib.rs delete mode 100644 testdata/skip_attr_expressions/Cargo_test.toml delete mode 100644 testdata/skip_attr_expressions/README.md delete mode 100644 testdata/skip_attr_expressions/src/lib.rs diff --git a/AGENTS.md b/AGENTS.md index cd7c9105..da60b5c4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,37 +44,6 @@ Always use `copy_of_testdata()` to create a temporary copy before running tests. Describe the purpose of each testdata tree in its `Cargo.toml` or `README.md`. -### Nightly-only integration tests (`mutants_nightly`) - -Some integration tests exercise testdata trees that use nightly-only Rust syntax (for example, custom proc-macro attributes on expressions, which require the `stmt_expr_attributes` and `proc_macro_hygiene` feature gates). To keep the default test run usable on stable Rust, these tests are gated on a custom cfg named `mutants_nightly`: - -```rust -#[test] -#[cfg_attr( - not(mutants_nightly), - ignore = "requires --cfg=mutants_nightly and a nightly toolchain" -)] -fn check_tree_with_my_nightly_feature() { ... } -``` - -Their testdata enables the required nightly features under the same cfg: - -```rust -#![cfg_attr(mutants_nightly, feature(stmt_expr_attributes, proc_macro_hygiene))] -``` - -To run these tests: - -```bash -cargo +nightly nextest run --all-features \ - --config 'build.rustflags=["--cfg=mutants_nightly"]' \ - check_tree_with_my_nightly_feature -``` - -The integration test forwards the cfg to the cargo-mutants subprocess (and from there to the testdata's `cargo check --tests`) via `RUSTFLAGS="--cfg=mutants_nightly"` set on the subprocess env. - -`mutants_nightly` is registered as a known cfg in the top-level `Cargo.toml`'s `[lints.rust]` section and in the testdata tree's `Cargo_test.toml`, so the `unexpected_cfgs` lint doesn't warn when the cfg is absent. - ## Linting and formatting Run `cargo fmt` before committing. diff --git a/Cargo.toml b/Cargo.toml index df824a94..5196fadd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -131,19 +131,6 @@ lto = "thin" inherits = "test" debug = "none" -# Custom cfgs used by the test suite. Listing them here suppresses the -# `unexpected_cfgs` lint warning when the cfg is not set. -# -# `mutants_nightly` — opts into integration tests that exercise nightly-only -# syntax in their testdata trees (for example `#[mutants::skip]` placed -# directly on an expression). Set by running: -# -# cargo +nightly test --config 'build.rustflags=["--cfg=mutants_nightly"]' -# -# See AGENTS.md for details. -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(mutants_nightly)'] } - # Config for [workspace.metadata.release] pre-release-replacements = [ diff --git a/testdata/skip_attr_block/Cargo_test.toml b/testdata/skip_attr_block/Cargo_test.toml deleted file mode 100644 index 832b22ca..00000000 --- a/testdata/skip_attr_block/Cargo_test.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "cargo-mutants-testdata-skip-attr-block" -version = "0.0.0" -edition = "2021" -publish = false - -# Custom proc-macro attributes on expressions and block expressions are -# nightly-only on the Rust toolchain, so the source uses -# `#![cfg_attr(mutants_nightly, feature(...))]` to enable the required -# feature gates only when the test is opted in via the `mutants_nightly` -# cfg. See AGENTS.md and the crate-level docs in `src/lib.rs`. -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(mutants_nightly)'] } - -[dependencies] -mutants = "0.0.3" diff --git a/testdata/skip_attr_block/README.md b/testdata/skip_attr_block/README.md deleted file mode 100644 index dea22cb7..00000000 --- a/testdata/skip_attr_block/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# skip_attr_block - -Exercises `#[mutants::skip]` placed directly on a block expression -`{ ... }` inside a function body. Each function in `src/lib.rs` annotates -a block in a different syntactic position and pairs it with un-annotated -sibling code so that the expected behavior is unambiguous: - -- `statement_position` — `#[mutants::skip] { ... }` used as a statement. -- `tail_block` — `#[mutants::skip] { ... }` used as the function's tail - expression. -- `labeled_block` — `#[mutants::skip] 'block: { ... }` used as the - function's tail expression. -- `unannotated_sibling` — no skip attribute; mutants must still be - produced. - -Inside each annotated block every mutant cargo-mutants would normally -produce must be suppressed; in `unannotated_sibling`, mutants must be -produced as usual. - -## Nightly-only - -Custom proc-macro attributes on expressions and block expressions are -nightly-only (`stmt_expr_attributes` and `proc_macro_hygiene`). The -crate-level `#![cfg_attr(mutants_nightly, feature(...))]` enables those -features only when the `mutants_nightly` cfg is set, and the integration -test that consumes this tree is `#[ignore]`d unless the same cfg is set. - -See `AGENTS.md` in the repository root for how to opt in. diff --git a/testdata/skip_attr_block/src/lib.rs b/testdata/skip_attr_block/src/lib.rs deleted file mode 100644 index 423f44f8..00000000 --- a/testdata/skip_attr_block/src/lib.rs +++ /dev/null @@ -1,76 +0,0 @@ -//! Verify that `#[mutants::skip]` placed directly on a block expression -//! `{ ... }` inside a function body suppresses every mutant generated -//! inside that block, while mutants in surrounding code are still -//! produced as usual. -//! -//! Custom proc-macro attributes on expressions and block expressions -//! require the unstable `stmt_expr_attributes` and `proc_macro_hygiene` -//! features, which are only available on nightly Rust. We therefore -//! gate the feature attributes on `cfg(mutants_nightly)`. The -//! integration test that consumes this tree is `#[ignore]`d unless -//! `mutants_nightly` is set, and the same cfg is forwarded to the -//! `cargo check --tests` subprocess that cargo-mutants runs against -//! this tree, so the feature gates kick in there too. See AGENTS.md -//! for the convention. - -#![cfg_attr( - mutants_nightly, - feature(stmt_expr_attributes, proc_macro_hygiene) -)] - -/// `#[mutants::skip]` on a block used as a statement. -/// -/// The `+=` and `+` operators inside the annotated block must produce no -/// mutants. The `+` and `-` on the tail expression must still be mutated -/// as usual. -pub fn statement_position(a: i32, b: i32, c: i32, d: i32) -> i32 { - let mut total = 0; - #[mutants::skip] - { - total += a + b; - } - total + (c - d) -} - -/// `#[mutants::skip]` on an unlabeled block used as the function's tail -/// expression. -/// -/// Every operator inside the annotated block — `>`, `+`, `-` — must -/// produce no mutants. -pub fn tail_block(a: i32, b: i32, c: i32) -> i32 { - let _ = 0; - #[mutants::skip] - { - if a > b { - a + b - } else { - a - c - } - } -} - -/// `#[mutants::skip]` on a labeled block used as the function's tail -/// expression. -/// -/// Every operator inside the annotated block — `>`, `+`, `-` — must -/// produce no mutants. -pub fn labeled_block(a: i32, b: i32, c: i32) -> i32 { - #[mutants::skip] - 'block: { - if a > b { - break 'block a + b; - } - a - c - } -} - -/// Sibling un-annotated block — mutants here must NOT be suppressed. -/// -/// Paired with `statement_position` etc. above, this confirms the -/// suppression is scoped to the annotated block and does not leak into -/// surrounding code. -pub fn unannotated_sibling(a: i32, b: i32) -> i32 { - let x = { a * b }; - let y = { a / b }; - x | y -} diff --git a/testdata/skip_attr_expressions/Cargo_test.toml b/testdata/skip_attr_expressions/Cargo_test.toml deleted file mode 100644 index 7a048625..00000000 --- a/testdata/skip_attr_expressions/Cargo_test.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "cargo-mutants-testdata-skip-attr-expressions" -version = "0.0.0" -edition = "2021" -publish = false - -# Custom proc-macro attributes on expressions and statements are -# nightly-only on the Rust toolchain, so the source uses -# `#![cfg_attr(mutants_nightly, feature(...))]` to enable the required -# feature gates only when the test is opted in via the `mutants_nightly` -# cfg. See AGENTS.md and the crate-level docs in `src/lib.rs`. -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(mutants_nightly)'] } - -[dependencies] -mutants = "0.0.3" diff --git a/testdata/skip_attr_expressions/README.md b/testdata/skip_attr_expressions/README.md deleted file mode 100644 index f9a54b88..00000000 --- a/testdata/skip_attr_expressions/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# skip_attr_expressions - -Exercises `#[mutants::skip]` placed directly on expressions other than -block expressions. Each function in `src/lib.rs` pairs an annotated -expression of a given genre with an un-annotated sibling expression of -the same genre so that the expected behavior is unambiguous: - -- `call_expr` — `#[mutants::skip]` on a call expression suppresses - mutants generated for its argument expressions. -- `method_call_expr` — same, on a method-call expression. -- `match_expr` — on a `match` expression, suppresses both arm-deletion - and guard-replacement mutants. -- `struct_expr` — on a struct literal that has a `..Default::default()` - base, suppresses the field-deletion mutants generated for the literal. -- `unary_expr` — on a unary `!` expression, suppresses the unary - deletion mutant. - -Inside each annotated expression every mutant cargo-mutants would -normally produce must be suppressed; in the un-annotated sibling in the -same function, mutants must be produced as usual. Block-expression skip -(`#[mutants::skip] { ... }`) is covered separately by the `skip_attr_block` -tree. - -## Nightly-only - -Custom proc-macro attributes on expressions and statements are -nightly-only (`stmt_expr_attributes` and `proc_macro_hygiene`). The -crate-level `#![cfg_attr(mutants_nightly, feature(...))]` enables those -features only when the `mutants_nightly` cfg is set, and the integration -test that consumes this tree is `#[ignore]`d unless the same cfg is set. - -See `AGENTS.md` in the repository root for how to opt in. diff --git a/testdata/skip_attr_expressions/src/lib.rs b/testdata/skip_attr_expressions/src/lib.rs deleted file mode 100644 index d5105064..00000000 --- a/testdata/skip_attr_expressions/src/lib.rs +++ /dev/null @@ -1,104 +0,0 @@ -//! Verify that `#[mutants::skip]` placed directly on expressions other -//! than block expressions suppresses every mutant generated inside the -//! annotated expression, while sibling expressions of the same genre in -//! the same function are still mutated as usual. -//! -//! Custom proc-macro attributes on expressions and statements require -//! the unstable `stmt_expr_attributes` and `proc_macro_hygiene` -//! features, which are only available on nightly Rust. We therefore -//! gate the feature attributes on `cfg(mutants_nightly)`. The -//! integration test that consumes this tree is `#[ignore]`d unless -//! `mutants_nightly` is set, and the same cfg is forwarded to the -//! `cargo check --tests` subprocess that cargo-mutants runs against -//! this tree, so the feature gates kick in there too. See AGENTS.md -//! for the convention. -//! -//! Block-expression skip is covered separately by the `skip_attr_block` -//! testdata tree. - -#![cfg_attr( - mutants_nightly, - feature(stmt_expr_attributes, proc_macro_hygiene) -)] - -/// `#[mutants::skip]` on a call expression. -/// -/// The `+` operator inside the annotated call's argument must produce -/// no mutants. The `-` operator inside the un-annotated sibling call -/// must still be mutated as usual. -pub fn call_expr(a: i32, b: i32, c: i32, d: i32) { - #[mutants::skip] - helper(a + b); - helper(c - d); -} - -/// `#[mutants::skip]` on a method-call expression. -/// -/// The `+` operator inside the annotated method call's argument must -/// produce no mutants. The `-` operator inside the un-annotated sibling -/// method call must still be mutated as usual. -pub fn method_call_expr(s: &Holder, a: i32, b: i32, c: i32, d: i32) { - #[mutants::skip] - s.frob(a + b); - s.frob(c - d); -} - -/// `#[mutants::skip]` on a `match` expression. -/// -/// The annotated match must produce no arm-deletion, guard-replacement -/// or operator mutants. The un-annotated sibling match must still -/// produce all of those mutants. -pub fn match_expr(x: i32, y: i32) { - let _ = #[mutants::skip] - match x { - 0 => "zero", - n if n > y => "gt", - _ => "other", - }; - let _ = match x { - 0 => "zero", - n if n > y => "gt", - _ => "other", - }; -} - -/// `#[mutants::skip]` on a struct-literal expression. -/// -/// The annotated struct literal must produce no field-deletion mutants. -/// The un-annotated sibling literal must still produce them. -pub fn struct_expr() { - let _ = #[mutants::skip] - Settings { - enabled: true, - count: 1, - ..Default::default() - }; - let _ = Settings { - enabled: true, - count: 1, - ..Default::default() - }; -} - -/// `#[mutants::skip]` on a unary `!` expression. -/// -/// The annotated `!b` must produce no `delete !` mutant. The -/// un-annotated sibling `!b` must still produce one. -pub fn unary_expr(b: bool) { - let _ = #[mutants::skip] !b; - let _ = !b; -} - -#[derive(Default)] -pub struct Settings { - pub enabled: bool, - pub count: i32, -} - -pub struct Holder; - -impl Holder { - pub fn frob(&self, _x: i32) {} -} - -fn helper(_x: i32) {} diff --git a/tests/main.rs b/tests/main.rs index 364fbefd..376ba105 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -2629,168 +2629,6 @@ fn check_tree_with_mutants_skip() { ); } -/// `#[mutants::skip]` placed on a block expression inside a function body -/// must suppress every mutant generated for code inside that block. -/// -/// The `skip_attr_block` tree pairs each annotated block with un-annotated -/// surrounding code, so the absence of mutants from the annotated lines and -/// their presence on the un-annotated lines is the actual assertion. We use -/// `--check` so the listed mutants are also confirmed to compile, ruling -/// out regressions where suppression accidentally rewrites the source. -/// -/// Custom proc-macro attributes on expressions are nightly-only on rustc, -/// so the testdata uses `#![cfg_attr(mutants_nightly, feature(...))]` to -/// enable the required feature gates and this test is `#[ignore]`d unless -/// `mutants_nightly` is set. To run it, build the test binary with: -/// -/// ```text -/// cargo +nightly test --config 'build.rustflags=["--cfg=mutants_nightly"]' \ -/// check_tree_with_skip_attr_on_block_expressions -/// ``` -/// -/// The cfg is forwarded to the cargo-mutants subprocess (and therefore to -/// the testdata's `cargo check --tests` invocation) via `RUSTFLAGS`. The -/// direct `#[mutants::skip]` form is additionally covered by unit tests in -/// `src/visit/test/skip_attr_expr_block.rs`, which only parse the source -/// through syn and so work on stable. -#[test] -#[cfg_attr( - not(mutants_nightly), - ignore = "requires --cfg=mutants_nightly and a nightly toolchain; see test docs" -)] -fn check_tree_with_skip_attr_on_block_expressions() { - let tmp_src_dir = copy_of_testdata("skip_attr_block"); - run() - .arg("mutants") - .args(["--check", "--no-times", "--no-shuffle"]) - .current_dir(tmp_src_dir.path()) - .env("RUSTFLAGS", "--cfg=mutants_nightly") - .env_remove("RUST_BACKTRACE") - .assert() - .success() - .stdout(indoc! { r" - Found 22 mutants to test - ok Unmutated baseline - ok src/lib.rs:27:5: replace statement_position -> i32 with 0 - ok src/lib.rs:27:5: replace statement_position -> i32 with 1 - ok src/lib.rs:27:5: replace statement_position -> i32 with -1 - ok src/lib.rs:32:11: replace + with - in statement_position - ok src/lib.rs:32:11: replace + with * in statement_position - ok src/lib.rs:32:16: replace - with + in statement_position - ok src/lib.rs:32:16: replace - with / in statement_position - ok src/lib.rs:41:5: replace tail_block -> i32 with 0 - ok src/lib.rs:41:5: replace tail_block -> i32 with 1 - ok src/lib.rs:41:5: replace tail_block -> i32 with -1 - ok src/lib.rs:58:5: replace labeled_block -> i32 with 0 - ok src/lib.rs:58:5: replace labeled_block -> i32 with 1 - ok src/lib.rs:58:5: replace labeled_block -> i32 with -1 - ok src/lib.rs:73:5: replace unannotated_sibling -> i32 with 0 - ok src/lib.rs:73:5: replace unannotated_sibling -> i32 with 1 - ok src/lib.rs:73:5: replace unannotated_sibling -> i32 with -1 - ok src/lib.rs:73:17: replace * with + in unannotated_sibling - ok src/lib.rs:73:17: replace * with / in unannotated_sibling - ok src/lib.rs:74:17: replace / with % in unannotated_sibling - ok src/lib.rs:74:17: replace / with * in unannotated_sibling - ok src/lib.rs:75:7: replace | with & in unannotated_sibling - ok src/lib.rs:75:7: replace | with ^ in unannotated_sibling - 22 mutants tested: 22 succeeded - "}) - .stderr(""); - assert_eq!( - outcome_json_counts(&tmp_src_dir), - serde_json::json!({ - "caught": 0, - "missed": 0, - "success": 22, - "timeout": 0, - "unviable": 0, - "total_mutants": 22, - }) - ); -} - -/// `#[mutants::skip]` placed directly on expressions other than block -/// expressions must suppress every mutant generated inside the annotated -/// expression, while sibling expressions of the same genre in the same -/// function are still mutated as usual. -/// -/// The `skip_attr_expressions` tree pairs an annotated expression of each -/// supported genre — call, method call, `match`, struct literal and unary — -/// with an un-annotated sibling expression of the same genre in the same -/// function. The absence of mutants on the annotated expression's lines and -/// their presence on the sibling's lines is the actual assertion. We use -/// `--check` so the listed mutants are also confirmed to compile, ruling -/// out regressions where suppression accidentally rewrites the source. -/// -/// Custom proc-macro attributes on expressions and statements are -/// nightly-only on rustc, so the testdata uses -/// `#![cfg_attr(mutants_nightly, feature(...))]` to enable the required -/// feature gates and this test is `#[ignore]`d unless `mutants_nightly` -/// is set. To run it, build the test binary with: -/// -/// ```text -/// cargo +nightly test --config 'build.rustflags=["--cfg=mutants_nightly"]' \ -/// check_tree_with_skip_attr_on_expressions -/// ``` -/// -/// The cfg is forwarded to the cargo-mutants subprocess (and therefore to -/// the testdata's `cargo check --tests` invocation) via `RUSTFLAGS`. The -/// direct `#[mutants::skip]` form is additionally covered by unit tests in -/// `src/visit/test/skip_attr_expr_*.rs`, which only parse the source -/// through syn and so work on stable. Block-expression skip has its own -/// dedicated integration test `check_tree_with_skip_attr_on_block_expressions`. -#[test] -#[cfg_attr( - not(mutants_nightly), - ignore = "requires --cfg=mutants_nightly and a nightly toolchain; see test docs" -)] -fn check_tree_with_skip_attr_on_expressions() { - let tmp_src_dir = copy_of_testdata("skip_attr_expressions"); - run() - .arg("mutants") - .args(["--check", "--no-times", "--no-shuffle"]) - .current_dir(tmp_src_dir.path()) - .env("RUSTFLAGS", "--cfg=mutants_nightly") - .env_remove("RUST_BACKTRACE") - .assert() - .success() - .stdout(indoc! { r" - Found 18 mutants to test - ok Unmutated baseline - ok src/lib.rs:30:5: replace call_expr with () - ok src/lib.rs:32:14: replace - with + in call_expr - ok src/lib.rs:32:14: replace - with / in call_expr - ok src/lib.rs:41:5: replace method_call_expr with () - ok src/lib.rs:43:14: replace - with + in method_call_expr - ok src/lib.rs:43:14: replace - with / in method_call_expr - ok src/lib.rs:52:5: replace match_expr with () - ok src/lib.rs:59:9: delete match arm 0 in match_expr - ok src/lib.rs:60:14: replace match guard n > y with true in match_expr - ok src/lib.rs:60:14: replace match guard n > y with false in match_expr - ok src/lib.rs:60:16: replace > with == in match_expr - ok src/lib.rs:60:16: replace > with < in match_expr - ok src/lib.rs:60:16: replace > with >= in match_expr - ok src/lib.rs:70:5: replace struct_expr with () - ok src/lib.rs:77:9: delete field enabled from struct Settings expression in struct_expr - ok src/lib.rs:78:9: delete field count from struct Settings expression in struct_expr - ok src/lib.rs:88:5: replace unary_expr with () - ok src/lib.rs:89:13: delete ! in unary_expr - 18 mutants tested: 18 succeeded - "}) - .stderr(""); - assert_eq!( - outcome_json_counts(&tmp_src_dir), - serde_json::json!({ - "caught": 0, - "missed": 0, - "success": 18, - "timeout": 0, - "unviable": 0, - "total_mutants": 18, - }) - ); -} - #[test] fn check_tree_where_build_fails() { let tmp_src_dir = copy_of_testdata("typecheck_fails");