big-code-analysis 2.1.0

Tool to compute and export code metrics
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
//! Ad-hoc parser for Rust `cfg(...)` attribute predicates.
//!
//! Determines whether a Rust attribute body marks the annotated item as
//! test-only. Inputs are the *contents* of a `#[...]` / `#![...]`
//! attribute (e.g. `"test"`, `"cfg(test)"`, `"cfg(all(unix, test))"`),
//! not AST nodes — the predicate walker is intentionally a string-level
//! mini-parser because tree-sitter-rust does not expand attribute
//! macros for us.
//!
//! Extracted from `checker.rs` so the cfg parsing rules live next to
//! each other and can be exercised in isolation. The single public
//! entry point is [`attribute_marks_test`]; everything else is module-
//! private.

use std::{iter, ops::Range};

/// Return `true` if the Rust attribute body marks the annotated item
/// as test-only.
///
/// Recognised forms:
///
/// - Bare test-attribute aliases: `test`, `rstest`, `wasm_bindgen_test`,
///   `test_case`.
/// - Path-form test attributes: `tokio::test`, `ext::module::test(args)`,
///   etc. — detected without entering the predicate walker.
/// - `cfg(...)` predicates where `test` appears as an operand of `all`,
///   `any`, or a bare comma list, at any depth. A `not(test)` operand
///   short-circuits — the item is included in production builds, so it
///   is not test-only (regression test for #278).
///
/// The slow path collapses interior whitespace and retries, tolerating
/// unusual spacing like `# [ cfg ( test ) ]`.
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;
    }
    // Slow path is only worth running when the input actually has
    // interior whitespace; the common cases hit the fast path above.
    if trimmed.bytes().any(|b| b.is_ascii_whitespace()) {
        return matches_test(&strip_whitespace(trimmed));
    }
    false
}

/// Strip interior whitespace from `s`, preserving multi-byte UTF-8.
///
/// Uses `chars()` (not `bytes().map(char::from)`) so a multi-byte
/// sequence like `é` (`0xC3 0xA9`) survives as a single `é` rather
/// than getting mangled into the two Latin-1 codepoints `é`.
fn strip_whitespace(s: &str) -> String {
    s.chars().filter(|c| !c.is_whitespace()).collect()
}

/// Return the inner predicate text of a `cfg(...)` attribute body,
/// stripping the `cfg(` prefix and matching `)`. Whitespace inside
/// is tolerated; callers receive a slice with surrounding spacing
/// preserved so the predicate walker can re-split on commas / parens.
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)
}

/// Byte offsets of every comma in a cfg predicate, bucketed by the
/// paren nesting depth the comma sits at.
///
/// A predicate is classified one *region* at a time: first the whole
/// predicate, then the argument list of every `all(...)` / `any(...)`
/// operand found inside it. A comma splits a region into operands
/// exactly when it sits at that region's own nesting depth, and a
/// region's depth is always its nesting level — a region begins right
/// after the `(` of an operand that itself starts at the parent
/// region's depth, so each descent adds exactly one. That makes the
/// depth a comma is recorded at directly comparable to the depth of
/// the region asking about it, so a single forward scan indexes the
/// split points of every region at once.
///
/// Before issue #1105 each region instead re-scanned its whole
/// interior just to learn whether it held a top-level comma, so a
/// predicate nested `d` levels deep was scanned `d` times over —
/// O(len²) in the attribute body, and a denial-of-service vector for
/// any `exclude_tests` run over machine-generated Rust.
struct CommaIndex {
    /// `(depth, offset)` pairs in ascending order, so the commas that
    /// split any one region occupy a contiguous run. Ordering by depth
    /// before offset is load-bearing — it is what groups a region's
    /// split points together for [`CommaIndex::splits`].
    entries: Vec<(usize, usize)>,
}

