Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ bytes = { version = "1.12.1", default-features = false }
camino = { version = "1.2.5", default-features = false }
# local dependencies
cargo-aprz-lib = { path = "crates/cargo-aprz-lib", default-features = false, version = "1.1.2" }
cargo-gamma-attrs-impl = { path = "crates/cargo-gamma-attrs-impl", default-features = false, version = "0.1.0" }
cargo-gamma-engine = { path = "crates/cargo-gamma-engine", default-features = false, version = "0.1.0" }
cargo-gamma-lib = { path = "crates/cargo-gamma-lib", default-features = false, version = "0.1.0" }
cargo-gamma-process = { path = "crates/cargo-gamma-process", default-features = false, version = "0.1.0" }
cargo-gamma-rt = { path = "crates/cargo-gamma-rt", default-features = false, version = "0.1.0" }
cargo-gamma-unsafe = { path = "crates/cargo-gamma-unsafe", default-features = false, version = "0.1.0" }
cargo-gamma-attrs-impl = { path = "crates/cargo-gamma-attrs-impl", default-features = false, version = "0.2.0" }
cargo-gamma-engine = { path = "crates/cargo-gamma-engine", default-features = false, version = "0.2.0" }
cargo-gamma-lib = { path = "crates/cargo-gamma-lib", default-features = false, version = "0.2.0" }
cargo-gamma-process = { path = "crates/cargo-gamma-process", default-features = false, version = "0.2.0" }
cargo-gamma-rt = { path = "crates/cargo-gamma-rt", default-features = false, version = "0.2.0" }
cargo-gamma-unsafe = { path = "crates/cargo-gamma-unsafe", default-features = false, version = "0.2.0" }
Comment thread
geeknoid marked this conversation as resolved.
cargo-heather = { path = "crates/cargo-heather", default-features = false, version = "0.2.1" }
# external dependencies
cargo-platform = { version = "0.3.3", default-features = false }
Expand Down
14 changes: 14 additions & 0 deletions crates/cargo-gamma-attrs-impl/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Changelog

## [Unreleased]

## [0.2.0] - 2026-09-03

- 🐛 Bug Fixes

