use std::{iter, ops::Range};
pub(crate) fn attribute_marks_test(body: &str) -> bool {
let matches_test = |s: &str| {
matches!(s, "test" | "rstest" | "wasm_bindgen_test" | "test_case")
|| s.ends_with("::test")
|| s.contains("::test(")
|| cfg_inner(s).is_some_and(cfg_predicate_marks_test)
};
let trimmed = body.trim();
if matches_test(trimmed) {
return true;
}
if trimmed.bytes().any(|b| b.is_ascii_whitespace()) {
return matches_test(&strip_whitespace(trimmed));
}
false
}
fn strip_whitespace(s: &str) -> String {
s.chars().filter(|c| !c.is_whitespace()).collect()
}
fn cfg_inner(body: &str) -> Option<&str> {
let rest = body.trim_start().strip_prefix("cfg")?.trim_start();
let after_open = rest.strip_prefix('(')?;
let inner = after_open.strip_suffix(')')?;
Some(inner)
}
struct CommaIndex {
entries: Vec<(usize, usize)>,
}
impl CommaIndex {
fn build(pred: &str) -> Self {
let mut entries = Vec::new();
let mut depth = 0_isize;
for (offset, byte) in pred.bytes().enumerate() {
match byte {
b'(' => depth += 1,
b')' => depth -= 1,
b',' => {
if let Ok(comma_depth) = usize::try_from(depth) {
entries.push((comma_depth, offset));
}
}
_ => {}
}
}
entries.sort_unstable();
Self { entries }
}
fn splits(&self, region: &Range<usize>, depth: usize) -> impl Iterator<Item = usize> {
let first = self
.entries
.partition_point(|entry| *entry < (depth, region.start));
let end = region.end;
self.entries[first..]
.iter()
.take_while(move |(entry_depth, offset)| *entry_depth == depth && *offset < end)
.map(|(_, offset)| *offset)
}
}
fn cfg_predicate_marks_test(pred: &str) -> bool {
let commas = CommaIndex::build(pred);
let mut stack = vec![(0..pred.len(), 0_usize)];
while let Some((region, depth)) = stack.pop() {
let mut operand_start = region.start;
for boundary in commas.splits(®ion, depth).chain(iter::once(region.end)) {
match classify_cfg_operand(pred, operand_start..boundary) {
Operand::Test => return true,
Operand::Args(args) => stack.push((args, depth + 1)),
Operand::Opaque => {}
}
operand_start = boundary + 1;
}
}
false
}
enum Operand {
Test,
Args(Range<usize>),
Opaque,
}
fn classify_cfg_operand(pred: &str, operand: Range<usize>) -> Operand {
let raw = &pred[operand.start..operand.end];
let trimmed = raw.trim();
if trimmed == "test" {
return Operand::Test;
}
if trimmed
.strip_prefix("not")
.map(str::trim_start)
.is_some_and(|rest| rest.starts_with('(') && rest.ends_with(')'))
{
return Operand::Opaque;
}
if let Some(rest) = trimmed
.strip_prefix("all")
.or_else(|| trimmed.strip_prefix("any"))
&& let Some(inside) = rest.trim_start().strip_prefix('(')
&& let Some(args) = inside.strip_suffix(')')
{
let args_start = operand.start + raw.trim_end().len() - inside.len();
return Operand::Args(args_start..args_start + args.len());
}
Operand::Opaque
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rust_attr_test_marks_bare_test_attribute() {
assert!(attribute_marks_test("test"));
assert!(attribute_marks_test("rstest"));
assert!(attribute_marks_test("wasm_bindgen_test"));
assert!(attribute_marks_test("test_case"));
assert!(attribute_marks_test("tokio::test"));
assert!(attribute_marks_test(
"tokio::test(flavor = \"current_thread\")"
));
}
#[test]
fn rust_attr_test_marks_cfg_test_variants() {
assert!(attribute_marks_test("cfg(test)"));
assert!(attribute_marks_test("cfg(test, foo)"));
assert!(attribute_marks_test("cfg(all(test, unix))"));
assert!(attribute_marks_test("cfg(any(test, foo))"));
}
#[test]
fn rust_attr_test_marks_cfg_with_test_not_first() {
assert!(
attribute_marks_test("cfg(all(unix, test))"),
"test as second all() operand must mark test-only"
);
assert!(
attribute_marks_test("cfg(any(feature = \"x\", test))"),
"test as second any() operand must mark test-only"
);
assert!(attribute_marks_test(
"cfg(all(unix, any(test, feature = \"x\")))"
));
}
#[test]
fn rust_attr_test_skips_not_test_and_feature_named_test() {
assert!(!attribute_marks_test("cfg(not(test))"));
assert!(!attribute_marks_test("cfg(all(unix, not(test)))"));
assert!(!attribute_marks_test("cfg(feature = \"test\")"));
assert!(!attribute_marks_test("cfg(all(unix, feature = \"test\"))"));
assert!(!attribute_marks_test("cfg(unix)"));
assert!(!attribute_marks_test("derive(Debug)"));
assert!(!attribute_marks_test(
"cfg(all(unix, target_os = \"linux\"))"
));
assert!(!attribute_marks_test("cfg(any(unix, windows))"));
assert!(!attribute_marks_test(
"cfg(all(unix, any(feature = \"x\", feature = \"y\")))"
));
assert!(!attribute_marks_test("cfg(any(unix, not(test)))"));
}
#[test]
fn rust_attr_test_not_led_comma_list_keeps_later_test_operand() {
assert!(
attribute_marks_test("cfg(not(foo), all(test))"),
"not(foo), all(test) list must still see the trailing test"
);
assert!(
attribute_marks_test("cfg(not(unix), any(test))"),
"not(unix), any(test) list must still see the trailing test"
);
assert!(attribute_marks_test("cfg(all(not(foo), all(test)))"));
assert!(attribute_marks_test("cfg(not(foo), test)"));
assert!(!attribute_marks_test("cfg(not(test))"));
assert!(!attribute_marks_test("cfg(not(foo, bar))"));
assert!(!attribute_marks_test("cfg(not(test, unix))"));
assert!(attribute_marks_test("cfg(all(test, unix))"));
}
#[test]
fn rust_attr_test_tolerates_internal_whitespace() {
assert!(attribute_marks_test("cfg( all( unix , test ) )"));
assert!(!attribute_marks_test("cfg( not ( test ) )"));
}
#[test]
fn rust_attr_test_handles_deeply_nested_cfg_without_overflow() {
const DEPTH: usize = 50_000;
fn nest(comb: &str, inner: &str) -> String {
let mut s = String::with_capacity(DEPTH * (comb.len() + 1) + inner.len() + DEPTH + 5);
s.push_str("cfg(");
for _ in 0..DEPTH {
s.push_str(comb);
s.push('(');
}
s.push_str(inner);
for _ in 0..DEPTH {
s.push(')');
}
s.push(')');
s
}
assert!(
attribute_marks_test(&nest("all", "test")),
"deeply nested all(...) wrapping `test` must mark test-only"
);
assert!(
!attribute_marks_test(&nest("any", "unix")),
"deeply nested any(...) without `test` must not mark test-only"
);
assert!(
!attribute_marks_test(&nest("all", "not(test)")),
"deeply nested not(test) must remain production-only"
);
}
#[test]
fn cfg_predicate_classification_matches_pre_1105_walker() {
let cases: &[(&str, bool)] = &[
("all(test", false),
("all(test))", false),
("all((test)", false),
("all(test)(x)", false),
("all(a)(test)", false),
("all(test)x", false),
(")test", false),
("test)", false),
("(test", false),
("a),test", false),
("all(a))(b, test)", false),
("all(a))(b, all(test))", false),
("any(test", false),
("", false),
(" ", false),
("all()", false),
("any()", false),
("not()", false),
("all( )", false),
("all(,)", false),
(",", false),
(",,", false),
("all(,test)", true),
("all(test,)", true),
("all(test,,)", true),
("testing", false),
("not_test", false),
("x_test", false),
("alltest", false),
("nottest", false),
("anytest", false),
("all(testing)", false),
("all(x_test, testing)", false),
("feature = \"test\"", false),
("all(feature = \"test\")", false),
("any(v = \"a,b\")", false),
("all(v = \"(\", test)", false),
("all(v = \"r#\\\"test(\\\"#\")", false),
("cfg_attr(test, derive(Debug))", false),
("cfg(test)", false),
("all(cfg(test))", false),
("not(all(test))", false),
("not(any(test))", false),
("not(not(test))", false),
("all(not(test), test)", true),
("any(not(test), unix)", false),
(" all ( test ) ", true),
("all\t(test)", true),
("all\n(\ntest\n)", true),
("not (test)", false),
("all( unix , test )", true),
("all(é, test)", true),
("all(日本語)", false),
("тест", false),
("all(тест, test)", true),
("all(all(all(test)))", true),
("any(all(any(test)))", true),
("all(any(unix), test)", true),
("all(a, b, c, test)", true),
("all(a, b, c, unix)", false),
("any(all(unix, test), all(windows, foo))", true),
("all(all(a,b), all(c,d))", false),
("a,all(b,c),test", true),
];
for &(pred, expected) in cases {
assert_eq!(
cfg_predicate_marks_test(pred),
expected,
"predicate {pred:?} must classify as {expected}"
);
}
}
#[test]
fn comma_index_buckets_by_paren_depth() {
let pred = "a,all(b,c),any(d,all(e,f))";
let index = CommaIndex::build(pred);
let depth0: Vec<usize> = index.splits(&(0..pred.len()), 0).collect();
assert_eq!(depth0, vec![1, 10], "commas outside any parens");
let args: Vec<usize> = index.splits(&(6..9), 1).collect();
assert_eq!(args, vec![7], "only the commas inside this region");
let depth1: Vec<usize> = index.splits(&(0..pred.len()), 1).collect();
assert_eq!(depth1, vec![7, 16], "both depth-1 commas");
let depth2: Vec<usize> = index.splits(&(0..pred.len()), 2).collect();
assert_eq!(depth2, vec![22], "the comma inside the inner all()");
assert!(
index.splits(&(0..pred.len()), 3).next().is_none(),
"no region nests three deep here"
);
let stray = CommaIndex::build("x,y),z");
assert_eq!(
stray.entries,
vec![(0, 1)],
"a comma at negative depth belongs to no region"
);
let restored = CommaIndex::build("a)(b,c");
assert_eq!(restored.entries, vec![(0, 4)]);
}
#[test]
fn strip_whitespace_preserves_non_ascii_utf8() {
assert_eq!(strip_whitespace("é test"), "étest");
assert_eq!(strip_whitespace("crate ::ñ::test"), "crate::ñ::test");
assert_eq!(strip_whitespace(" 日本語 test"), "日本語test");
assert_eq!(
strip_whitespace("cfg( all( unix , test ) )"),
"cfg(all(unix,test))"
);
}
}