impl CommaIndex {
    /// Index every comma in `pred` by the paren depth it appears at.
    ///
    /// Depth is a signed running count, matching the split rule this
    /// replaced: an unbalanced `)` drives it negative and a later `(`
    /// brings it back up. A comma stranded at a negative depth belongs
    /// to no region and is dropped.
    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 }
    }

    /// Offsets of the commas that split `region`, whose own operands
    /// sit at `depth`.
    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)
    }
}

/// Return `true` if the cfg predicate `pred` marks the item as
/// test-only.
///
/// Driven by an explicit work stack rather than mutual recursion: a
/// pathological deeply-nested input such as
/// `cfg(all(all(all(…test…))))` would otherwise recurse once per
/// nesting level (`cfg_predicate_marks_test` → operand walk →
/// `cfg_predicate_marks_test`) and overflow the stack on adversarial
/// or machine-generated attribute bodies (issue #709). The work stack
/// keeps live state on the heap, so nesting depth is bounded by
/// available memory rather than the call-frame limit.
///
/// Every operand is classified by [`classify_cfg_operand`]; the
/// [`CommaIndex`] turns "where does this region split" into a lookup
/// instead of a rescan, so the whole walk is linear in `pred` up to
/// the index sort (issue #1105).
fn cfg_predicate_marks_test(pred: &str) -> bool {
    let commas = CommaIndex::build(pred);
    // Regions still to classify, as `(byte range, nesting depth)`. Both
    // this and the index are bounded by `pred`'s length, so a deeper
    // predicate costs proportionally more memory, never more per byte.
    let mut stack = vec![(0..pred.len(), 0_usize)];
    while let Some((region, depth)) = stack.pop() {
        // Bare comma-separated predicate lists like `cfg(test, foo)`
        // — pre-#278 callers relied on this form being treated as
        // `cfg(all(test, foo))`. Splitting the region MUST happen
        // before an operand meets the `not`/`all`/`any` prefix checks:
        // those classify by leading prefix and trailing `)`, which only
        // describe a single operand. For a list whose first operand is
        // `not(...)` and last ends in `)` — e.g. `not(foo), all(test)`
        // — `strip_prefix("not")` leaves `(foo), all(test)`, which both
        // starts with `(` and ends with `)`, so the `not` short-circuit
        // would otherwise swallow the whole list and drop the trailing
        // `test` (regression for #763). The index respects paren depth,
        // so a comma nested inside a predicate's own parens —
        // `not(foo, bar)`, `all(test, unix)` — is not a split point and
        // the operand reaches the prefix checks intact.
        //
        // The region's end acts as a final split point, so the operand
        // after the last comma — or the whole region, when there is no
        // comma at this depth — goes through the same classification.
        let mut operand_start = region.start;
        for boundary in commas.splits(&region, 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
}

/// How one operand of a cfg predicate classifies.
enum Operand {
    /// The bare `test` predicate: the item is test-only.
    Test,
    /// The argument list of an `all(...)` / `any(...)` operand, as a
    /// byte range of the predicate, to be walked one level deeper.
    Args(Range<usize>),
    /// Neither matches nor descends: `not(...)`, plain idents,
    /// `feature = "test"` and other key/value pairs.
    Opaque,
}

/// Classify one operand of a cfg predicate, given as a byte range of
/// `pred`.
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;
    }
    // `not(...)` short-circuits: we do not look inside, because
    // `not(test)` excludes the item from test builds (#278). Kept as an
    // explicit arm even though it is currently redundant — an operand
    // starting with `not` cannot match the `all`/`any` prefixes below,
    // so it would fall through to `Opaque` anyway. Deleting it would
    // make the #278 rule an emergent property of a prefix test three
    // lines down; stating it here keeps the rule visible if that test is
    // ever loosened.
    if trimmed
        .strip_prefix("not")
        .map(str::trim_start)
        .is_some_and(|rest| rest.starts_with('(') && rest.ends_with(')'))
    {
        return Operand::Opaque;
    }
    // `all(...)` and `any(...)` use the same "contains a `test`
    // operand" rule here. Strictly, `any(test, foo)` is over-broad (the
    // item is included in production when `foo` holds), but the
    // pre-#278 code treated both identically and the issue spec
    // preserves that behavior.
    //
    // The three conditions are one shape check, so they read as one
    // chain: combinator name, then its opening paren, then a closing
    // paren as the operand's *last* byte. That last byte is not
    // necessarily the opening paren's match — `all(a)(b)` has argument
    // list `a)(b`, and the walk must reproduce that.
    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(')')
    {
        // `raw.trim_end()` starts where `raw` does, so the trimmed
        // operand ends at `operand.start + raw.trim_end().len()`.
        // `inside` is a suffix of that, and `args` a prefix of `inside`,
        // so both map back onto `pred` by length alone.
        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() {
        // Direct attribute names (and aliases) match without ever
        // entering the cfg predicate walker. Locks in pre-#278
        // behavior so the rewrite does not regress the common case.
        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() {
        // Pre-#278 forms with `test` in the first position must
        // still match.
        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() {
        // Regression for #278. `test` was previously required to be
        // the first operand of `all(...)` / `any(...)`. The predicate
        // walker now matches it anywhere.
        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"
        );
        // Nested predicate: `any(test, ...)` inside `all(...)` still
        // counts as test-only via recursion.
        assert!(attribute_marks_test(
            "cfg(all(unix, any(test, feature = \"x\")))"
        ));
    }

    #[test]
    fn rust_attr_test_skips_not_test_and_feature_named_test() {
        // `cfg(not(test))` is *production-only*; it must not be
        // treated as test-only or `exclude_tests` would strip
        // production code.
        assert!(!attribute_marks_test("cfg(not(test))"));
        assert!(!attribute_marks_test("cfg(all(unix, not(test)))"));
        // A feature literally named "test" is a string-valued
        // key/value pair, not the bare `test` predicate.
        assert!(!attribute_marks_test("cfg(feature = \"test\")"));
        assert!(!attribute_marks_test("cfg(all(unix, feature = \"test\"))"));
        // Unrelated predicates remain unmatched.
        assert!(!attribute_marks_test("cfg(unix)"));
        assert!(!attribute_marks_test("derive(Debug)"));
        // `all(...)` / `any(...)` with no `test` operand anywhere must
        // not match — guards against an over-eager walker that treats
        // any combinator as test-only regardless of contents.
        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\")))"
        ));
        // Nested `not(test)` inside `any(...)` is still non-matching;
        // `not(...)` short-circuits at any depth.
        assert!(!attribute_marks_test("cfg(any(unix, not(test)))"));
    }

    #[test]
    fn rust_attr_test_not_led_comma_list_keeps_later_test_operand() {
        // Regression for #763. A top-level comma list whose FIRST
        // operand is `not(...)` and whose LAST operand ends in `)` was
        // misclassified as a single `not(...)` operand: the `not`
        // short-circuit fired on the entire list, discarding the
        // trailing `test`-bearing operand. These must mark test-only.
        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"
        );
        // Cases that were already correct must keep working: the
        // wrapped form, and a list not ending in `)`.
        assert!(attribute_marks_test("cfg(all(not(foo), all(test)))"));
        assert!(attribute_marks_test("cfg(not(foo), test)"));
        // A pure `not(test)` (single operand, no top-level comma) still
        // short-circuits to production-only.
        assert!(!attribute_marks_test("cfg(not(test))"));
        // A comma INSIDE the `not(...)` predicate's own parens is a
        // single operand, not a list — `not(foo, bar)` must remain a
        // short-circuiting non-match, and `all(test, unix)` must still
        // match via its own operand walk.
        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() {
        // The slow path strips ASCII whitespace before re-running
        // both checks, so spaced forms still resolve correctly.
        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() {
        // Regression test for issue #709. The former mutual recursion
        // (`cfg_predicate_marks_test` ⇄ operand walk) recursed once per
        // nesting level and overflowed the stack on pathological input.
        // This depth comfortably blows a recursive stack (a recursive
        // walker overflows in the low tens of thousands of frames) yet
        // the work-stack walker must terminate and preserve semantics.
        const DEPTH: usize = 50_000;

        // Build `cfg(comb(comb(… inner …)))` directly — O(n) — rather than
        // by repeated `format!`, which is O(n²) in the nesting depth.
        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
        }

        // all(all(all(... test ...))) — `test` buried at the bottom marks
        // the item test-only.
        assert!(
            attribute_marks_test(&nest("all", "test")),
            "deeply nested all(...) wrapping `test` must mark test-only"
        );

        // Same depth wrapping a non-test operand must still return false
        // rather than overflowing.
        assert!(
            !attribute_marks_test(&nest("any", "unix")),
            "deeply nested any(...) without `test` must not mark test-only"
        );

        // A deeply nested `not(test)` must short-circuit at the wrapping
        // depth without descending — still non-matching, still no overflow.
        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() {
        // Issue #1105 replaced the pop-and-rescan predicate walker with
        // a comma index plus one classification pass. Every expectation
        // below was produced by running the *pre-#1105* walker over the
        // input, then transcribed here, so the table pins the exact
        // behaviour the rewrite had to preserve — including the corners
        // no hand-written test covered: unbalanced parens, empty and
        // whitespace-only operands, `test` as a substring, and the
        // long-standing blind spot that parens and commas inside string
        // literals are counted as structure.
        //
        // The rewrite was additionally checked against the old walker
        // over millions of generated predicates with zero disagreements;
        // this table is the cheap, checked-in residue of that run. Note
        // the generator alphabet has to be able to spell `test`:
        // `trimmed == "test"` is the only check in either implementation
        // that can return `true`, so a sweep over an alphabet without
        // `e` and `s` agrees trivially on every input and proves nothing
        // about the bug class that matters (a missed match).
        let cases: &[(&str, bool)] = &[
            // Unbalanced or truncated parens: the `all(...)` shape check
            // needs the operand's *last* byte to be `)`, so trailing
            // junk or a missing paren drops the whole operand.
            ("all(test", false),
            ("all(test))", false),
            ("all((test)", false),
            // `all(test)(x)` is the load-bearing member of this pair:
            // under matching-paren semantics it would be `true`. Its
            // neighbour classifies the same either way — keep both, but
            // do not drop this one.
            ("all(test)(x)", false),
            ("all(a)(test)", false),
            ("all(test)x", false),
            (")test", false),
            ("test)", false),
            ("(test", false),
            // A stray `)` drives the depth counter negative. A comma at
            // negative depth belongs to no region and is dropped, so it
            // splits nothing: `a),test` is one dead operand rather than
            // two. Clamping the depth at 0 instead would make that comma
            // a top-level split and wrongly surface the trailing `test`,
            // so this row is the end-to-end guard on the signed counter.
            ("a),test", false),
            // These two are *not* negative-depth cases despite the stray
            // `)`: the following `(` restores depth to 0, so the comma
            // does split. They are false because the second operand ends
            // in a trailing paren. Kept for the last-byte rule, not the
            // depth rule.
            ("all(a))(b, test)", false),
            ("all(a))(b, all(test))", false),
            ("any(test", false),
            // Empty and whitespace-only operands.
            ("", false),
            ("   ", false),
            ("all()", false),
            ("any()", false),
            ("not()", false),
            ("all( )", false),
            ("all(,)", false),
            (",", false),
            (",,", false),
            ("all(,test)", true),
            ("all(test,)", true),
            ("all(test,,)", true),
            // `test` as a substring of another identifier must not match.
            ("testing", false),
            ("not_test", false),
            ("x_test", false),
            ("alltest", false),
            ("nottest", false),
            ("anytest", false),
            ("all(testing)", false),
            ("all(x_test, testing)", false),
            // String literals are not lexed: a `(` or `,` inside one
            // still moves the depth counter. `all(v = "(", test)` is
            // therefore read as one operand and misses the `test`. This
            // is pre-existing behaviour, pinned here so a future lexer
            // change is a deliberate decision rather than a surprise.
            ("feature = \"test\"", false),
            ("all(feature = \"test\")", false),
            ("any(v = \"a,b\")", false),
            ("all(v = \"(\", test)", false),
            ("all(v = \"r#\\\"test(\\\"#\")", false),
            // Only `all` / `any` descend; every other combinator, `cfg`
            // and `cfg_attr` included, is an opaque operand.
            ("cfg_attr(test, derive(Debug))", false),
            ("cfg(test)", false),
            ("all(cfg(test))", false),
            // `not(...)` short-circuits at any depth, even around a
            // combinator that would otherwise match.
            ("not(all(test))", false),
            ("not(any(test))", false),
            ("not(not(test))", false),
            ("all(not(test), test)", true),
            ("any(not(test), unix)", false),
            // Whitespace between the combinator and its parens, and
            // around operands, is tolerated.
            (" all ( test ) ", true),
            ("all\t(test)", true),
            ("all\n(\ntest\n)", true),
            ("not (test)", false),
            ("all( unix , test )", true),
            // Non-ASCII operands neither match nor break byte offsets.
            ("all(é, test)", true),
            ("all(日本語)", false),
            ("тест", false),
            ("all(тест, test)", true),
            // Ordinary nesting.
            ("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),
            // Sibling regions at the same depth, where the *earlier*
            // sibling contains a comma. Nothing else in this table has
            // that shape, and without it the lower bound of the index
            // lookup is never varied: dropping it leaves every other row
            // passing while these panic on an inverted slice range.
            ("any(all(unix, test), all(windows, foo))", true),
            ("all(all(a,b), all(c,d))", false),
            // A top-level comma list whose `test` follows a nested
            // region. The index must be ordered for the trailing operand
            // to be reached at all.
            ("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() {
        // The index is what makes classification linear: a region asks
        // for the commas at its own nesting depth instead of rescanning
        // its interior. Seed a predicate whose commas sit at three
        // different depths so each bucket is distinguishable from the
        // others and from an empty one.
        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");
        // `all(b,c)` spans 2..10; its argument list 6..9 holds one
        // depth-1 comma, and the depth-1 comma of `any(...)` is outside
        // that range and must not leak in.
        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"
        );

        // A `)` with no opener drives the depth counter negative, so the
        // comma that follows splits nothing and is dropped entirely —
        // the behaviour the former `cfg_split_top_level_args` had, and
        // what makes `a),test` a single dead operand.
        //
        // Seeded with a depth-0 comma *before* the stray `)` so the
        // assertion distinguishes "dropped the negative-depth comma"
        // from "recorded no commas at all"; against an empty index both
        // readings look identical (.claude/rules/testing.md).
        let stray = CommaIndex::build("x,y),z");
        assert_eq!(
            stray.entries,
            vec![(0, 1)],
            "a comma at negative depth belongs to no region"
        );
        // The following `(` brings the counter back to zero, restoring
        // the comma as a top-level split point.
        let restored = CommaIndex::build("a)(b,c");
        assert_eq!(restored.entries, vec![(0, 4)]);
    }

    #[test]
    fn strip_whitespace_preserves_non_ascii_utf8() {
        // Regression test for #312. The slow path previously rebuilt
        // the compact string with `bytes().map(char::from).collect()`,
        // which interprets each byte as a Latin-1 codepoint and
        // mangles any multi-byte UTF-8 sequence. `é` (`0xC3 0xA9`)
        // would emerge as the two-char string `é`. Iterating over
        // `chars()` decodes UTF-8 correctly.
        assert_eq!(strip_whitespace("é test"), "étest");
        assert_eq!(strip_whitespace("crate ::ñ::test"), "crate::ñ::test");
        assert_eq!(strip_whitespace("  日本語  test"), "日本語test");
        // ASCII-only inputs round-trip identically to the old code.
        assert_eq!(
            strip_whitespace("cfg( all( unix , test ) )"),
            "cfg(all(unix,test))"
        );
    }
}