- harden discovery and process cleanup
Comment thread
geeknoid marked this conversation as resolved.
- address hardening feedback ([#144](https://github.com/microsoft/ox-tools/pull/144))

## [0.1.0] - 2026-09-02

- Initial release.
2 changes: 1 addition & 1 deletion crates/cargo-gamma-attrs-impl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[package]
name = "cargo-gamma-attrs-impl"
description = "Implementation of the inert attribute macros exposed by cargo-gamma-attrs"
version = "0.1.0"
version = "0.2.0"
readme = "README.md"
keywords = ["cargo", "mutation-testing", "testing"]
categories = ["development-tools::testing"]
Expand Down
42 changes: 40 additions & 2 deletions crates/cargo-gamma-attrs-impl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,49 @@

</div>

This is an implementation detail of the cargo-gamma-attrs crate. Do not take a dependency on this
crate as it may change in incompatible ways without warning.
The implementation behind [`cargo-gamma-attrs`][__link0],
which is where the inert `#[gamma::skip]`, `#[gamma::expect_survived]` and
`#[gamma::expect_killed]` attributes are actually exposed.

You almost certainly want that crate instead. This one is a normal library rather than a
proc-macro crate so its logic can be called by ordinary tests, covered, and mutation tested.
What remains in the proc-macro crate is a shim thin enough to read at a glance.

## Why this crate exists

`cargo-gamma-attrs` is a proc-macro crate, and a proc macro’s code runs only inside `rustc`,
while some *other* crate is being compiled. That puts it beyond the reach of both measurements
this project cares about:

* A coverage harness collects counters from test binaries. A proc macro increments its counters
inside the compiler, which writes no profile the harness sees.
* A mutation run selects one mutant per test process at run time. A proc macro has already
finished by then, so none of its mutants can be active while a test is watching.

Splitting the logic into an ordinary library makes it reachable by coverage and mutation tests.
The proc-macro crate remains a thin shim.

## What the macros accept

See the [`cargo-gamma-attrs`][__link1] documentation for the
user-facing description. In brief: a comma-separated selector list, optionally followed by
`reason = "..."` and `tag = "..."`, both of which must be string literals.

`#[gamma::value(<expr>)]` instead takes an expression. It is checked by [`value`][__link2], because its
argument is spliced into the user’s crate as a mutant and must be exactly one expression.

## Stability

This crate is an implementation detail of `cargo-gamma-attrs` and carries no stability
guarantee of its own. Depend on `cargo-gamma-attrs`.


<hr/>
<sub>
This crate was developed as part of <a href="../..">The Oxidizer Project</a>. Browse this crate's <a href="https://github.com/microsoft/ox-tools/tree/main/crates/cargo-gamma-attrs-impl">source code</a>.
</sub>

[__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjNhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQbH5RUmmY8e-sbYyqmHPyeK9obgdLJAJ7T65AbUAUW0Y4uz2thZIGDdmNhcmdvLWdhbW1hLWF0dHJzLWltcGxlMC4yLjB2Y2FyZ29fZ2FtbWFfYXR0cnNfaW1wbA
[__link0]: https://crates.io/crates/cargo-gamma-attrs
[__link1]: https://docs.rs/cargo-gamma-attrs
[__link2]: https://docs.rs/cargo-gamma-attrs-impl/0.2.0/cargo_gamma_attrs_impl/?search=value
4 changes: 4 additions & 0 deletions crates/cargo-gamma-attrs-impl/docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ metadata.
logic outside rustc makes it directly testable and mutation-testable.
- It accepts exactly one Rust expression where an attribute promises an
expression and rejects unsupported keys or malformed selectors.
- Delimiter depth and the combined chain of operators, casts, postfix links,
and `else` arms are bounded before input reaches `syn`. The chain categories
share one budget, matching the engine guard rather than allowing mixed syntax
to evade each independent limit.
- A stated value is rejected on any function the tool would never mutate: a
declaration with no body, a `const fn`, or a function whose body is empty.
Accepting one there would leave a hint that reads as working and generates
Expand Down
94 changes: 56 additions & 38 deletions crates/cargo-gamma-attrs-impl/src/implementation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,10 @@ pub fn value(attr: TokenStream, item: TokenStream) -> TokenStream {
#[doc(hidden)]
pub(super) const NESTING_LIMIT: usize = 64;

/// How many postfix links are allowed per delimiter nesting level.
/// How many expression-path links are allowed per delimiter nesting level.
///
/// This stays in step with `cargo_gamma_lib::parse::nesting`: a run of calls or indexes is a
/// recursive expression tree even though each delimiter closes before the next one opens; field,
/// method, and try links add the same recursive shape.
/// This stays in step with `cargo_gamma_lib::parse::nesting`: operators, casts, calls, indexes,
/// field and method access, try links, and `else` arms all add recursive expression shape.
///
/// Exposed (hidden from docs) so `cargo-gamma-lib`'s agreement test can pin this copy against the
/// library's own `CHAIN_FACTOR`.
Expand All @@ -111,21 +110,32 @@ pub(super) const CHAIN_FACTOR: usize = 4;
enum Previous {
Other,
Expression,
Operator,
}

impl Previous {
const fn can_end_expression(self) -> bool {
matches!(self, Self::Expression | Self::Operator)
}
}

/// One token-stream level waiting to be walked.
struct Frame {
iter: proc_macro2::token_stream::IntoIter,
depth: usize,
postfix: usize,
casts: usize,
operators: usize,
links: usize,
ladders: usize,
awaiting_else: bool,
previous: Previous,
}

/// Returns whether a token stream exceeds its delimiter or postfix-expression limits.
impl Frame {
fn exceeds_chain_limit(&self, limit: usize) -> bool {
self.links > limit || self.ladders > limit
}
}

/// Returns whether a token stream exceeds its delimiter or expression-path limits.
///
/// Nested groups are walked with an explicit stack rather than recursion, because this code runs
/// inside `rustc` while compiling user code: a proc macro that exhausts the stack takes the
Expand All @@ -136,7 +146,7 @@ struct Frame {
/// `else`, and drops that chain the same way. Delimiter depth alone would let a long enough ladder
/// through to overflow the stack instead of producing this guard's diagnostic. `ladders` counts
/// every `else` that follows a completed group at the same level, mirroring
/// `cargo_gamma_engine::parse::nesting`'s `ladders` counter and bounded by the same `postfix_limit`
/// `cargo_gamma_engine::parse::nesting`'s `ladders` counter and bounded by the same chain limit
/// as every other expression-path chain this walk already tracks.
///
/// Exposed (hidden from docs) so `cargo-gamma-lib`'s agreement test can drive this scanner with
Expand All @@ -150,13 +160,11 @@ struct Frame {
reason = "delimiter and expression-path state must advance together through one token walk"
)]
pub(super) fn exceeds_nesting_limit(stream: &TokenStream, limit: usize) -> bool {
let postfix_limit = limit.saturating_mul(CHAIN_FACTOR);
let chain_limit = limit.saturating_mul(CHAIN_FACTOR);
let mut frames = vec![Frame {
iter: stream.clone().into_iter(),
depth: 0,
postfix: 0,
casts: 0,
operators: 0,
links: 0,
ladders: 0,
awaiting_else: false,
previous: Previous::Other,
Expand All @@ -171,16 +179,14 @@ pub(super) fn exceeds_nesting_limit(stream: &TokenStream, limit: usize) -> bool
}

let postfix =
matches!(group.delimiter(), Delimiter::Parenthesis | Delimiter::Bracket) && frame.previous == Previous::Expression;
matches!(group.delimiter(), Delimiter::Parenthesis | Delimiter::Bracket) && frame.previous.can_end_expression();

if postfix {
frame.postfix += 1;
frame.links += 1;

if frame.postfix > postfix_limit {
if frame.exceeds_chain_limit(chain_limit) {
return true;
}
} else {
frame.postfix = 0;
}

let next_depth = frame.depth + 1;
Expand All @@ -190,17 +196,15 @@ pub(super) fn exceeds_nesting_limit(stream: &TokenStream, limit: usize) -> bool
}

// A complete group can be the receiver of the next call or index, or the `{ }`
// block an `else` ladder continues from. The child gets a fresh postfix chain
// because only adjacent links share one expression.
// block an `else` ladder continues from. The child gets a fresh expression
// path because only links at the same token-stream level share one chain.
frame.awaiting_else = group.delimiter() == Delimiter::Brace;
frame.previous = Previous::Expression;
frames.push(frame);
frames.push(Frame {
iter: group.stream().into_iter(),
depth: next_depth,
postfix: 0,
casts: 0,
operators: 0,
links: 0,
ladders: 0,
awaiting_else: false,
previous: Previous::Other,
Expand All @@ -213,10 +217,9 @@ pub(super) fn exceeds_nesting_limit(stream: &TokenStream, limit: usize) -> bool
frame.ladders = 0;
frame.awaiting_else = false;
}
frame.postfix = 0;
frame.casts += 1;
frame.links += 1;

if frame.casts > postfix_limit {
if frame.exceeds_chain_limit(chain_limit) {
return true;
}

Expand All @@ -226,10 +229,10 @@ pub(super) fn exceeds_nesting_limit(stream: &TokenStream, limit: usize) -> bool
TokenTree::Ident(ident) if ident == "else" && frame.awaiting_else => {
// Reached only right after a completed group, which is what an `else` following
// an `if`'s or a prior arm's `{ }` block looks like at the token-stream level.
frame.postfix = 0;
frame.ladders += 1;
frame.links += 1;

if frame.ladders > postfix_limit {
if frame.exceeds_chain_limit(chain_limit) {
return true;
}

Expand All @@ -242,6 +245,14 @@ pub(super) fn exceeds_nesting_limit(stream: &TokenStream, limit: usize) -> bool
frame.ladders = 0;
frame.awaiting_else = false;
}

if frame.previous == Previous::Expression {
Comment thread
geeknoid marked this conversation as resolved.
// An operand after an expression begins a new chain across statement and
// keyword boundaries. Keep `ladders`: an `else if` condition resets
// `links`, while the separate ladder count must span every arm.
frame.links = 0;
}

frame.previous = Previous::Expression;
}

Expand All @@ -254,33 +265,30 @@ pub(super) fn exceeds_nesting_limit(stream: &TokenStream, limit: usize) -> bool
punct.as_char(),
'+' | '-' | '*' | '/' | '%' | '&' | '|' | '^' | '!' | '<' | '>' | '='
) {
frame.operators += 1;
frame.links += 1;

if frame.operators > postfix_limit {
if frame.exceeds_chain_limit(chain_limit) {
return true;
}
}

let postfix = matches!(punct.as_char(), '.' | '?') && frame.previous == Previous::Expression;
let postfix = matches!(punct.as_char(), '.' | '?') && frame.previous.can_end_expression();

if postfix {
frame.postfix += 1;
frame.links += 1;

if frame.postfix > postfix_limit {
if frame.exceeds_chain_limit(chain_limit) {
return true;
}
} else {
frame.postfix = 0;
}

if matches!(punct.as_char(), ',' | ';') {
frame.casts = 0;
frame.operators = 0;
frame.links = 0;
frame.ladders = 0;
}

frame.previous = if matches!(punct.as_char(), '?' | '>') {
Previous::Expression
Previous::Operator
} else {
Previous::Other
};
Expand Down Expand Up @@ -1624,6 +1632,16 @@ mod tests {
assert_eq!(err, "expression nests too deeply to be safely parsed");
}

#[test]
fn mixed_operator_and_cast_chains_share_one_nesting_budget() {
let links = NESTING_LIMIT * CHAIN_FACTOR / 2 + 1;
let expression = format!("1{}{}", " + 1".repeat(links), " as u64".repeat(links));
let error = validate_value(stream(&expression), &stream("fn f() -> u64 { 1 }"))
.expect_err("the combined expression chain must be rejected");

assert_eq!(error, "expression nests too deeply to be safely parsed");
}

#[test]
fn a_long_unary_chain_expands_to_a_guard_diagnostic() {
let expression = format!("{}1", "-".repeat(NESTING_LIMIT * CHAIN_FACTOR + 1));
Expand Down
Loading
Loading