Skip to main content

big_code_analysis/metrics/
abc.rs

1// Per-language metric and AST modules deliberately consume the macro-
2// generated tree-sitter token enums via `use crate::*` and `use Foo::*`
3// inside match expressions — explicit imports would list dozens of
4// variants per arm and obscure the per-language token sets that are the
5// point of these files. Allowed at the module level rather than per
6// function so the per-language impl blocks stay readable.
7#![allow(
8    clippy::enum_glob_use,
9    clippy::too_many_lines,
10    clippy::wildcard_imports
11)]
12// Metric counts (token, function, branch, argument, etc.) are stored as
13// `usize` and crossed with `f64` averages, ratios, and Halstead scores
14// across the cyclomatic / MI / Halstead computations. The `usize as f64`
15// and `f64 as usize` casts are intentional and snapshot-anchored — every
16// site is bounded by the count it came from. Allowing the lints at the
17// module level keeps the metric arithmetic legible.
18#![allow(
19    clippy::cast_precision_loss,
20    clippy::cast_possible_truncation,
21    clippy::cast_sign_loss
22)]
23
24use std::fmt;
25
26use crate::checker::Checker;
27
28use crate::macros::implement_metric_trait;
29
30use crate::*;
31
32mod bash;
33mod c;
34mod cpp;
35mod csharp;
36mod elixir;
37mod go;
38mod groovy;
39mod irules;
40mod java;
41mod js_family;
42mod kotlin;
43mod lua;
44mod mozcpp;
45mod objc;
46mod perl;
47mod php;
48mod python;
49mod ruby;
50mod rust;
51mod tcl;
52
53/// The `ABC` metric.
54///
55/// The `ABC` metric measures the size of a source code by counting
56/// the number of Assignments (`A`), Branches (`B`) and Conditions (`C`).
57/// The metric defines an ABC score as a vector of three elements (`<A,B,C>`).
58/// The ABC score can be represented by its individual components (`A`, `B` and `C`)
59/// or by the magnitude of the vector (`|<A,B,C>| = sqrt(A^2 + B^2 + C^2)`).
60///
61/// Official paper and definition:
62///
63/// Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report.
64///
65/// <https://www.softwarerenovation.com/Articles.aspx>
66///
67/// # Cross-language `&&` / `||` policy
68///
69/// Per Fitzpatrick's conditional-operator rule (Rule 5 in Figure 2
70/// for C and Figure 4 for Java; Rule 7 in Figure 3 for C++), only
71/// comparison operators (`==`, `!=`, `<=`, `>=`, `<`, `>`) and a
72/// paper-defined keyword set (`else`, `case`, `default`, `?`, plus
73/// `try` / `catch` for C++ and Java) contribute to the condition
74/// count. Per-language `impl Abc` blocks narrow this set where
75/// appropriate — e.g., C++/Rust/Go/Python exclude `default` since
76/// it falls through unconditionally (matching the Rust `_ =>` and
77/// Java `default:` precedent). The short-
78/// circuit logical operators `&&` and `||` (and per-language
79/// equivalents — Python's `and` / `or`, Lua's `and` / `or`, Tcl's
80/// `&&` / `||`, Perl's `&&` / `||` / `//` / `and` / `or` / `xor`)
81/// are deliberately **not** counted on their own. The paper's
82/// worked Listing 2 annotates `(am >= 0 && am <= 0xF) ? '/' : 'C'`
83/// as `accc` — three conditions for `>=`, `<=`, `?`, zero for
84/// `&&`.
85///
86/// Fitzpatrick's Rule 7 (Figure 3, C++) / Rule 9 (Figure 4, Java) —
87/// "Add one to the condition count for each unary conditional
88/// expression" — instead counts each non-comparison operand of a
89/// `&&` / `||` chain once. The paper's worked example for this
90/// rule is `if (x || y) printf("test failure\n");`, annotated:
91/// "there are two unary conditions since both `x` and `y` are
92/// tested as conditional expressions" (so `||` contributes zero,
93/// `x` contributes one, `y` contributes one, and `printf(...)`
94/// contributes one branch). The walker machinery for this —
95/// modelled on `java_count_unary_conditions` /
96/// `java_inspect_container` — is present today for Java, Groovy,
97/// C#, Rust, Go, JavaScript, TypeScript, TSX, Mozjs, PHP, C++,
98/// Python, Perl, Lua, Tcl, iRules, Kotlin, Ruby, and Elixir. So
99/// `if (a && b)` reports 2 conditions across this set, matching
100/// the paper. Bash is the lone exception: its `&&` / `||` are
101/// command-list separators rather than boolean-expression operands
102/// with named leaf operands, so Fitzpatrick's Rule 9 does not map
103/// onto its grammar and the walker is deliberately not wired.
104///
105/// This policy is paper-faithful and deviates from RuboCop's
106/// `Metrics/AbcSize` (which counts `and` / `or` as conditions
107/// directly) while matching `StepicOrg/abcmeter` and
108/// `eoinnoble/python-abc`. The book's *ABC counting rules*
109/// section reproduces the rule tables, a per-language deviation
110/// table, and worked examples — see the chapter at
111/// <https://dekobon.github.io/big-code-analysis/metrics.html#abc>.
112///
113/// # Cross-language empty-`for`-condition policy
114///
115/// `for (;;)` — and every other spelling that omits the test slot
116/// (`for (init; ; update)`, Go's bare `for {}`) — counts **zero**
117/// conditions. Nothing in Fitzpatrick's condition rules attributes a
118/// count to a `for` keyword: they count conditional operators and
119/// unary conditions that are *present*, and an omitted test is not a
120/// decision. Most languages get this for free: `*_walk_for_statement`
121/// asks `for_statement` for its `condition` field and an empty header
122/// has none. Two do not, and neither needs a special case either —
123/// the JS family fills the slot with an `empty_statement`, which is
124/// not a boolean terminal and not a paren / `!` wrapper, so it falls
125/// through; and Go, whose `for_statement` exposes no `condition` field
126/// at all, locates the header slot structurally and finds only the
127/// body. Before #1276 Java and Groovy alone disagreed, counting the
128/// `;` or `)` that landed in a positional child slot as a
129/// vacuously-true condition.
130///
131/// See issue #395 for the Phase-1 cross-language policy
132/// alignment, #403 for the Phase-2 unary-conditional walker
133/// fan-out, #404 for the Phase-3 book documentation, #557
134/// for the Kotlin / Ruby / Elixir walker wiring, and #1276 for the
135/// `for`-header condition slot.
136#[derive(Debug, Clone, PartialEq)]
137#[non_exhaustive]
138pub struct Stats {
139    pub(super) assignments: f64,
140    assignments_sum: f64,
141    assignments_min: f64,
142    assignments_max: f64,
143    pub(super) branches: f64,
144    branches_sum: f64,
145    branches_min: f64,
146    branches_max: f64,
147    pub(super) conditions: f64,
148    conditions_sum: f64,
149    conditions_min: f64,
150    conditions_max: f64,
151    space_count: usize,
152}
153
154impl Default for Stats {
155    fn default() -> Self {
156        Self {
157            assignments: 0.,
158            assignments_sum: 0.,
159            assignments_min: f64::MAX,
160            assignments_max: 0.,
161            branches: 0.,
162            branches_sum: 0.,
163            branches_min: f64::MAX,
164            branches_max: 0.,
165            conditions: 0.,
166            conditions_sum: 0.,
167            conditions_min: f64::MAX,
168            conditions_max: 0.,
169            space_count: 1,
170        }
171    }
172}
173
174impl fmt::Display for Stats {
175    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
176        write!(
177            f,
178            "assignments: {}, branches: {}, conditions: {}, magnitude: {}, \
179            assignments_average: {}, branches_average: {}, conditions_average: {}, \
180            assignments_min: {}, assignments_max: {}, \
181            branches_min: {}, branches_max: {}, \
182            conditions_min: {}, conditions_max: {}",
183            self.assignments_sum(),
184            self.branches_sum(),
185            self.conditions_sum(),
186            self.magnitude_sum(),
187            self.assignments_average(),
188            self.branches_average(),
189            self.conditions_average(),
190            self.assignments_min(),
191            self.assignments_max(),
192            self.branches_min(),
193            self.branches_max(),
194            self.conditions_min(),
195            self.conditions_max()
196        )
197    }
198}
199
200impl Stats {
201    /// Merges a second `Abc` metric into the first one.
202    pub fn merge(&mut self, other: &Stats) {
203        // Calculates minimum and maximum values
204        self.assignments_min = self.assignments_min.min(other.assignments_min);
205        self.assignments_max = self.assignments_max.max(other.assignments_max);
206        self.branches_min = self.branches_min.min(other.branches_min);
207        self.branches_max = self.branches_max.max(other.branches_max);
208        self.conditions_min = self.conditions_min.min(other.conditions_min);
209        self.conditions_max = self.conditions_max.max(other.conditions_max);
210
211        self.assignments_sum += other.assignments_sum;
212        self.branches_sum += other.branches_sum;
213        self.conditions_sum += other.conditions_sum;
214
215        self.space_count += other.space_count;
216    }
217
218    /// Returns the `Abc` assignments metric value.
219    #[must_use]
220    pub fn assignments(&self) -> u64 {
221        self.assignments as u64
222    }
223
224    /// Returns the `Abc` assignments sum metric value.
225    #[must_use]
226    pub fn assignments_sum(&self) -> u64 {
227        self.assignments_sum as u64
228    }
229
230    /// Returns the `Abc` assignments average value.
231    ///
232    /// This value is computed dividing the `Abc`
233    /// assignments value for the number of spaces.
234    #[must_use]
235    pub fn assignments_average(&self) -> f64 {
236        crate::metrics::average(self.assignments_sum() as f64, self.space_count)
237    }
238
239    /// Returns the `Abc` assignments minimum value.
240    ///
241    /// Collapses the `f64::MAX` sentinel that `Stats::default()` plants
242    /// into `assignments_min` to `0`, so a never-observed space
243    /// serializes to a meaningful number rather than `1.7976931e308`.
244    #[allow(clippy::float_cmp)]
245    #[must_use]
246    pub fn assignments_min(&self) -> u64 {
247        if self.assignments_min == f64::MAX {
248            0
249        } else {
250            self.assignments_min as u64
251        }
252    }
253
254    /// Returns the `Abc` assignments maximum value.
255    #[must_use]
256    pub fn assignments_max(&self) -> u64 {
257        self.assignments_max as u64
258    }
259
260    /// Returns the `Abc` branches metric value.
261    #[must_use]
262    pub fn branches(&self) -> u64 {
263        self.branches as u64
264    }
265
266    /// Returns the `Abc` branches sum metric value.
267    #[must_use]
268    pub fn branches_sum(&self) -> u64 {
269        self.branches_sum as u64
270    }
271
272    /// Returns the `Abc` branches average value.
273    ///
274    /// This value is computed dividing the `Abc`
275    /// branches value for the number of spaces.
276    #[must_use]
277    pub fn branches_average(&self) -> f64 {
278        crate::metrics::average(self.branches_sum() as f64, self.space_count)
279    }
280
281    /// Returns the `Abc` branches minimum value.
282    ///
283    /// Same `f64::MAX` sentinel collapse as `assignments_min`.
284    #[allow(clippy::float_cmp)]
285    #[must_use]
286    pub fn branches_min(&self) -> u64 {
287        if self.branches_min == f64::MAX {
288            0
289        } else {
290            self.branches_min as u64
291        }
292    }
293
294    /// Returns the `Abc` branches maximum value.
295    #[must_use]
296    pub fn branches_max(&self) -> u64 {
297        self.branches_max as u64
298    }
299
300    /// Returns the `Abc` conditions metric value.
301    #[must_use]
302    pub fn conditions(&self) -> u64 {
303        self.conditions as u64
304    }
305
306    /// Returns the `Abc` conditions sum metric value.
307    #[must_use]
308    pub fn conditions_sum(&self) -> u64 {
309        self.conditions_sum as u64
310    }
311
312    /// Returns the `Abc` conditions average value.
313    ///
314    /// This value is computed dividing the `Abc`
315    /// conditions value for the number of spaces.
316    #[must_use]
317    pub fn conditions_average(&self) -> f64 {
318        crate::metrics::average(self.conditions_sum() as f64, self.space_count)
319    }
320
321    /// Returns the `Abc` conditions minimum value.
322    ///
323    /// Same `f64::MAX` sentinel collapse as `assignments_min`.
324    #[allow(clippy::float_cmp)]
325    #[must_use]
326    pub fn conditions_min(&self) -> u64 {
327        if self.conditions_min == f64::MAX {
328            0
329        } else {
330            self.conditions_min as u64
331        }
332    }
333
334    /// Returns the `Abc` conditions maximum value.
335    #[must_use]
336    pub fn conditions_max(&self) -> u64 {
337        self.conditions_max as u64
338    }
339
340    /// Returns the `Abc` magnitude metric value.
341    #[must_use]
342    pub fn magnitude(&self) -> f64 {
343        (self.assignments.powi(2) + self.branches.powi(2) + self.conditions.powi(2)).sqrt()
344    }
345
346    /// Returns the `Abc` magnitude sum metric value.
347    #[must_use]
348    pub fn magnitude_sum(&self) -> f64 {
349        (self.assignments_sum.powi(2) + self.branches_sum.powi(2) + self.conditions_sum.powi(2))
350            .sqrt()
351    }
352
353    #[inline]
354    pub(crate) fn compute_sum(&mut self) {
355        self.assignments_sum += self.assignments;
356        self.branches_sum += self.branches;
357        self.conditions_sum += self.conditions;
358    }
359
360    #[inline]
361    pub(crate) fn compute_minmax(&mut self) {
362        self.assignments_min = self.assignments_min.min(self.assignments);
363        self.assignments_max = self.assignments_max.max(self.assignments);
364        self.branches_min = self.branches_min.min(self.branches);
365        self.branches_max = self.branches_max.max(self.branches);
366        self.conditions_min = self.conditions_min.min(self.conditions);
367        self.conditions_max = self.conditions_max.max(self.conditions);
368        self.compute_sum();
369    }
370}
371
372#[doc(hidden)]
373/// Per-language computation of the ABC metric.
374pub(crate) trait Abc
375where
376    Self: Checker,
377{
378    /// Walk `node` and update `stats` with this metric for the language
379    /// implementing the trait.
380    ///
381    /// `code` is the source bytes underlying the parsed tree. Most
382    /// languages ignore it: assignments, branches, and conditions all
383    /// surface as distinct grammar productions and a `kind_id()` match
384    /// is enough. Elixir is the exception — `case` / `cond` / `if` /
385    /// `with` / guard `when` arms surface as `Call` nodes whose keyword
386    /// target lives only in the source text. Matching the `Cyclomatic`
387    /// / `Halstead` / `Exit` / `Cognitive` pattern keeps the signature
388    /// uniform.
389    ///
390    /// `ancestors` is the chain the walker descended through. Nearly
391    /// every language classifies some token by what encloses it: `<`
392    /// and `>` are comparisons only under a binary expression (a type
393    /// -argument list otherwise), and `&&` / `||` reach their operands
394    /// through the enclosing chain node. Reaching that parent with
395    /// [`Node::parent`] costs `O(depth)` per node (#1096). The
396    /// condition-slot walkers take *their* parent as an argument
397    /// instead, because the caller descended from it.
398    fn compute<'a>(
399        node: &Node<'a>,
400        code: &'a [u8],
401        ancestors: Ancestors<'a, '_>,
402        stats: &mut Stats,
403    );
404}
405
406// Shared Phase-2B helper (issue #403): walk every named child of an
407// expression-list-style wrapper (Go's `expression_list`, Lua's
408// `expression_list`) and route each through a language-specific
409// classifier. Used for `return value1, value2, ...` arms where the
410// values live one level below the return statement under a list
411// wrapper. The classifier receives only named children so that
412// `,` / `;` / `(` / `)` tokens never reach it, plus `list` itself as
413// the child's parent — the container classifiers seed their
414// boolean-context flag from the parent kind, and reaching it with
415// `Node::parent` would cost `O(depth)` per node (#1096).
416pub(super) fn for_each_named_child(
417    list: &Node,
418    conditions: &mut f64,
419    f: fn(&Node, &Node, &mut f64),
420) {
421    let mut cursor = list.cursor();
422    if cursor.goto_first_child() {
423        loop {
424            let child = cursor.node();
425            if child.is_named() {
426                f(&child, list, conditions);
427            }
428            if !cursor.goto_next_sibling() {
429                break;
430            }
431        }
432    }
433}
434
435// Default no-op `Abc` impls. Audited in #188; the matrix below
436// records the rationale for every entry so the no-op default is a
437// deliberate choice, not scaffolding leftover.
438//
439// Real defaults (the language has no construct ABC measures, so the
440// metric is genuinely 0):
441//   - PreprocCode, CcommentCode: no executable code (comments /
442//     preprocessor lines only).
443implement_metric_trait!(Abc, PreprocCode, CcommentCode);
444
445#[cfg(test)]
446#[allow(
447    clippy::float_cmp,
448    clippy::cast_precision_loss,
449    clippy::cast_possible_truncation,
450    clippy::cast_sign_loss,
451    clippy::similar_names,
452    clippy::doc_markdown,
453    clippy::needless_raw_string_hashes,
454    clippy::too_many_lines
455)]
456mod tests {
457    #[cfg(feature = "csharp")]
458    use crate::test_support::assert_csharp_fixture_spells;
459    use crate::test_support::{
460        assert_fixture_spells, ast_has_kind_id, check_func_space_only_shim,
461        check_metrics_only_shim, child_space, metrics_verbatim,
462    };
463    // Hand-written, not derived. A trait is used through its methods, so
464    // nothing in a test body names `ParserTrait` and
465    // `check-test-lang-gates.py` derives no gate for an import at all,
466    // so an ungated one stays compiled into builds that never call
467    // `<Lang>Parser::new`. These seven are the languages that do; a new
468    // `XParser::new` test in this module needs its feature added here,
469    // and the leg that lacks it says so as an unused import.
470    #[cfg(any(
471        feature = "csharp",
472        feature = "groovy",
473        feature = "kotlin",
474        feature = "php",
475        feature = "ruby",
476        feature = "rust",
477        feature = "typescript",
478    ))]
479    use crate::traits::ParserTrait;
480
481    use super::*;
482
483    check_metrics_only_shim!(check_metrics, Abc);
484    // Every `check_func_space` caller in this module is an ABC-versus-
485    // cyclomatic parity test (`abc.conditions() == cyclomatic() - 1` on
486    // the same space), so the func-space shim carries Cyclomatic too.
487    check_func_space_only_shim!(check_func_space, Abc, Cyclomatic);
488
489    /// `abc.conditions_sum()` of `src` under a walk restricted to ABC
490    /// and the metrics it resolves, for the cross-language parity tests
491    /// that compare one construct across sibling grammars.
492    /// `check_metrics` expands to a plain `fn` and so cannot close over
493    /// a reference value; this can. Restricted rather than
494    /// `MetricsOptions::default()` per `metrics_verbatim`'s own doc
495    /// (#1127).
496    #[cfg(any(
497        feature = "c",
498        feature = "cpp",
499        feature = "go",
500        feature = "javascript",
501        feature = "mozcpp",
502        feature = "mozjs",
503        feature = "objc",
504        feature = "perl",
505        feature = "typescript",
506    ))]
507    fn abc_conditions(lang: LANG, src: &str) -> u64 {
508        metrics_verbatim(
509            lang,
510            src.as_bytes(),
511            MetricsOptions::default().with_only(&[crate::Metric::Abc]),
512        )
513        .abc
514        .conditions_sum()
515    }
516
517    // Recurse to the innermost function space and assert the invariant
518    // named above there. `conditions()` is that one space's own count,
519    // so it pairs with `cyclomatic()` and never `cyclomatic_sum()`,
520    // which folds in a base of 1 per nested space.
521    #[cfg(any(
522        feature = "cpp",
523        feature = "csharp",
524        feature = "groovy",
525        feature = "java",
526        feature = "perl",
527        feature = "php",
528        feature = "typescript",
529    ))]
530    fn assert_deepest_conditions_match_cyclomatic(space: &crate::FuncSpace, expected: u64) {
531        let mut deepest = space;
532        while let Some(child) = deepest.spaces.last() {
533            deepest = child;
534        }
535        let decisions = deepest.metrics.cyclomatic.cyclomatic() - 1;
536        assert_eq!(decisions, expected);
537        assert_eq!(deepest.metrics.abc.conditions(), decisions);
538    }
539
540    // The sibling form of the helper above, for a fixture whose point is
541    // that several *different* spellings of one construct agree. The
542    // deepest-space walk cannot serve those: it follows `spaces.last()`
543    // and so inspects exactly one member, which is the shape #1383's
544    // over-count hid behind — a merged root total cannot tell a
545    // relational method scoring 2 from the constant control scoring 2.
546    #[cfg(any(feature = "csharp", feature = "elixir"))]
547    fn assert_every_member_scores(
548        container: &crate::FuncSpace,
549        members: usize,
550        expected: u64,
551        why: &str,
552    ) {
553        // The count is a parameter rather than a `!is_empty()` check so
554        // a caller cannot omit it: without it a fixture that lost every
555        // member but one still satisfies "every member scores N".
556        assert_eq!(
557            container.spaces.len(),
558            members,
559            "member count changed — the fixture moved, not the metric"
560        );
561        for member in &container.spaces {
562            let name = member.name.as_deref().unwrap_or("?");
563            assert_eq!(member.metrics.abc.conditions(), expected, "{name}: {why}");
564            assert_eq!(
565                member.metrics.abc.conditions(),
566                member.metrics.cyclomatic.cyclomatic() - 1,
567                "{name}: §8 parity with the cyclomatic decision count"
568            );
569        }
570    }
571
572    // The second sibling, for a fixture whose point is that a construct
573    // moves a member *off* the value its unguarded control scores.
574    // `assert_every_member_scores` cannot serve those: it takes one
575    // expected value for the whole container, and every #1422 guard
576    // fixture deliberately pairs guarded members with an unguarded
577    // control one lower — which is the comparison the test exists to
578    // make. Both tiers per member, because a guard is a decision in each
579    // and a fix that moved only one of them would satisfy neither claim
580    // on its own, and because a member may legitimately sit *above* its
581    // decision count — an `else` arm, or a comparison nested inside
582    // another comparison, is an ABC condition with no cyclomatic
583    // decision behind it (#1421).
584    #[cfg(any(
585        feature = "csharp",
586        feature = "elixir",
587        feature = "groovy",
588        feature = "java",
589        feature = "kotlin",
590        feature = "python",
591        feature = "ruby",
592        feature = "rust",
593    ))]
594    #[track_caller]
595    fn assert_members_score(container: &crate::FuncSpace, expected: &[(&str, u64, u64)]) {
596        assert_eq!(
597            container.spaces.len(),
598            expected.len(),
599            "member count changed — the fixture moved, not the metric"
600        );
601        for (name, conditions, cyclomatic) in expected {
602            let member = child_space(container, name);
603            assert_eq!(
604                member.metrics.abc.conditions(),
605                *conditions,
606                "{name}: abc.conditions"
607            );
608            assert_eq!(
609                member.metrics.cyclomatic.cyclomatic(),
610                *cyclomatic,
611                "{name}: cyclomatic"
612            );
613        }
614    }
615
616    /// Regression for #227: a `Stats::default()` that never sees an
617    /// observation must not leak the `f64::MAX` sentinel for
618    /// `assignments_min`, `branches_min`, or `conditions_min`. All
619    /// three getters collapse the sentinel to `0.0` so JSON never
620    /// emits `1.7976931e308`.
621    #[test]
622    fn abc_empty_file_min_is_zero() {
623        let stats = Stats::default();
624        assert_eq!(stats.assignments_min(), 0);
625        assert_eq!(stats.branches_min(), 0);
626        assert_eq!(stats.conditions_min(), 0);
627    }
628
629    // The `EQ` arm of `java_count_token_assignment`: a plain `=` counts
630    // unless `java_eq_initializes_final_binding` finds it initialising a
631    // `final` binding, whether the declaration is a local or a field.
632    #[cfg(feature = "java")]
633    #[test]
634    fn java_eq_arm_counts_outside_final_declarations() {
635        check_metrics::<JavaParser>(
636            "class A { void m() { int x = 0; x = 1; x = 2; x = 3; } }",
637            "foo.java",
638            |metric| {
639                // `int x = 0;` is not `final`, so it counts like each
640                // `x = N;` that follows.
641                assert_eq!(metric.abc.assignments_sum(), 4);
642            },
643        );
644    }
645
646    #[cfg(feature = "java")]
647    #[test]
648    fn java_eq_arm_skips_final_initializers() {
649        check_metrics::<JavaParser>(
650            "class A {
651                final int X = 1;
652                @Deprecated private final int Y = 2;
653                void m() { final int Z = 3; }
654            }",
655            "foo.java",
656            |metric| {
657                // All three `=` tokens are `final` initializers — the
658                // second behind an annotation and an access modifier in
659                // the same `modifiers` node — so assignments are 0
660                // across all spaces.
661                assert_eq!(metric.abc.assignments_sum(), 0);
662            },
663        );
664    }
665
666    #[cfg(feature = "java")]
667    #[test]
668    fn java_final_initializer_does_not_suppress_the_assignments_inside_it() {
669        // The sentinel stack the structural predicate replaced stayed
670        // live from `final` to the next `;`, so every `=` nested in the
671        // initializer — a lambda body's, an array initializer's — was
672        // suppressed with the declarator's own. Java lambdas open no
673        // space, so nothing else separated them. Each row names the
674        // pre-fix value.
675        let cases = [
676            ("void m() { final Runnable r = () -> { x = 1; }; }", 1, 0),
677            (
678                "void m() { final Runnable r = () -> { x = 1; y = 2; }; }",
679                2,
680                1,
681            ),
682            ("void m() { final Runnable r = () -> x = 1; }", 1, 0),
683            ("void m() { final int[] a = { x = 1 }; }", 1, 0),
684            ("private final Runnable f = () -> { x = 1; };", 1, 0),
685            // The `;` that closed the leak is the reference: the same
686            // body after a `final` local counts as it always did.
687            ("void m() { final int q = 1; x = 1; }", 1, 1),
688        ];
689        let mut ran = 0;
690        for (body, expected, before) in cases {
691            let src = format!("class K {{ int x, y; {body} }}\n");
692            let assignments = metrics_verbatim(
693                LANG::Java,
694                src.as_bytes(),
695                MetricsOptions::default().with_only(&[crate::Metric::Abc]),
696            )
697            .abc
698            .assignments_sum();
699            assert_eq!(assignments, expected, "`{body}` (pre-fix {before})");
700            ran += 1;
701        }
702        assert_eq!(ran, cases.len());
703        assert!(cases.iter().any(|&(_, now, before)| now != before));
704    }
705
706    // Constant declarations are not counted as assignments
707    #[cfg(feature = "java")]
708    #[test]
709    fn java_constant_declarations() {
710        check_metrics::<JavaParser>(
711            "class A {
712                private final int X1 = 0, Y1 = 0;
713                public final float PI = 3.14f;
714                final static String HELLO = \"Hello,\";
715                protected String world = \" world!\";   // +1a
716                public float e = 2.718f;                // +1a
717                private int x2 = 1, y2 = 2;             // +2a
718
719                void m() {
720                    final int Z1 = 0, Z2 = 0, Z3 = 0;
721                    final float T = 0.0f;
722                    int z1 = 1, z2 = 2, z3 = 3;         // +3a
723                    float t = 60.0f;                    // +1a
724                }
725            }",
726            "foo.java",
727            |metric| {
728                // magnitude: sqrt(64 + 0 + 0) = sqrt(64)
729                // space count: 3 (1 unit, 1 class and 1 method)
730                insta::assert_json_snapshot!(
731                    metric.abc,
732                    @r#"
733                {
734                  "assignments": 8,
735                  "branches": 0,
736                  "conditions": 0,
737                  "magnitude": 8.0,
738                  "value": 0.0,
739                  "assignments_average": 2.6666666666666665,
740                  "branches_average": 0.0,
741                  "conditions_average": 0.0,
742                  "assignments_min": 0,
743                  "assignments_max": 4,
744                  "branches_min": 0,
745                  "branches_max": 0,
746                  "conditions_min": 0,
747                  "conditions_max": 0
748                }
749                "#
750                );
751            },
752        );
753    }
754
755    // "In computer science, conditionals (that is, conditional statements, conditional expressions
756    // and conditional constructs,) are programming language commands for handling decisions."
757    // Source: https://en.wikipedia.org/wiki/Conditional_(computer_programming)
758    // According to this definition, boolean expressions that are evaluated to make a decision are considered as conditions
759    // Variables, method invocations and true or false values used inside
760    // variable declarations and assignment expressions are not counted as conditions
761    #[cfg(feature = "java")]
762    #[test]
763    fn java_declarations_with_conditions() {
764        check_metrics::<JavaParser>(
765            "
766            boolean a = (1 > 2);            // +1a +1c
767            boolean b = 3 > 4;              // +1a +1c
768            boolean c = (1 > 2) && 3 > 4;   // +1a +2c
769            boolean d = b && (x > 5) || c;  // +1a +3c
770            boolean e = !d;                 // +1a +1c
771            boolean f = ((!false));         // +1a +1c
772            boolean g = !(!(true));         // +1a +1c
773            boolean h = true;               // +1a
774            boolean i = (false);            // +1a
775            boolean j = (((((true)))));     // +1a
776            boolean k = (((((m())))));      // +1a +1b
777            boolean l = (((((!m())))));     // +1a +1b +1c
778            boolean m = (!(!((m()))));      // +1a +1b +1c
779            List<String> n = null;          // +1a (< and > used for generic types are not counted as conditions)
780            ",
781            "foo.java",
782          |metric| {
783                // magnitude: sqrt(196 + 9 + 144) = sqrt(349)
784                // space count: 1 (1 unit)
785                insta::assert_json_snapshot!(
786                    metric.abc,
787                    @r#"
788                {
789                  "assignments": 14,
790                  "branches": 3,
791                  "conditions": 12,
792                  "magnitude": 18.681541692269406,
793                  "value": 18.681541692269406,
794                  "assignments_average": 14.0,
795                  "branches_average": 3.0,
796                  "conditions_average": 12.0,
797                  "assignments_min": 14,
798                  "assignments_max": 14,
799                  "branches_min": 3,
800                  "branches_max": 3,
801                  "conditions_min": 12,
802                  "conditions_max": 12
803                }
804                "#
805                );
806            },
807        );
808    }
809
810    // Conditions can be found in assignment expressions
811    #[cfg(feature = "java")]
812    #[test]
813    fn java_assignments_with_conditions() {
814        check_metrics::<JavaParser>(
815            "
816            a = 2 < 1;                  // +1a +1c
817            b = (4 >= 3) && 2 <= 1;     // +1a +2c
818            c = a || (x != 10) && b;    // +1a +3c
819            d = !false;                 // +1a +1c
820            e = (!false);               // +1a +1c
821            f = !(false);               // +1a +1c
822            g = (!(((true))));          // +1a +1c
823            h = ((true));               // +1a
824            i = !m();                   // +1a +1b +1c
825            j = !((m()));               // +1a +1b +1c
826            k = (!(m()));               // +1a +1b +1c
827            l = ((!(m())));             // +1a +1b +1c
828            m = !B.<Integer>m(2);       // +1a +1b +1c
829            n = !((B.<Integer>m(4)));   // +1a +1b +1c
830            ",
831            "foo.java",
832            |metric| {
833                // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
834                // space count: 1 (1 unit)
835                insta::assert_json_snapshot!(
836                    metric.abc,
837                    @r#"
838                {
839                  "assignments": 14,
840                  "branches": 6,
841                  "conditions": 16,
842                  "magnitude": 22.090722034374522,
843                  "value": 22.090722034374522,
844                  "assignments_average": 14.0,
845                  "branches_average": 6.0,
846                  "conditions_average": 16.0,
847                  "assignments_min": 14,
848                  "assignments_max": 14,
849                  "branches_min": 6,
850                  "branches_max": 6,
851                  "conditions_min": 16,
852                  "conditions_max": 16
853                }
854                "#
855                );
856            },
857        );
858    }
859
860    // Conditions can be found in method arguments
861    #[cfg(feature = "java")]
862    #[test]
863    fn java_methods_arguments_with_conditions() {
864        check_metrics::<JavaParser>(
865            "
866            m1(a);                                  // +1b
867            m2(a, b);                               // +1b
868            m3(true, (false), (((true))));          // +1b
869            m3(m1(false), m1(true), m1(false));     // +4b
870            m1(!a);                                 // +1b +1c
871            m2((((a))), (!b));                      // +1b +1c
872            m3(!(a), b, !!!c);                      // +1b +2c
873            m3(a, !b, m2(!a, !m2(!b, !m1(!c))));    // +4b +6c
874            ",
875            "foo.java",
876            |metric| {
877                // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
878                // space count: 1 (1 unit)
879                insta::assert_json_snapshot!(
880                    metric.abc,
881                    @r#"
882                {
883                  "assignments": 0,
884                  "branches": 14,
885                  "conditions": 10,
886                  "magnitude": 17.204650534085253,
887                  "value": 17.204650534085253,
888                  "assignments_average": 0.0,
889                  "branches_average": 14.0,
890                  "conditions_average": 10.0,
891                  "assignments_min": 0,
892                  "assignments_max": 0,
893                  "branches_min": 14,
894                  "branches_max": 14,
895                  "conditions_min": 10,
896                  "conditions_max": 10
897                }
898                "#
899                );
900            },
901        );
902    }
903
904    // "A unary conditional expression is an implicit condition that uses no relational operators."
905    // Source: Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report.
906    // https://www.softwarerenovation.com/Articles.aspx (page 5)
907    #[cfg(feature = "java")]
908    #[test]
909    fn java_if_single_conditions() {
910        check_metrics::<JavaParser>(
911            "
912            if ( a < 0 ) {}             // +1c
913            if ( ((a != 0)) ) {}        // +1c
914            if ( !(a > 0) ) {}          // +1c
915            if ( !(((a == 0))) ) {}     // +1c
916            if ( b.m1() ) {}            // +1b +1c
917            if ( !b.m1() ) {}           // +1b +1c
918            if ( !!b.m2() ) {}          // +1b +1c
919            if ( (!(b.m1())) ) {}       // +1b +1c
920            if ( (!(!b.m1())) ) {}      // +1b +1c
921            if ( ((b.m2())) ) {}        // +1b +1c
922            if ( ((b.m().m1())) ) {}    // +2b +1c
923            if ( c ) {}                 // +1c
924            if ( !c ) {}                // +1c
925            if ( !!!!!!!!!!c ) {}       // +1c
926            if ( (((c))) ) {}           // +1c
927            if ( (((!c))) ) {}          // +1c
928            if ( ((!(c))) ) {}          // +1c
929            if ( true ) {}              // +1c
930            if ( !true ) {}             // +1c
931            if ( ((false)) ) {}         // +1c
932            if ( !(!(false)) ) {}       // +1c
933            if ( !!!false ) {}          // +1c
934            ",
935            "foo.java",
936            |metric| {
937                // magnitude: sqrt(0 + 64 + 484) = sqrt(548)
938                // space count: 1 (1 unit)
939                insta::assert_json_snapshot!(
940                    metric.abc,
941                    @r#"
942                {
943                  "assignments": 0,
944                  "branches": 8,
945                  "conditions": 22,
946                  "magnitude": 23.40939982143925,
947                  "value": 23.40939982143925,
948                  "assignments_average": 0.0,
949                  "branches_average": 8.0,
950                  "conditions_average": 22.0,
951                  "assignments_min": 0,
952                  "assignments_max": 0,
953                  "branches_min": 8,
954                  "branches_max": 8,
955                  "conditions_min": 22,
956                  "conditions_max": 22
957                }
958                "#
959                );
960            },
961        );
962    }
963
964    #[cfg(feature = "java")]
965    #[test]
966    fn java_if_multiple_conditions() {
967        check_metrics::<JavaParser>(
968            "
969            if ( a || b || c || d ) {}              // +4c
970            if ( a || b && c && d ) {}              // +4c
971            if ( x < y && a == b ) {}               // +2c
972            if ( ((z < (x + y))) ) {}               // +1c
973            if ( a || ((((b))) && c) ) {}           // +3c
974            if ( a && ((((a == b))) && c) ) {}      // +3c
975            if ( a || ((((a == b))) || ((c))) ) {}  // +3c
976            if ( x < y && B.m() ) {}                // +1b +2c
977            if ( x < y && !(((B.m()))) ) {}         // +1b +2c
978            if ( !(x < y) && !B.m() ) {}            // +1b +2c
979            if ( !!!(!!!(a)) && B.m() ||            // +1b +2c
980                 !B.m() && (((x > 4))) ) {}         // +1b +2c
981            ",
982            "foo.java",
983            |metric| {
984                // magnitude: sqrt(0 + 25 + 900) = sqrt(925)
985                // space count: 1 (1 unit)
986                insta::assert_json_snapshot!(
987                    metric.abc,
988                    @r#"
989                {
990                  "assignments": 0,
991                  "branches": 5,
992                  "conditions": 30,
993                  "magnitude": 30.4138126514911,
994                  "value": 30.4138126514911,
995                  "assignments_average": 0.0,
996                  "branches_average": 5.0,
997                  "conditions_average": 30.0,
998                  "assignments_min": 0,
999                  "assignments_max": 0,
1000                  "branches_min": 5,
1001                  "branches_max": 5,
1002                  "conditions_min": 30,
1003                  "conditions_max": 30
1004                }
1005                "#
1006                );
1007            },
1008        );
1009    }
1010
1011    #[cfg(feature = "java")]
1012    #[test]
1013    fn java_while_and_do_while_conditions() {
1014        check_metrics::<JavaParser>(
1015            "
1016            while ( (!(!(!(a)))) ) {}                   // +1c
1017            while ( b || 1 > 2 ) {}                     // +2c
1018            while ( x.m() && (((c))) ) {}               // +1b +2c
1019            do {} while ( !!!(((!!!a))) );              // +1c
1020            do {} while ( a || (b && c) );              // +3c
1021            do {} while ( !x.m() && 1 > 2 || !true );   // +1b +3c
1022            ",
1023            "foo.java",
1024            |metric| {
1025                // magnitude: sqrt(0 + 4 + 144) = sqrt(148)
1026                // space count: 1 (1 unit)
1027                insta::assert_json_snapshot!(
1028                    metric.abc,
1029                    @r#"
1030                {
1031                  "assignments": 0,
1032                  "branches": 2,
1033                  "conditions": 12,
1034                  "magnitude": 12.165525060596439,
1035                  "value": 12.165525060596439,
1036                  "assignments_average": 0.0,
1037                  "branches_average": 2.0,
1038                  "conditions_average": 12.0,
1039                  "assignments_min": 0,
1040                  "assignments_max": 0,
1041                  "branches_min": 2,
1042                  "branches_max": 2,
1043                  "conditions_min": 12,
1044                  "conditions_max": 12
1045                }
1046                "#
1047                );
1048            },
1049        );
1050    }
1051
1052    // GMetrics, a Groovy source code analyzer, provides the following definition of unary conditional expression:
1053    // "These are cases where a single variable/field/value is treated as a boolean value.
1054    // Examples include `if (x)` and `return !ready`."
1055    // According to this definition, unary conditional expressions are counted also in function return values.
1056    // Source: https://dx42.github.io/gmetrics/metrics/AbcMetric.html
1057    // Examples: https://github.com/dx42/gmetrics/blob/master/src/test/groovy/org/gmetrics/metric/abc/AbcMetric_MethodTest.groovy
1058    #[cfg(feature = "java")]
1059    #[test]
1060    fn java_return_with_conditions() {
1061        check_metrics::<JavaParser>(
1062            "class A {
1063                boolean m1() {
1064                    return !(z >= 0);       // +1c
1065                }
1066                boolean m2() {
1067                    return (((!x)));        // +1c
1068                }
1069                boolean m3() {
1070                    return x && y;          // +2c
1071                }
1072                boolean m4() {
1073                    return y || (z < 0);    // +2c
1074                }
1075                boolean m5() {
1076                    return x || y ?         // +3c (two unary conditions and one ?)
1077                        true : false;
1078                }
1079            }",
1080            "foo.java",
1081            |metric| {
1082                // magnitude: sqrt(0 + 0 + 81) = sqrt(81)
1083                // space count: 7 (1 unit, 1 class and 5 methods)
1084                insta::assert_json_snapshot!(
1085                    metric.abc,
1086                    @r#"
1087                {
1088                  "assignments": 0,
1089                  "branches": 0,
1090                  "conditions": 9,
1091                  "magnitude": 9.0,
1092                  "value": 0.0,
1093                  "assignments_average": 0.0,
1094                  "branches_average": 0.0,
1095                  "conditions_average": 1.2857142857142858,
1096                  "assignments_min": 0,
1097                  "assignments_max": 0,
1098                  "branches_min": 0,
1099                  "branches_max": 0,
1100                  "conditions_min": 0,
1101                  "conditions_max": 3
1102                }
1103                "#
1104                );
1105            },
1106        );
1107    }
1108
1109    // Variables, method invocations, and true or false values
1110    // inside return statements are not counted as conditions
1111    #[cfg(feature = "java")]
1112    #[test]
1113    fn java_return_without_conditions() {
1114        check_metrics::<JavaParser>(
1115            "class A {
1116                boolean m1() {
1117                    return x;
1118                }
1119                boolean m2() {
1120                    return (x);
1121                }
1122                boolean m3() {
1123                    return y.m();   // +1b
1124                }
1125                boolean m4() {
1126                    return false;
1127                }
1128                void m5() {
1129                    return;
1130                }
1131            }",
1132            "foo.java",
1133            |metric| {
1134                // magnitude: sqrt(0 + 1 + 0) = sqrt(1)
1135                // space count: 7 (1 unit, 1 class and 5 methods)
1136                insta::assert_json_snapshot!(
1137                    metric.abc,
1138                    @r#"
1139                {
1140                  "assignments": 0,
1141                  "branches": 1,
1142                  "conditions": 0,
1143                  "magnitude": 1.0,
1144                  "value": 0.0,
1145                  "assignments_average": 0.0,
1146                  "branches_average": 0.14285714285714285,
1147                  "conditions_average": 0.0,
1148                  "assignments_min": 0,
1149                  "assignments_max": 0,
1150                  "branches_min": 0,
1151                  "branches_max": 1,
1152                  "conditions_min": 0,
1153                  "conditions_max": 0
1154                }
1155                "#
1156                );
1157            },
1158        );
1159    }
1160
1161    // Variables, method invocations, and true or false values
1162    // in lambda expression return values are not counted as conditions
1163    #[cfg(feature = "java")]
1164    #[test]
1165    fn java_lambda_expressions_return_with_conditions() {
1166        check_metrics::<JavaParser>(
1167            "
1168            Predicate<Boolean> p1 = a -> a;                         // +1a
1169            Predicate<Boolean> p2 = b -> true;                      // +1a
1170            Predicate<Boolean> p3 = c -> m();                       // +1a
1171            Predicate<Integer> p4 = d -> d > 10;                    // +1a +1c
1172            Predicate<Boolean> p5 = (e) -> !e;                      // +1a +1c
1173            Predicate<Boolean> p6 = (f) -> !((!f));                 // +1a +1c
1174            Predicate<Boolean> p7 = (g) -> !g && true;              // +1a +2c
1175            BiPredicate<Boolean, Boolean> bp1 = (h, i) -> !h && !i; // +1a +2c
1176            BiPredicate<Boolean, Boolean> bp2 = (j, k) -> {
1177                return j || k;                                      // +1a +2c
1178            };
1179            ",
1180            "foo.java",
1181            |metric| {
1182                // magnitude: sqrt(81 + 1 + 81) = sqrt(163)
1183                // space count: 1 (1 unit)
1184                insta::assert_json_snapshot!(
1185                    metric.abc,
1186                    @r#"
1187                {
1188                  "assignments": 9,
1189                  "branches": 1,
1190                  "conditions": 9,
1191                  "magnitude": 12.767145334803704,
1192                  "value": 12.767145334803704,
1193                  "assignments_average": 9.0,
1194                  "branches_average": 1.0,
1195                  "conditions_average": 9.0,
1196                  "assignments_min": 9,
1197                  "assignments_max": 9,
1198                  "branches_min": 1,
1199                  "branches_max": 1,
1200                  "conditions_min": 9,
1201                  "conditions_max": 9
1202                }
1203                "#
1204                );
1205            },
1206        );
1207    }
1208
1209    #[cfg(feature = "java")]
1210    #[test]
1211    fn java_for_with_variable_declaration() {
1212        check_metrics::<JavaParser>(
1213            "
1214            for ( int i1 = 0; !(!(!(!a))); i1++ ) {}                // +2a +1c
1215            for ( int i2 = 0; !B.m(); i2++ ) {}                     // +2a +1b +1c
1216            for ( int i3 = 0; a || false; i3++ ) {}                 // +2a +2c
1217            for ( int i4 = 0; a && B.m() ? true : false; i4++ ) {}  // +2a +1b +3c
1218            for ( int i5 = 0; true; i5++ ) {}                       // +2a +1c
1219            ",
1220            "foo.java",
1221            |metric| {
1222                // magnitude: sqrt(100 + 4 + 64) = sqrt(168)
1223                // space count: 1 (1 unit)
1224                insta::assert_json_snapshot!(
1225                    metric.abc,
1226                    @r#"
1227                {
1228                  "assignments": 10,
1229                  "branches": 2,
1230                  "conditions": 8,
1231                  "magnitude": 12.96148139681572,
1232                  "value": 12.96148139681572,
1233                  "assignments_average": 10.0,
1234                  "branches_average": 2.0,
1235                  "conditions_average": 8.0,
1236                  "assignments_min": 10,
1237                  "assignments_max": 10,
1238                  "branches_min": 2,
1239                  "branches_max": 2,
1240                  "conditions_min": 8,
1241                  "conditions_max": 8
1242                }
1243                "#
1244                );
1245            },
1246        );
1247    }
1248
1249    #[cfg(feature = "java")]
1250    #[test]
1251    fn java_for_without_variable_declaration() {
1252        check_metrics::<JavaParser>(
1253            "class A{
1254                void m1() {
1255                    for (i = 0; x < y; i++) {}          // +2a +1c
1256                    for (i = 0; ((x < y)); i++) {}      // +2a +1c
1257                    for (i = 0; !(!(x < y)); i++) {}    // +2a +1c
1258                    for (i = 0; true; i++) {}           // +2a +1c
1259                }
1260                void m2() {
1261                    for ( ; true; ) {}  // +1c
1262                }
1263                void m3() {
1264                    for ( ; ; ) {}      // +0c — no condition to count (#1276)
1265                }
1266            }",
1267            "foo.java",
1268            |metric| {
1269                // magnitude: sqrt(64 + 0 + 25) = sqrt(89)
1270                // space count: 5 (1 unit, 1 class and 3 methods)
1271                insta::assert_json_snapshot!(
1272                    metric.abc,
1273                    @r#"
1274                {
1275                  "assignments": 8,
1276                  "branches": 0,
1277                  "conditions": 5,
1278                  "magnitude": 9.433981132056603,
1279                  "value": 0.0,
1280                  "assignments_average": 1.6,
1281                  "branches_average": 0.0,
1282                  "conditions_average": 1.0,
1283                  "assignments_min": 0,
1284                  "assignments_max": 8,
1285                  "branches_min": 0,
1286                  "branches_max": 0,
1287                  "conditions_min": 0,
1288                  "conditions_max": 4
1289                }
1290                "#
1291                );
1292            },
1293        );
1294    }
1295
1296    // Issue #1276 changed Java's answer here. `java_walk_for_statement`
1297    // used to read child(3), fall through to child(4) when that was the
1298    // `;` an expression initializer leaves behind, and count a `;` or
1299    // `)` landing there as a vacuously-true condition — so `for (;;)`
1300    // scored one. Nothing in Fitzpatrick's C dimension attributes a
1301    // condition to a `for` keyword; it counts conditional operators and
1302    // unary conditions that are present. Java and Groovy were the only
1303    // two impls disagreeing, and the field-addressed walker now reports
1304    // zero the way the C family, the JS family, PHP, C# and Go all do.
1305    #[cfg(feature = "java")]
1306    #[test]
1307    fn java_empty_for_condition_counts_nothing() {
1308        check_metrics::<JavaParser>(
1309            "class A { void m() { for (;;) { break; } } }",
1310            "foo.java",
1311            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
1312        );
1313        // The other empty spelling, and the one the old cascade
1314        // actually mis-scored: with an **expression** initializer the
1315        // children are `for ( init ; ; update )`, so child(3) was the
1316        // separating `;` and child(4) the empty condition's `;`, which
1317        // the vacuous-true arm counted. The `for (int i = 0; ; i++)`
1318        // spelling scored 0 both before and after — Java's
1319        // `local_variable_declaration` swallows its own `;`, putting the
1320        // update expression at child(4), where it matched no arm — so it
1321        // would be a vacuous regression test and is deliberately not
1322        // used here.
1323        check_metrics::<JavaParser>(
1324            "class A { void m() { int i; for (i = 0; ; i++) { break; } } }",
1325            "foo.java",
1326            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
1327        );
1328    }
1329
1330    // The second defect the positional cascade carried: tree-sitter
1331    // counts comments among a node's children, so a comment anywhere in
1332    // the header shifted every index and the condition went unread —
1333    // the same failure #1181 removed from `java_walk_ternary`. Reading
1334    // the `condition` field cannot shift.
1335    #[cfg(feature = "java")]
1336    #[test]
1337    fn java_for_condition_survives_a_header_comment() {
1338        check_metrics::<JavaParser>(
1339            "class A { void m(boolean a) { for (; /* n */ a; ) { break; } } }",
1340            "foo.java",
1341            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
1342        );
1343        // Unchanged control: the same loop without the comment. Both
1344        // spellings must agree, which is the property the positional
1345        // form broke.
1346        check_metrics::<JavaParser>(
1347            "class A { void m(boolean a) { for (; a; ) { break; } } }",
1348            "foo.java",
1349            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
1350        );
1351    }
1352
1353    // Variables, method invocations, and true or false values
1354    // in ternary expression return values are not counted as conditions
1355    #[cfg(feature = "java")]
1356    #[test]
1357    fn java_ternary_conditions() {
1358        check_metrics::<JavaParser>(
1359            "
1360            a = true;                                   // +1a
1361            b = a ? true : false;                       // +1a +2c
1362            c = ((((a)))) ? !false : !b;                // +1a +4c
1363            d = !this.m() ? !!a : (false);              // +1a +1b +3c
1364            e = !(a) && b ? ((c)) : !d;                 // +1a +4c
1365            if ( this.m() ? a : !this.m() ) {}          // +2b +3c
1366            if ( x > 0 ? !(false) : this.m() ) {}       // +1b +3c
1367            if ( x > 0 && x != 3 ? !(a) : (!(b)) ) {}   // +5c
1368            ",
1369            "foo.java",
1370            |metric| {
1371                // magnitude: sqrt(25 + 16 + 576) = sqrt(617)
1372                //  space count: 1 (1 unit)
1373                insta::assert_json_snapshot!(
1374                    metric.abc,
1375                    @r#"
1376                {
1377                  "assignments": 5,
1378                  "branches": 4,
1379                  "conditions": 24,
1380                  "magnitude": 24.839484696748443,
1381                  "value": 24.839484696748443,
1382                  "assignments_average": 5.0,
1383                  "branches_average": 4.0,
1384                  "conditions_average": 24.0,
1385                  "assignments_min": 5,
1386                  "assignments_max": 5,
1387                  "branches_min": 4,
1388                  "branches_max": 4,
1389                  "conditions_min": 24,
1390                  "conditions_max": 24
1391                }
1392                "#
1393                );
1394            },
1395        );
1396    }
1397
1398    #[cfg(feature = "bash")]
1399    #[test]
1400    fn bash_assignments_only() {
1401        check_metrics::<BashParser>(
1402            "f() {
1403                 a=1
1404                 b=2
1405                 c+=3
1406             }",
1407            "foo.sh",
1408            |metric| {
1409                insta::assert_json_snapshot!(
1410                    metric.abc,
1411                    @r#"
1412                {
1413                  "assignments": 3,
1414                  "branches": 0,
1415                  "conditions": 0,
1416                  "magnitude": 3.0,
1417                  "value": 0.0,
1418                  "assignments_average": 1.5,
1419                  "branches_average": 0.0,
1420                  "conditions_average": 0.0,
1421                  "assignments_min": 0,
1422                  "assignments_max": 3,
1423                  "branches_min": 0,
1424                  "branches_max": 0,
1425                  "conditions_min": 0,
1426                  "conditions_max": 0
1427                }
1428                "#
1429                );
1430            },
1431        );
1432    }
1433
1434    #[cfg(feature = "bash")]
1435    #[test]
1436    fn bash_commands_only() {
1437        check_metrics::<BashParser>(
1438            "f() {
1439                 echo a
1440                 ls
1441             }",
1442            "foo.sh",
1443            |metric| {
1444                insta::assert_json_snapshot!(
1445                    metric.abc,
1446                    @r#"
1447                {
1448                  "assignments": 0,
1449                  "branches": 2,
1450                  "conditions": 0,
1451                  "magnitude": 2.0,
1452                  "value": 0.0,
1453                  "assignments_average": 0.0,
1454                  "branches_average": 1.0,
1455                  "conditions_average": 0.0,
1456                  "assignments_min": 0,
1457                  "assignments_max": 0,
1458                  "branches_min": 0,
1459                  "branches_max": 2,
1460                  "conditions_min": 0,
1461                  "conditions_max": 0
1462                }
1463                "#
1464                );
1465            },
1466        );
1467    }
1468
1469    #[cfg(feature = "bash")]
1470    #[test]
1471    fn bash_control_flow_counts_conditions() {
1472        // Regression for #696: Bash control-flow branches are ABC
1473        // conditions (a Bash predicate is a command, so the branch keyword
1474        // is the only condition signal). Each mirrors a cyclomatic decision.
1475        //
1476        // expected: 4 conditions — `if` (1) + `elif` (1) + `while` (1) +
1477        // the non-wildcard case arm `a)` (1). The bare-`*)` wildcard arm is
1478        // the Bash analogue of `default:` and is excluded, exactly as the
1479        // cyclomatic standard count excludes it. No comparison / test
1480        // operators appear, so every condition here is control-flow.
1481        check_metrics::<BashParser>(
1482            "f() {
1483                 if cmd; then
1484                     echo a
1485                 elif other; then
1486                     echo b
1487                 fi
1488                 while running; do
1489                     echo c
1490                 done
1491                 case \"$x\" in
1492                     a) echo d ;;
1493                     *) echo e ;;
1494                 esac
1495             }",
1496            "foo.sh",
1497            |metric| {
1498                assert_eq!(metric.abc.conditions_sum(), 4);
1499            },
1500        );
1501    }
1502
1503    #[cfg(feature = "bash")]
1504    #[test]
1505    fn bash_conditions_mix() {
1506        // Exercises every condition path: `==` and `!=` inside `[[ ]]`,
1507        // arithmetic `<` inside `(( ))`, and the prefix `-z` test operator
1508        // inside `[ ]`. Each `if` body's `echo` contributes a branch.
1509        //
1510        // expected: 8 conditions — each of the four `if`s contributes one
1511        // for the control-flow branch (#696) plus one for its comparison /
1512        // test operator (`==`, `!=`, `<`, `-z`). 4 branches (one `echo`
1513        // each). magnitude = sqrt(4² + 8²) = sqrt(80).
1514        check_metrics::<BashParser>(
1515            "f() {
1516                 if [[ \"$a\" == \"$b\" ]]; then
1517                     echo eq
1518                 fi
1519                 if [[ \"$x\" != \"$y\" ]]; then
1520                     echo ne
1521                 fi
1522                 if (( $a < $b )); then
1523                     echo lt
1524                 fi
1525                 if [ -z \"$x\" ]; then
1526                     echo empty
1527                 fi
1528             }",
1529            "foo.sh",
1530            |metric| {
1531                assert_eq!(metric.abc.conditions_sum(), 8);
1532                assert_eq!(metric.abc.branches_sum(), 4);
1533                insta::assert_json_snapshot!(
1534                    metric.abc,
1535                    @r#"
1536                {
1537                  "assignments": 0,
1538                  "branches": 4,
1539                  "conditions": 8,
1540                  "magnitude": 8.94427190999916,
1541                  "value": 0.0,
1542                  "assignments_average": 0.0,
1543                  "branches_average": 2.0,
1544                  "conditions_average": 4.0,
1545                  "assignments_min": 0,
1546                  "assignments_max": 0,
1547                  "branches_min": 0,
1548                  "branches_max": 4,
1549                  "conditions_min": 0,
1550                  "conditions_max": 8
1551                }
1552                "#
1553                );
1554            },
1555        );
1556    }
1557
1558    #[cfg(feature = "bash")]
1559    #[test]
1560    fn bash_redirection_is_not_a_condition() {
1561        // `>` and `<` spell an I/O redirection as well as a comparison, and
1562        // the grammar parents the redirection under `file_redirect` rather
1563        // than `binary_expression`. Ungated, every redirect in a script
1564        // scored a condition: this fixture measured 2 before the parent
1565        // gate — the Bash instance of #1280's positive-parent polarity.
1566        // expected: 0 conditions — no test, no `if`, no comparison.
1567        check_metrics::<BashParser>(
1568            "f() {\n  echo hi > out.txt\n  read x < in.txt\n}\n",
1569            "foo.sh",
1570            |metric| {
1571                assert_eq!(metric.abc.conditions_sum(), 0);
1572                // The two `echo` / `read` commands still count as branches,
1573                // so the zero above is the gate firing rather than the walk
1574                // skipping the function body.
1575                assert_eq!(metric.abc.branches_sum(), 2);
1576            },
1577        );
1578    }
1579
1580    #[cfg(feature = "bash")]
1581    #[test]
1582    fn bash_comparison_inside_an_arithmetic_or_test_context_is_a_condition() {
1583        // The positive control for the gate above: the same tokens under a
1584        // `binary_expression` are real comparisons in both the `[[ … ]]`
1585        // test form and the `(( … ))` arithmetic form, and still count.
1586        // expected: 4 — one `if` control-flow condition and one `>` per
1587        // function.
1588        check_metrics::<BashParser>(
1589            "f() {\n  if [[ $a > $b ]]; then :; fi\n}\ng() {\n  if (( a > b )); then :; fi\n}\n",
1590            "foo.sh",
1591            |metric| {
1592                assert_eq!(metric.abc.conditions_sum(), 4);
1593            },
1594        );
1595    }
1596
1597    #[cfg(feature = "bash")]
1598    #[test]
1599    fn bash_arithmetic_ternary_is_a_condition() {
1600        // The ABC half of #1268. Cyclomatic and cognitive both count Bash's
1601        // only ternary form; ABC did not, so the identical construct scored
1602        // 1 here against the C family's 2.
1603        // expected: 2 — the `>` comparison and the ternary itself, matching
1604        // `int m = a > b ? a : b;` in C.
1605        check_metrics::<BashParser>(
1606            "f() {\n  local m=$(( a > b ? a : b ))\n}\n",
1607            "foo.sh",
1608            |metric| {
1609                assert_eq!(metric.abc.conditions_sum(), 2);
1610            },
1611        );
1612    }
1613
1614    #[cfg(feature = "bash")]
1615    #[test]
1616    fn bash_magnitude() {
1617        // Combined assignments + branches + conditions. The single `if`
1618        // contributes two conditions (the control-flow branch, #696, plus
1619        // the `==` operator), so magnitude = sqrt(2² + 1² + 2²) = sqrt(9).
1620        check_metrics::<BashParser>(
1621            "f() {
1622                 a=1
1623                 b=2
1624                 if [[ \"$a\" == \"$b\" ]]; then
1625                     echo eq
1626                 fi
1627             }",
1628            "foo.sh",
1629            |metric| {
1630                assert_eq!(metric.abc.conditions_sum(), 2);
1631                insta::assert_json_snapshot!(
1632                    metric.abc,
1633                    @r#"
1634                {
1635                  "assignments": 2,
1636                  "branches": 1,
1637                  "conditions": 2,
1638                  "magnitude": 3.0,
1639                  "value": 0.0,
1640                  "assignments_average": 1.0,
1641                  "branches_average": 0.5,
1642                  "conditions_average": 1.0,
1643                  "assignments_min": 0,
1644                  "assignments_max": 2,
1645                  "branches_min": 0,
1646                  "branches_max": 1,
1647                  "conditions_min": 0,
1648                  "conditions_max": 2
1649                }
1650                "#
1651                );
1652            },
1653        );
1654    }
1655
1656    #[cfg(feature = "java")]
1657    #[test]
1658    fn java_malformed_parenthesized_no_panic() {
1659        check_metrics::<JavaParser>("class A { void m() { if (( }) }", "foo.java", |metric| {
1660            // tree-sitter emits ERROR nodes for this malformed source, so no
1661            // IfStatement, branch, or condition is recognised — all counts are 0.
1662            // Primary goal: the unwrap-free path does not panic.
1663            assert_eq!(metric.abc.assignments(), 0);
1664            assert_eq!(metric.abc.branches(), 0);
1665            assert_eq!(metric.abc.conditions(), 0);
1666            assert_eq!(metric.abc.magnitude(), 0.0);
1667        });
1668    }
1669
1670    #[cfg(feature = "java")]
1671    #[test]
1672    fn java_bool_returning_terminal_kinds_count() {
1673        // Companion to `csharp_bool_returning_terminal_kinds_count`
1674        // (issue #372 / lesson #19). Java's grammar wraps every
1675        // if/while/do condition in `parenthesized_expression`, so
1676        // the gap lived in `java_inspect_container`'s terminal-arm
1677        // recognizer: `FieldAccess` (`cfg.flag`), `CastExpression`
1678        // (`(boolean)v`), `ArrayAccess` (`flags[0]`), and
1679        // `InstanceofExpression` (`x instanceof Foo`) were never
1680        // counted. Java has no `await` or `is_pattern` analogues,
1681        // so the C# fix's five-kind set collapses to four here.
1682        //
1683        // `instanceof` has since moved out of the terminal set to an
1684        // unconditional arm (#1461), so it no longer depends on the
1685        // slot to be counted. The total is unchanged, which is the
1686        // point of that change rather than an accident of it.
1687        //
1688        // expected: 4 conditions (one per `if`), 0 assignments,
1689        // 0 branches (no invocations).
1690        check_metrics::<JavaParser>(
1691            "class Cfg { boolean flag; }
1692            class A {
1693                void m(Object v, boolean[] flags, Cfg cfg) {
1694                    if (cfg.flag) { }
1695                    if ((boolean) v) { }
1696                    if (v instanceof Cfg) { }
1697                    if (flags[0]) { }
1698                }
1699            }",
1700            "foo.java",
1701            |metric| {
1702                assert_eq!(metric.abc.conditions_sum(), 4);
1703                assert_eq!(metric.abc.assignments_sum(), 0);
1704                assert_eq!(metric.abc.branches_sum(), 0);
1705            },
1706        );
1707    }
1708
1709    // Issue #1274: a generic *declaration* is type syntax, not a
1710    // decision. `java_count_token_condition` denied only
1711    // `type_arguments`, so `class Gen<T>` and `<U> U ident(U x)` — both
1712    // `type_parameters` — each scored two conditions. The fix counts
1713    // `<` / `>` only under `binary_expression`, the polarity C / C++ /
1714    // Rust / Go already use; a `grammar.json` sweep proves the token is
1715    // emitted from exactly three productions, so the two shapes agree on
1716    // every well-formed input.
1717    //
1718    // One fixture per production the arm must ignore: the class-level
1719    // `type_parameters` (`Gen<T …>`), its `extends` bound's nested
1720    // `type_arguments` (`Comparable<T>`), the method-level
1721    // `type_parameters` (`<U>`), and a nested generic
1722    // (`Map<String, List<T>>`) whose two closing `>` lex as separate
1723    // tokens under separate `type_arguments`. Pre-fix this file scored
1724    // 4 conditions — two per `type_parameters` bracket pair; expected 0,
1725    // the file contains no conditional construct at all.
1726    #[cfg(feature = "java")]
1727    #[test]
1728    fn java_generic_declarations_are_not_conditions() {
1729        check_metrics::<JavaParser>(
1730            "class Gen<T extends Comparable<T>> {
1731                <U> U ident(U x) { return x; }
1732                List<T> pick(Map<String, List<T>> m) { return m.get(\"k\"); }
1733            }",
1734            "foo.java",
1735            |metric| {
1736                assert_eq!(metric.abc.conditions_sum(), 0);
1737                // Non-vacuity guard: 0 is also what an unparsed file
1738                // scores, so pin a value only a walked body can produce
1739                // — the `m.get("k")` invocation.
1740                assert_eq!(metric.abc.branches_sum(), 1);
1741            },
1742        );
1743    }
1744
1745    // Second half of #1274, same premise and same match: a wildcard type
1746    // argument's `?` is type syntax, not a ternary. tree-sitter-java emits
1747    // a bare `?` from exactly two productions — `ternary_expression` and
1748    // `wildcard` — so the `QMARK` arm carries the same allowlist gate the
1749    // `<` / `>` arm does.
1750    //
1751    // The body carries a real ternary over a real comparison so the
1752    // expected total is 2, not 0. Asserting 0 on a wildcard-only fixture
1753    // would have been vacuous — an unparsable file scores 0 too, so the
1754    // test could not tell "the wildcard was ignored" from "the fixture
1755    // stopped being recognised". Each failure mode now lands on its own
1756    // number: 4 if the wildcard `?` counts again (the pre-fix value), 3
1757    // if the gate is aimed at `Wildcard` instead, 1 if it swallows the
1758    // genuine ternary, 0 if parsing breaks. Two wildcards against one
1759    // ternary is deliberate — with one of each, aiming the gate at the
1760    // wrong one of the two productions still totals 2 and the test
1761    // cannot see the difference.
1762    #[cfg(feature = "java")]
1763    #[test]
1764    fn java_generic_wildcard_is_not_a_condition() {
1765        check_metrics::<JavaParser>(
1766            "class A {
1767                int m(List<? extends Number> xs, Map<String, ? extends Number> ys,
1768                      int a, int b) { return a < b ? 1 : 2; }
1769            }",
1770            "foo.java",
1771            |metric| {
1772                assert_eq!(metric.abc.conditions_sum(), 2);
1773            },
1774        );
1775    }
1776
1777    // The other half of #1274: narrowing the `<` / `>` arm must not
1778    // swallow real comparisons. `List<String>` in the signature keeps a
1779    // generic in scope so the fixture proves both directions at once,
1780    // and the assertion is the grammar-dispatch §8 pin — on the
1781    // method's own space the ABC condition count must equal the
1782    // cyclomatic decision count (`cyclomatic()` minus the per-space
1783    // base of 1). Both are 2, one per `if`.
1784    #[cfg(feature = "java")]
1785    #[test]
1786    fn java_comparison_operators_still_count_alongside_generics() {
1787        check_func_space::<JavaParser, _>(
1788            "class A {
1789                int m(List<String> xs, int a, int b) {
1790                    if (a < b) { return 1; }
1791                    if (a > b) { return 2; }
1792                    return 0;
1793                }
1794            }",
1795            "foo.java",
1796            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
1797        );
1798    }
1799
1800    #[cfg(feature = "java")]
1801    #[test]
1802    fn java_constructor_delegation_is_a_branch() {
1803        // Regression for #1279: `super(…)` / `this(…)` parse as
1804        // `explicit_constructor_invocation`, not `method_invocation`, so
1805        // each scored zero branches while Groovy scored one for identical
1806        // source. Both constructors delegate, and neither body contains any
1807        // other call.
1808        // expected: 2 branches — one delegation each.
1809        check_metrics::<JavaParser>(
1810            "class Sub extends Base {
1811                Sub() { super(1); }
1812                Sub(int a) { this(); }
1813            }",
1814            "foo.java",
1815            |metric| {
1816                assert_eq!(metric.abc.branches_sum(), 2);
1817            },
1818        );
1819    }
1820
1821    #[cfg(feature = "java")]
1822    #[test]
1823    fn java_constructor_delegation_does_not_double_count_arguments() {
1824        // The delegation node does not wrap a `method_invocation` for the
1825        // call itself, so `super(f())` is exactly two branches — the
1826        // delegation and the argument call — not three (#1279).
1827        // expected: 2 branches.
1828        check_metrics::<JavaParser>(
1829            "class Sub extends Base {
1830                Sub() { super(f()); }
1831            }",
1832            "foo.java",
1833            |metric| {
1834                assert_eq!(metric.abc.branches_sum(), 2);
1835            },
1836        );
1837    }
1838
1839    #[cfg(feature = "csharp")]
1840    #[test]
1841    fn csharp_constructor_initializer_is_a_branch() {
1842        // C# spells the same delegation as a `constructor_initializer`
1843        // (`: base(…)` / `: this(…)`), which likewise scored zero (#1279).
1844        // expected: 2 branches — one initializer each, no other calls.
1845        check_metrics::<CsharpParser>(
1846            "class Sub : Base {
1847                Sub() : base(1) { }
1848                Sub(int a) : this() { }
1849            }",
1850            "foo.cs",
1851            |metric| {
1852                assert_eq!(metric.abc.branches_sum(), 2);
1853            },
1854        );
1855    }
1856
1857    #[cfg(feature = "csharp")]
1858    #[test]
1859    fn csharp_primary_constructor_base_call_is_a_branch() {
1860        // The C# 12 primary-constructor superclass call invokes the base
1861        // constructor exactly as the `: base(…)` above it does, and scored
1862        // zero until #1406 — the C# sibling of Kotlin's #1384.
1863        //
1864        // tree-sitter-c-sharp 0.23.5 spells the `record` and `class`
1865        // families differently, so per `.claude/rules/grammar-dispatch.md`
1866        // §11 they are two independent paths and each needs its own
1867        // fixture: the record form nests the arguments under a
1868        // `primary_constructor_base_type`, the class form hangs them
1869        // straight off the `base_list`. A record-only fixture says nothing
1870        // about the class arm, and vice versa.
1871        //
1872        // `S` and `R3` are the negatives: a base type with no argument
1873        // list is not a call, and neither spelling emits an
1874        // `argument_list` for it.
1875        let src = "class Sub(int x) : Base(x) { }
1876                   record R1(int x) : Base(x);
1877                   record class R2(int x) : Base(x);
1878                   class G<T>(int x) : GBase<T>(x) where T : class { }
1879                   class Multi(int x) : Base(x), IBase { }
1880                   struct S(int x) : IBase { }
1881                   record R3(int x) : Base;";
1882        // Anchors every spelling against a fixture edit: with presence
1883        // checks only, deleting `(x)` from the class form leaves the
1884        // record form satisfying the arm and this test green. The last
1885        // two counts anchor the declarations that would otherwise decay
1886        // into duplicates of `Sub` — `Multi` owns the fixture's only
1887        // comma (`Base(x), IBase`) and `G` its only type argument list
1888        // (`GBase<T>`), so trimming either back fails here by name.
1889        assert_csharp_fixture_spells(
1890            src,
1891            &[
1892                (
1893                    Csharp::PrimaryConstructorBaseType as u16,
1894                    2,
1895                    "`record` base calls",
1896                ),
1897                (Csharp::ArgumentList as u16, 5, "base-call argument lists"),
1898                (Csharp::BaseList2 as u16, 7, "base lists"),
1899                // The §2 drift marker for the arm's other half. Both ids
1900                // render to `"base_list"`; at this pin every declaration
1901                // family above emits 252 and none emits 246, so
1902                // `BaseList` is defensive and this zero says so. A
1903                // grammar bump that starts emitting it fails here rather
1904                // than silently changing which arm does the work.
1905                (Csharp::BaseList as u16, 0, "unaliased `base_list` nodes"),
1906                (Csharp::COMMA as u16, 1, "`Multi`'s trailing interface"),
1907                (Csharp::TypeArgumentList as u16, 1, "`G`'s generic base"),
1908            ],
1909        );
1910        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
1911            for name in ["Sub", "R1", "R2", "G", "Multi"] {
1912                assert_eq!(
1913                    child_space(&space, name).metrics.abc.branches(),
1914                    1,
1915                    "{name}: the primary-constructor base call is one branch"
1916                );
1917            }
1918            for name in ["S", "R3"] {
1919                assert_eq!(
1920                    child_space(&space, name).metrics.abc.branches(),
1921                    0,
1922                    "{name}: a base type with no argument list is not a call"
1923                );
1924            }
1925        });
1926    }
1927
1928    #[cfg(feature = "csharp")]
1929    #[test]
1930    fn csharp_base_list_gate_excludes_other_argument_lists() {
1931        // The `base_list` parent gate on the `ArgumentList` arm is
1932        // load-bearing, not decoration: ungated it bills a branch for the
1933        // argument list of every call and allocation in the file, on top
1934        // of the `InvocationExpression` / `ObjectCreationExpression` the
1935        // arm already counts — doubling both (#1406).
1936        //
1937        // The fixture pairs one real base call with the four other
1938        // argument-carrying productions C# spells nearby: an attribute
1939        // (`attribute_argument_list`), an invocation, an object creation,
1940        // and an indexer (`bracketed_argument_list`) alongside a generic
1941        // (`type_argument_list`). Those last three are distinct kind ids
1942        // rather than `argument_list`, so they pin the §1 neighbour set
1943        // as much as the gate.
1944        let src = "[System.Obsolete(\"why\")]
1945                   class A(int x) : Base(x) {
1946                       void M(int y) { Helper(y); }
1947                       object N() { return new Foo(1); }
1948                       int P(System.Collections.Generic.List<int> l, int i) { return l[i]; }
1949                   }";
1950        assert_csharp_fixture_spells(
1951            src,
1952            &[
1953                (
1954                    Csharp::AttributeArgumentList as u16,
1955                    1,
1956                    "attribute argument lists",
1957                ),
1958                (
1959                    Csharp::BracketedArgumentList as u16,
1960                    1,
1961                    "bracketed argument lists",
1962                ),
1963                (Csharp::TypeArgumentList as u16, 1, "type argument lists"),
1964                (Csharp::ArgumentList as u16, 3, "argument lists"),
1965            ],
1966        );
1967        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
1968            let class = child_space(&space, "A");
1969            assert_eq!(
1970                class.metrics.abc.branches(),
1971                1,
1972                "`: Base(x)` alone — the attribute's arguments are not a call"
1973            );
1974            // The controls. 2 apiece would mean the gate let the
1975            // argument list through beside the call that owns it.
1976            assert_eq!(
1977                child_space(class, "M").metrics.abc.branches(),
1978                1,
1979                "`Helper(y)` is one branch, not one per argument list"
1980            );
1981            assert_eq!(
1982                child_space(class, "N").metrics.abc.branches(),
1983                1,
1984                "`new Foo(1)` is one branch, not one per argument list"
1985            );
1986            // `P` carries the indexer and the generic deliberately, but
1987            // gets no assertion: `bracketed_argument_list` and
1988            // `type_argument_list` are distinct kind ids, so `P` reads 0
1989            // with the gate, without it, and without this change
1990            // altogether. The census above is what actually pins them —
1991            // an assertion here would read as coverage it cannot provide.
1992        });
1993    }
1994
1995    #[cfg(feature = "csharp")]
1996    #[test]
1997    fn csharp_base_call_and_constructor_initializer_do_not_double_count() {
1998        // A class can spell *both* delegations at once: the primary
1999        // constructor's `: Base(x)` and a secondary constructor's
2000        // `: this(a)`. They are separate nodes in separate spaces, so the
2001        // file scores 2 — one each. 3 would mean the #1406 arm also fired
2002        // on something `ConstructorInitializer` already owned; 1 would
2003        // mean one of the two stopped counting.
2004        //
2005        // Java has the equivalent guard
2006        // (`java_constructor_delegation_does_not_double_count_arguments`);
2007        // C# now has two delegation spellings and this is the pairing
2008        // test for them.
2009        let src = "class Both(int x) : Base(x) {
2010                       public Both(int a, int b) : this(a) { }
2011                   }";
2012        assert_csharp_fixture_spells(
2013            src,
2014            &[
2015                (Csharp::ConstructorInitializer as u16, 1, "`: this(a)`"),
2016                (Csharp::BaseList2 as u16, 1, "the primary-constructor base"),
2017            ],
2018        );
2019        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
2020            let class = child_space(&space, "Both");
2021            assert_eq!(
2022                class.metrics.abc.branches(),
2023                1,
2024                "the class space owns `: Base(x)` and nothing else"
2025            );
2026            assert_eq!(
2027                child_space(class, "Both").metrics.abc.branches(),
2028                1,
2029                "the secondary constructor owns `: this(a)` and nothing else"
2030            );
2031        });
2032    }
2033
2034    #[cfg(feature = "kotlin")]
2035    #[test]
2036    fn kotlin_constructor_delegation_is_a_branch() {
2037        // Kotlin's secondary-constructor delegation is a
2038        // `constructor_delegation_call`, a distinct production from
2039        // `CallExpression`, so it scored zero for the same reason (#1279).
2040        // expected: 1 branch — the `: super(x)` delegation.
2041        check_metrics::<KotlinParser>(
2042            "class Sub : Base {
2043                constructor(x: Int) : super(x) { }
2044            }",
2045            "foo.kt",
2046            |metric| {
2047                assert_eq!(metric.abc.branches_sum(), 1);
2048            },
2049        );
2050    }
2051
2052    #[cfg(feature = "kotlin")]
2053    #[test]
2054    fn kotlin_primary_constructor_superclass_call_is_a_branch() {
2055        // Kotlin's *primary*-constructor superclass call is a
2056        // `constructor_invocation` under a `delegation_specifier` — a third
2057        // production, distinct from both `CallExpression` and the
2058        // `ConstructorDelegationCall` #1279 added, so it scored zero while
2059        // the secondary form beside it scored one (#1384). An object
2060        // expression's superclass call uses the same production.
2061        //
2062        // `class Plain : Marker` is the negative case: a delegation
2063        // specifier with no argument list is a plain `user_type`, so it
2064        // must stay at zero. Broadening the arm to bare
2065        // `DelegationSpecifier` fails here at `branches_sum` (5 against
2066        // 3), though not on `Plain`'s account alone: `class Classic :
2067        // Base` below is argument-less too, so deleting `Plain` still
2068        // leaves the broadening failing, at 4. What `Plain` buys is the
2069        // case stated outright rather than riding on the contrast fixture,
2070        // and since the per-space assertions below name it, trimming it
2071        // from the fixture fails in `child_space` instead of passing
2072        // silently as it did while this test read only the sum.
2073        //
2074        // `class Classic` carries the secondary spelling of the same call
2075        // alongside, which is what makes the per-space assertions below a
2076        // contrast rather than a census: a class header's superclass call
2077        // sits in `delegation_specifiers`, outside every member, so no
2078        // function space encloses it and the branch is the **class**
2079        // space's, while `constructor(x: Int) : super(x)` opens a function
2080        // space and takes the branch with it. `branches_sum` cannot see the
2081        // difference — the file totals 3 either way — so a codebase moving
2082        // to primary constructors shifts per-function `branches_max` /
2083        // `branches_average` and per-space `abc` thresholds without any
2084        // behaviour change. That is deliberate (there is no function to
2085        // attribute the call to), and C# has the identical split, pinned by
2086        // `csharp_base_call_and_constructor_initializer_do_not_double_count`
2087        // (#1456).
2088        //
2089        // expected: 3 branches — `Base(1, 2)`, `Base(3)` and `super(x)`.
2090        // Nothing else in the fixture is a call, so deleting any one
2091        // construction from the fixture moves both the total and the space
2092        // it belongs to.
2093        check_func_space::<KotlinParser, _>(
2094            "class Sub : Base(1, 2) { }
2095             class Plain : Marker { }
2096             fun make(): Any = object : Base(3) { }
2097             class Classic : Base {
2098                 constructor(x: Int) : super(x) { }
2099             }",
2100            "foo.kt",
2101            |space| {
2102                assert_eq!(space.metrics.abc.branches_sum(), 3);
2103                assert_eq!(
2104                    child_space(&space, "Sub").metrics.abc.branches(),
2105                    1,
2106                    "the primary-constructor superclass call is the class space's"
2107                );
2108                assert_eq!(
2109                    child_space(&space, "Plain").metrics.abc.branches(),
2110                    0,
2111                    "a delegation specifier with no argument list is not a call"
2112                );
2113                // The object expression is its own Class space (#463), so
2114                // `make` holds nothing of its own — the same attribution
2115                // one level down.
2116                let make = child_space(&space, "make");
2117                assert_eq!(
2118                    make.metrics.abc.branches(),
2119                    0,
2120                    "`make` owns no branch; `object : Base(3)` is its own space"
2121                );
2122                assert_eq!(
2123                    child_space(make, "<anonymous>").metrics.abc.branches(),
2124                    1,
2125                    "the object expression's superclass call is that space's"
2126                );
2127                // The contrast. `Classic` spells the same call through a
2128                // secondary constructor, which *does* open a function
2129                // space, so the class owns nothing.
2130                let classic = child_space(&space, "Classic");
2131                assert_eq!(
2132                    classic.metrics.abc.branches(),
2133                    0,
2134                    "a secondary constructor's `: super(x)` is not the class's"
2135                );
2136                assert_eq!(
2137                    child_space(classic, "<anonymous>").metrics.abc.branches(),
2138                    1,
2139                    "it belongs to the constructor's own function space"
2140                );
2141            },
2142        );
2143    }
2144
2145    #[cfg(feature = "kotlin")]
2146    #[test]
2147    fn kotlin_annotation_arguments_are_not_a_branch() {
2148        // The parent gate on that arm is load-bearing, not decoration:
2149        // tree-sitter-kotlin-ng spells an annotation's argument list with
2150        // the *same* `constructor_invocation` production — `@Mark("a")`
2151        // parses as `annotation > constructor_invocation` and
2152        // `@file:Suppress(…)` as `file_annotation > constructor_invocation`.
2153        // Ungated, every argument-carrying annotation in a Kotlin file
2154        // would bill a branch, and annotations are everywhere (#1384).
2155        //
2156        // The fixture pairs three annotation spellings — file-level,
2157        // class-level and use-site-targeted — with one real superclass
2158        // call, so the total discriminates in both directions: 1 with the
2159        // gate, 4 without it, 0 without the arm at all.
2160        //
2161        // A correctly-excluded annotation contributes to no ABC axis, so
2162        // `branches_sum()` alone cannot notice the annotations being
2163        // trimmed out of the fixture — measured: dropping any of the three
2164        // leaves this test green, and with all three gone, removing the
2165        // production gate fails nothing. The node census below is the
2166        // anchor: it pins that the fixture really does hand the arm four
2167        // `constructor_invocation` nodes for it to score 1 out of, and
2168        // that the `file_annotation` parent — the spelling a denylist of
2169        // `Annotation` alone would have missed — is among them.
2170        //
2171        // expected: 1 branch — `Base(1)` only.
2172        let src = r#"@file:Suppress("unused")
2173             @Mark("a")
2174             class Ann : Base(1) {
2175                 @get:Mark("b")
2176                 val v: Int = 0
2177             }"#;
2178        let parser = KotlinParser::new(
2179            src.as_bytes().to_vec(),
2180            &std::path::PathBuf::from("foo.kt"),
2181            None,
2182        );
2183        assert_eq!(
2184            parser
2185                .root()
2186                .preorder()
2187                .filter(|n| n.kind_id() == Kotlin::ConstructorInvocation as u16)
2188                .count(),
2189            4,
2190            "fixture must keep three annotations plus the superclass call"
2191        );
2192        assert!(ast_has_kind_id(&parser, Kotlin::FileAnnotation as u16));
2193
2194        check_metrics::<KotlinParser>(src, "foo.kt", |metric| {
2195            assert_eq!(metric.abc.branches_sum(), 1);
2196        });
2197    }
2198
2199    #[cfg(feature = "groovy")]
2200    #[test]
2201    fn groovy_constructor_delegation_is_a_branch() {
2202        // Groovy already counted this shape before #1279; the assertion
2203        // pins the JVM-family parity the Java and Kotlin fixes restore.
2204        // expected: 1 branch — the `super(1)` delegation.
2205        check_metrics::<GroovyParser>(
2206            "class Sub extends Base {
2207                Sub() { super(1) }
2208            }",
2209            "foo.groovy",
2210            |metric| {
2211                assert_eq!(metric.abc.branches_sum(), 1);
2212            },
2213        );
2214    }
2215
2216    // The three tests below are one fix seen from three grammars (#1407):
2217    // an enum constant carrying constructor arguments is an object
2218    // construction and scored zero in every JVM-family language, which
2219    // #1279 and #1384 left behind because both looked at superclass
2220    // delegation only.
2221    //
2222    // Each fixture pairs an argument-carrying constant with a bare one in
2223    // the *same* enum, because that is the only shape that shows the gate
2224    // working: a fixture of only `A(1)` scores the same whether the arm is
2225    // gated on the argument list or matches every constant
2226    // (`.claude/rules/grammar-dispatch.md` §6). Each also carries a
2227    // constant whose argument is itself a call, which must score 2 rather
2228    // than 1 (§5).
2229    //
2230    // `branches_sum()` alone cannot notice the bare constant being trimmed
2231    // out — it contributes to no axis — so every fixture is anchored on a
2232    // node census that fails by name when a spelling is edited away
2233    // (`.claude/rules/testing.md`, "Perturb the fixture as well as the
2234    // production line"). The two are coupled in one helper for the same
2235    // reason `assert_kotlin_class_members` couples its pair: a branch
2236    // total added without the census reads as coverage and is not.
2237
2238    #[cfg(any(feature = "groovy", feature = "java", feature = "kotlin"))]
2239    fn assert_enum_branches<P: MetricSuite>(
2240        src: &str,
2241        path: &str,
2242        kinds: &[(u16, usize, &str)],
2243        branches: u64,
2244    ) {
2245        assert_fixture_spells::<P>(src, path, kinds);
2246        check_func_space::<P, _>(src, path, |space| {
2247            assert_eq!(space.metrics.abc.branches_sum(), branches);
2248        });
2249    }
2250
2251    #[cfg(feature = "java")]
2252    #[test]
2253    fn java_enum_constant_with_arguments_is_a_branch() {
2254        // `@SuppressWarnings("x") B` is the discriminating case for the
2255        // gate's *kind*, not just its presence: the annotation carries an
2256        // argument list of its own, so an arm gated on "has some argument
2257        // list anywhere below" would score it. It cannot satisfy this one
2258        // twice over — `annotation_argument_list` is a distinct kind id
2259        // from `argument_list`, and it hangs off the constant's `modifiers`
2260        // child rather than the constant itself. Both verified with `bca
2261        // dump`.
2262        //
2263        // expected: 4 branches — `A(1)`, `K(2)`, `D(…)`, and the `f()`
2264        // inside `D`'s argument list. `B` scores 0.
2265        let src = "class Outer {
2266                static int f() { return 1; }
2267                enum E {
2268                    A(1),
2269                    @SuppressWarnings(\"x\") B,
2270                    K(2),
2271                    D(f());
2272                    E(int v) {}
2273                }
2274            }";
2275        assert_enum_branches::<JavaParser>(
2276            src,
2277            "foo.java",
2278            &[
2279                (Java::EnumConstant as u16, 4, "enum constants"),
2280                (Java::ArgumentList as u16, 4, "argument lists"),
2281                (
2282                    Java::AnnotationArgumentList as u16,
2283                    1,
2284                    "`@SuppressWarnings`'s argument list",
2285                ),
2286                (Java::MethodInvocation as u16, 1, "`D`'s nested `f()` call"),
2287            ],
2288            4,
2289        );
2290    }
2291
2292    #[cfg(feature = "kotlin")]
2293    #[test]
2294    fn kotlin_enum_entry_with_arguments_is_a_branch() {
2295        // Kotlin needs a defaulted primary-constructor parameter to spell
2296        // both cases in one enum: `enum class E(val v: Int)` obliges every
2297        // entry to pass an argument, so `= 0` is what lets the bare `B`
2298        // sit beside `A(1)`. `enum class P` covers the other shape the gate
2299        // has to leave alone — an enum with no constructor at all, whose
2300        // entries can never carry an argument list.
2301        //
2302        // expected: 3 branches — `A(1)`, `C(…)`, and the `g()` inside `C`'s
2303        // argument list. `B`, `X` and `Y` score 0.
2304        let src = "fun g(): Int = 1
2305             enum class E(val v: Int = 0) { A(1), B, C(g()) }
2306             enum class P { X, Y }";
2307        assert_enum_branches::<KotlinParser>(
2308            src,
2309            "foo.kt",
2310            &[
2311                (Kotlin::EnumEntry as u16, 5, "enum entries"),
2312                (Kotlin::ValueArguments as u16, 3, "argument lists"),
2313                (Kotlin::CallExpression as u16, 1, "`C`'s nested `g()` call"),
2314            ],
2315            3,
2316        );
2317    }
2318
2319    #[cfg(feature = "groovy")]
2320    #[test]
2321    fn groovy_enum_constant_with_arguments_is_a_branch() {
2322        // Groovy's enum has no annotated-constant case to cover: the
2323        // dekobon grammar cannot parse `@Deprecated A(1)` inside an enum
2324        // body and recovers into `ERROR` nodes, emitting no `enum_constant`
2325        // at all. The remaining two cases are the ones that matter.
2326        //
2327        // expected: 3 branches — `A(1)`, `D(…)`, and the `Outer.f()` inside
2328        // `D`'s argument list. `B` scores 0.
2329        let src = "class Outer { static int f() { 1 } }
2330             enum E { A(1), B, D(Outer.f()) }";
2331        assert_enum_branches::<GroovyParser>(
2332            src,
2333            "foo.groovy",
2334            &[
2335                (Groovy::EnumConstant as u16, 3, "enum constants"),
2336                (Groovy::ArgumentList as u16, 3, "argument lists"),
2337                (
2338                    Groovy::MethodInvocation as u16,
2339                    1,
2340                    "`D`'s nested `Outer.f()` call",
2341                ),
2342            ],
2343            3,
2344        );
2345    }
2346
2347    #[cfg(feature = "groovy")]
2348    #[test]
2349    fn groovy_no_abc() {
2350        // Comment-only file has no executable code → all-zero ABC.
2351        check_metrics::<GroovyParser>(
2352            "// just a comment, no executable code",
2353            "foo.groovy",
2354            |metric| {
2355                assert_eq!(metric.abc.assignments_sum(), 0);
2356                assert_eq!(metric.abc.branches_sum(), 0);
2357                assert_eq!(metric.abc.conditions_sum(), 0);
2358            },
2359        );
2360    }
2361
2362    #[cfg(feature = "groovy")]
2363    #[test]
2364    fn groovy_single_assignment() {
2365        // `int x = 1` is a local-variable declaration whose `=` counts
2366        // as one assignment (matches Java's semantics).
2367        check_metrics::<GroovyParser>("int x = 1", "foo.groovy", |metric| {
2368            assert_eq!(metric.abc.assignments_sum(), 1);
2369            assert_eq!(metric.abc.branches_sum(), 0);
2370            assert_eq!(metric.abc.conditions_sum(), 0);
2371        });
2372    }
2373
2374    #[cfg(feature = "groovy")]
2375    #[test]
2376    fn groovy_assignments() {
2377        check_metrics::<GroovyParser>(
2378            "void f() {
2379                int a = 1
2380                int b = 2
2381                a = 3
2382                b = 4
2383                a += 1
2384                b -= 1
2385            }",
2386            "foo.groovy",
2387            |metric| {
2388                // Six `=` tokens total. The two `Final`-less local
2389                // var-decls (`int a = 1`, `int b = 2`) and the two
2390                // bare assignments (`a = 3`, `b = 4`) each contribute
2391                // one assignment via the `EQ` arm; the `+=` / `-=`
2392                // each contribute one via the compound-assign arm.
2393                assert_eq!(metric.abc.assignments_sum(), 6);
2394            },
2395        );
2396    }
2397
2398    #[cfg(feature = "groovy")]
2399    #[test]
2400    fn groovy_branches() {
2401        check_metrics::<GroovyParser>(
2402            "void f() {
2403                doStuff()
2404                helper.invoke()
2405                new Worker()
2406            }",
2407            "foo.groovy",
2408            |metric| {
2409                // 2 method invocations + 1 object creation = 3 branches
2410                assert_eq!(metric.abc.branches_sum(), 3);
2411            },
2412        );
2413    }
2414
2415    #[cfg(feature = "groovy")]
2416    #[test]
2417    fn groovy_conditions_in_if() {
2418        check_metrics::<GroovyParser>(
2419            "void f(int a) {
2420                if (a == 0) { println(a) }
2421                if (a >= 1) { println(a) }
2422                if (a != 2) { println(a) }
2423            }",
2424            "foo.groovy",
2425            |metric| {
2426                // Three relational ops = 3 conditions
2427                assert_eq!(metric.abc.conditions_sum(), 3);
2428            },
2429        );
2430    }
2431
2432    #[cfg(feature = "groovy")]
2433    #[test]
2434    fn groovy_branches_with_juxt_call() {
2435        // Groovy's parens-less call form `println foo` must be counted
2436        // as a branch (`JuxtFunctionCall`).
2437        check_metrics::<GroovyParser>(
2438            "void f() {
2439                println 'hi'
2440                println 'bye'
2441            }",
2442            "foo.groovy",
2443            |metric| {
2444                // 2 juxt calls = 2 branches.
2445                assert_eq!(metric.abc.branches_sum(), 2);
2446            },
2447        );
2448    }
2449
2450    #[cfg(feature = "groovy")]
2451    #[test]
2452    fn groovy_try_catch_conditions() {
2453        // Each `try` and `catch` keyword token contributes +1 to
2454        // conditions (mirrors Java).
2455        check_metrics::<GroovyParser>(
2456            "void f() {
2457                try {
2458                    risky()
2459                } catch (Exception e) {
2460                    handle(e)
2461                }
2462            }",
2463            "foo.groovy",
2464            |metric| {
2465                // try + catch = 2 conditions
2466                assert_eq!(metric.abc.conditions_sum(), 2);
2467            },
2468        );
2469    }
2470
2471    #[cfg(feature = "groovy")]
2472    #[test]
2473    fn groovy_ternary_conditions() {
2474        check_metrics::<GroovyParser>(
2475            "void f(int x) {
2476                def y = x > 0 ? 1 : 2
2477            }",
2478            "foo.groovy",
2479            |metric| {
2480                // QMARK alone is +1 condition, plus the `>` condition = 2.
2481                assert_eq!(metric.abc.conditions_sum(), 2);
2482            },
2483        );
2484    }
2485
2486    #[cfg(feature = "groovy")]
2487    #[test]
2488    fn groovy_constant_excluded_from_assignments() {
2489        // `final` declarations are not counted as assignments
2490        // (mirrors Java's `Final` handling).
2491        check_metrics::<GroovyParser>(
2492            "class A {
2493                final int CONST = 42
2494                int field = 0
2495            }",
2496            "foo.groovy",
2497            |metric| {
2498                // The `=` on `final int CONST = 42` is a constant
2499                // initialiser (skipped). Only `field = 0` counts.
2500                assert_eq!(metric.abc.assignments_sum(), 1);
2501            },
2502        );
2503    }
2504
2505    #[cfg(feature = "groovy")]
2506    #[test]
2507    fn groovy_malformed_parenthesized_no_panic() {
2508        // Regression: malformed Groovy input must not panic the ABC
2509        // walker; the `spaces.rs` Unit fallback (lesson 9) covers
2510        // structural recovery. amaanq's grammar treats `def x = (((`
2511        // as a `local_variable_declaration` whose initialiser is the
2512        // first opening paren — the `=` still fires the assignment
2513        // arm.
2514        check_metrics::<GroovyParser>("def x = (((", "foo.groovy", |metric| {
2515            assert_eq!(metric.abc.assignments_sum(), 1);
2516        });
2517    }
2518
2519    #[cfg(feature = "groovy")]
2520    #[test]
2521    fn groovy_bool_returning_terminal_kinds_count() {
2522        // Companion to `csharp_bool_returning_terminal_kinds_count`
2523        // (issue #372 / lesson #19). The dekobon Groovy grammar
2524        // shares Java's wrapping conventions for `FieldAccess` and
2525        // `InstanceofExpression`, but it splits casts into two
2526        // distinct kinds — `cast_expression` for the Groovy-idiomatic
2527        // `v as Boolean` and `parenthesized_type_cast` for the
2528        // Java-style `(boolean) v`. The grammar has no `await` or
2529        // `array_access` analogues, so the C# fix's five-kind set
2530        // collapses to four here (with the cast slot doubled).
2531        //
2532        // `instanceof` has since moved out of the terminal set to an
2533        // unconditional arm (#1461), so it no longer depends on the
2534        // slot to be counted. The total is unchanged, which is the
2535        // point of that change rather than an accident of it.
2536        //
2537        // expected: 4 conditions (one per `if`), 0 assignments,
2538        // 0 branches (no invocations).
2539        check_metrics::<GroovyParser>(
2540            "class Cfg { boolean flag }
2541            class A {
2542                void m(Object v, Cfg cfg) {
2543                    if (cfg.flag) { }
2544                    if ((boolean) v) { }
2545                    if (v as Boolean) { }
2546                    if (v instanceof Cfg) { }
2547                }
2548            }",
2549            "foo.groovy",
2550            |metric| {
2551                assert_eq!(metric.abc.conditions_sum(), 4);
2552                assert_eq!(metric.abc.assignments_sum(), 0);
2553                assert_eq!(metric.abc.branches_sum(), 0);
2554            },
2555        );
2556    }
2557
2558    // Groovy half of #1274 — see `java_generic_declarations_are_not_conditions`.
2559    // The class-level `type_parameters` and its bound's nested
2560    // `type_arguments` mirror the Java fixture; pre-fix this file scored
2561    // 2 conditions, expected 0.
2562    #[cfg(feature = "groovy")]
2563    #[test]
2564    fn groovy_generic_declarations_are_not_conditions() {
2565        check_metrics::<GroovyParser>(
2566            "class Gen<T extends Comparable<T>> {
2567                List<T> pick(Map<String, List<T>> m) { m.get(\"k\") }
2568            }",
2569            "foo.groovy",
2570            |metric| {
2571                assert_eq!(metric.abc.conditions_sum(), 0);
2572                // Non-vacuity guard — see the Java counterpart.
2573                assert_eq!(metric.abc.branches_sum(), 1);
2574            },
2575        );
2576    }
2577
2578    // Groovy counterpart of `java_generic_wildcard_is_not_a_condition`,
2579    // including its choice of a non-zero expected total and its
2580    // two-wildcards-one-ternary shape; the dekobon grammar emits the
2581    // same `wildcard` node. Pre-fix: 4.
2582    #[cfg(feature = "groovy")]
2583    #[test]
2584    fn groovy_generic_wildcard_is_not_a_condition() {
2585        check_metrics::<GroovyParser>(
2586            "class A {
2587                int m(List<? extends Number> xs, Map<String, ? extends Number> ys,
2588                      int a, int b) { a < b ? 1 : 2 }
2589            }",
2590            "foo.groovy",
2591            |metric| {
2592                assert_eq!(metric.abc.conditions_sum(), 2);
2593            },
2594        );
2595    }
2596
2597    // A generic *method* gets its own fixture because the dekobon
2598    // grammar gives it its own production: `def <U> U ident(U x)` emits
2599    // `method_type_parameters`, not the `type_parameters` the class form
2600    // above uses. It is the fourth and last production from which that
2601    // grammar emits a bare `<` / `>` (the others being
2602    // `binary_expression`, `type_arguments` and `type_parameters`), and
2603    // the one a denylist extended only with `TypeParameters` — the fix
2604    // this issue's own plan proposed — would still miss. Revert-verified
2605    // in both directions: under that denylist this test still fails
2606    // while `groovy_generic_declarations_are_not_conditions` passes.
2607    //
2608    // The body carries a ternary over a comparison for the same
2609    // non-vacuity reason as the wildcard tests: expected 2, pre-fix 4
2610    // (the `<U>` bracket pair), 0 if the fixture stops parsing.
2611    #[cfg(feature = "groovy")]
2612    #[test]
2613    fn groovy_method_type_parameters_are_not_conditions() {
2614        check_metrics::<GroovyParser>(
2615            "class A {
2616                def <U> U ident(U x, int a, int b) { a < b ? x : null }
2617            }",
2618            "foo.groovy",
2619            |metric| {
2620                assert_eq!(metric.abc.conditions_sum(), 2);
2621            },
2622        );
2623    }
2624
2625    // Groovy counterpart of
2626    // `java_comparison_operators_still_count_alongside_generics`: the
2627    // narrowed arm keeps counting real comparisons, pinned against the
2628    // cyclomatic decision count on the method's own space (§8).
2629    #[cfg(feature = "groovy")]
2630    #[test]
2631    fn groovy_elvis_counts_one_condition_per_token() {
2632        // `a ?: c` is a short-circuit decision Groovy cyclomatic already
2633        // counts and the Kotlin ABC arm counts for its identical token;
2634        // Groovy's arm listed neither `?:` nor its `elvis_expression`, so
2635        // a method whose only branching is elvis chains reported
2636        // cyclomatic > 1 with zero conditions. One per token, so the
2637        // grammar-dispatch §8 invariant holds on the chain:
2638        // `conditions() == cyclomatic() - 1`.
2639        check_func_space::<GroovyParser, _>(
2640            "class K { def f(a, c) { return a ?: c } }",
2641            "foo.groovy",
2642            |space| assert_deepest_conditions_match_cyclomatic(&space, 1),
2643        );
2644        check_func_space::<GroovyParser, _>(
2645            "class K { def g(a, b, c) { return a ?: b ?: c } }",
2646            "foo.groovy",
2647            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
2648        );
2649    }
2650
2651    #[cfg(feature = "groovy")]
2652    #[test]
2653    fn groovy_comparison_operators_still_count_alongside_generics() {
2654        check_func_space::<GroovyParser, _>(
2655            "class A {
2656                int m(List<String> xs, int a, int b) {
2657                    if (a < b) { return 1 }
2658                    if (a > b) { return 2 }
2659                    return 0
2660                }
2661            }",
2662            "foo.groovy",
2663            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
2664        );
2665    }
2666
2667    #[cfg(feature = "groovy")]
2668    #[test]
2669    fn groovy_if_multiple_conditions() {
2670        // Mirrors `java_if_multiple_conditions`: `&&` / `||` chains
2671        // and parenthesised unary forms each contribute one
2672        // condition per primitive comparison; the inspect-container
2673        // pass picks up the unary `!a` / `!b` arguments inside the
2674        // `BinaryExpression` and counts them too.
2675        check_metrics::<GroovyParser>(
2676            "void f(boolean a, boolean b, boolean c) {
2677                if (a || b || c) { println(a) }
2678                if (a && b && c) { println(a) }
2679                if (!a && !b) { println(a) }
2680            }",
2681            "foo.groovy",
2682            |metric| {
2683                // Conditions counted via the AMPAMP/PIPEPIPE arms
2684                // (one count per identifier in the chain — three
2685                // for `||`, three for `&&`, two for the unary chain)
2686                // = 8.
2687                assert_eq!(metric.abc.conditions_sum(), 8);
2688                // Three `println a` juxt calls — each is a branch.
2689                assert_eq!(metric.abc.branches_sum(), 3);
2690            },
2691        );
2692    }
2693
2694    #[cfg(feature = "groovy")]
2695    #[test]
2696    fn groovy_while_and_do_while_conditions() {
2697        // Covers the WhileStatement and DoStatement arms in
2698        // `impl Abc for GroovyCode`. Each `while` / `do-while` has
2699        // its condition inspected through `groovy_inspect_container`.
2700        check_metrics::<GroovyParser>(
2701            "void f(boolean a, boolean b) {
2702                while (a) {
2703                    a = false
2704                }
2705                do {
2706                    b = !b
2707                } while (b)
2708            }",
2709            "foo.groovy",
2710            |metric| {
2711                // `while(a)` + `while(b)` each contribute one condition;
2712                // the unary `!b` on the do body's right-hand side adds
2713                // one more via the assignment-arm inspection = 3.
2714                assert_eq!(metric.abc.conditions_sum(), 3);
2715                // Two assignments to existing variables (`a = false`,
2716                // `b = !b`).
2717                assert_eq!(metric.abc.assignments_sum(), 2);
2718            },
2719        );
2720    }
2721
2722    #[cfg(feature = "groovy")]
2723    #[test]
2724    fn groovy_if_while_boolean_literal_condition() {
2725        // Regression for the Groovy half of #371-class bugs: the
2726        // dekobon tree-sitter-groovy grammar wraps a bare
2727        // `true` / `false` literal used as the condition of
2728        // `if` / `while` / `do` / `?:` in a `boolean_literal` node
2729        // (`Groovy::BooleanLiteral`, kind_id 270), not the leaf
2730        // `True` / `False` keyword tokens. `groovy_count_condition`
2731        // must therefore match `BooleanLiteral` (the wrapper).
2732        // Without that, every literal-condition statement silently
2733        // scored 0 conditions. Mirror of
2734        // `csharp_if_while_boolean_literal_condition`.
2735        check_metrics::<GroovyParser>(
2736            "void m() {
2737                if (true) { println 'a' }
2738                if (false) { println 'b' }
2739                while (true) { break }
2740                int t = true ? 1 : 0
2741            }",
2742            "foo.groovy",
2743            |metric| {
2744                // Four literal-condition statements contribute 4
2745                // `BooleanLiteral` conditions (if / if / while /
2746                // ternary), plus the ternary's `?` token adds one
2747                // more via `groovy_count_token_condition` → 5
2748                // total. The `println` calls contribute 2 branches
2749                // (the `while` body's `break` is not a branch).
2750                // The `int t = …` initializer contributes 1
2751                // assignment.
2752                assert_eq!(metric.abc.conditions_sum(), 5);
2753                assert_eq!(metric.abc.branches_sum(), 2);
2754                assert_eq!(metric.abc.assignments_sum(), 1);
2755            },
2756        );
2757    }
2758
2759    #[cfg(feature = "groovy")]
2760    #[test]
2761    fn groovy_return_unary_boolean_literal() {
2762        // Companion to `groovy_if_while_boolean_literal_condition`:
2763        // a `!true` / `!false` operand inside a `return` statement
2764        // routes through `groovy_inspect_container` (via
2765        // `groovy_inspect_child(node, 1)` on the ReturnStatement).
2766        // The `!` operator establishes boolean context, then the
2767        // innermost-operand check matches `BooleanLiteral` — that
2768        // helper's `BooleanLiteral` arm must be present or the
2769        // count silently drops. Mutation-verified: removing
2770        // `BooleanLiteral` from `groovy_inspect_container` leaves
2771        // every other Groovy test passing.
2772        check_metrics::<GroovyParser>(
2773            "boolean f() {
2774                return !true
2775            }
2776            boolean g() {
2777                return !false
2778            }",
2779            "foo.groovy",
2780            |metric| {
2781                // Each `return !X` walks into
2782                // `groovy_inspect_container` with a UnaryExpression
2783                // wrapping a `BANG` + BooleanLiteral. The `!` arm
2784                // seeds `has_boolean_content = true` (ReturnStatement
2785                // is not a known-boolean parent), then the
2786                // BooleanLiteral operand contributes one condition.
2787                // Two `return !X` → 2 conditions, no branches, no
2788                // assignments.
2789                assert_eq!(metric.abc.conditions_sum(), 2);
2790                assert_eq!(metric.abc.branches_sum(), 0);
2791                assert_eq!(metric.abc.assignments_sum(), 0);
2792            },
2793        );
2794    }
2795
2796    #[cfg(feature = "groovy")]
2797    #[test]
2798    fn groovy_short_circuit_with_boolean_literal_operand() {
2799        // Companion to `groovy_if_while_boolean_literal_condition`:
2800        // a bare `true` / `false` operand of `&&` / `||` lands in
2801        // `groovy_count_unary_conditions`, which iterates the
2802        // parent BinaryExpression's children. That helper must
2803        // match the `BooleanLiteral` wrapper just like
2804        // `groovy_count_condition` does — otherwise the operand
2805        // silently scores zero. Mutation-verified: removing
2806        // `BooleanLiteral` from the `groovy_count_unary_conditions`
2807        // arm leaves every other Groovy test passing.
2808        check_metrics::<GroovyParser>(
2809            "void m(boolean x) {
2810                if (x && true) { println 'a' }
2811                if (false || x) { println 'b' }
2812            }",
2813            "foo.groovy",
2814            |metric| {
2815                // `&&` and `||` themselves are NOT in
2816                // `groovy_count_token_condition`'s match list —
2817                // they route through
2818                // `groovy_walk_for_conditions::AMPAMP|PIPEPIPE`,
2819                // which calls `groovy_count_unary_conditions` on
2820                // the parent BinaryExpression. Each invocation
2821                // counts every child that matches the terminal-
2822                // operand kinds and whose parent is a
2823                // BinaryExpression. For `x && true`: Identifier x
2824                // (+1) + BooleanLiteral true (+1) = 2. For
2825                // `false || x`: BooleanLiteral false (+1) +
2826                // Identifier x (+1) = 2. Total 4.
2827                assert_eq!(metric.abc.conditions_sum(), 4);
2828                assert_eq!(metric.abc.branches_sum(), 2);
2829                assert_eq!(metric.abc.assignments_sum(), 0);
2830            },
2831        );
2832    }
2833
2834    #[cfg(feature = "groovy")]
2835    #[test]
2836    fn groovy_methods_arguments_with_conditions() {
2837        // Mirror of `java_methods_arguments_with_conditions`: a
2838        // unary `!x` inside an argument list must count both the
2839        // method invocation as a branch AND the unary as a
2840        // condition. The `ArgumentList | ArgumentList2` arm in
2841        // `impl Abc for GroovyCode` is what exercises this.
2842        check_metrics::<GroovyParser>(
2843            "void f(boolean a, boolean b, boolean c) {
2844                m1(a)
2845                m1(!a)
2846                m2(!a, !b)
2847            }",
2848            "foo.groovy",
2849            |metric| {
2850                // 3 method invocations (m1, m1, m2) — each fires the
2851                // branches arm.
2852                assert_eq!(metric.abc.branches_sum(), 3);
2853                // Three `!` unaries — `m1(!a)` and the two args of
2854                // `m2(!a, !b)` — each contribute one condition via
2855                // the ArgumentList inspection.
2856                assert_eq!(metric.abc.conditions_sum(), 3);
2857            },
2858        );
2859    }
2860
2861    #[cfg(feature = "groovy")]
2862    #[test]
2863    fn groovy_return_with_conditions() {
2864        // Mirror of `java_return_with_conditions`: a parenthesised
2865        // or unary expression inside `return` flows through the
2866        // `ReturnStatement` arm to `groovy_inspect_container`.
2867        check_metrics::<GroovyParser>(
2868            "boolean f(boolean a) {
2869                return (a)
2870            }
2871            boolean g(boolean a) {
2872                return !a
2873            }",
2874            "foo.groovy",
2875            |metric| {
2876                // Only one of the two return forms surfaces a
2877                // condition: `return !a` hits the UnaryExpression
2878                // path and adds one; `return (a)` reaches
2879                // `groovy_inspect_container` but the inner
2880                // identifier `a` is not in a boolean-context-firing
2881                // parent, so no condition is added.
2882                assert_eq!(metric.abc.conditions_sum(), 1);
2883            },
2884        );
2885    }
2886
2887    #[cfg(feature = "groovy")]
2888    #[test]
2889    fn groovy_for_with_variable_declaration() {
2890        // Classical `for (int i = 0; cond; i++)` form. The init
2891        // slot's `int i = 0` is suppressed from assignments by the
2892        // `LocalVariableDeclaration` push/pop dance; the `i++` in
2893        // the update slot contributes one assignment via the
2894        // `PLUSPLUS` arm. The condition `i < 10` flows through the
2895        // `ForStatement` arm.
2896        check_metrics::<GroovyParser>(
2897            "void f() {
2898                for (int i = 0; i < 10; i++) {
2899                    println(i)
2900                }
2901            }",
2902            "foo.groovy",
2903            |metric| {
2904                // `int i = 0` fires the EQ arm + `i++` fires the
2905                // PLUSPLUS arm = 2 assignments.
2906                assert_eq!(metric.abc.assignments_sum(), 2);
2907                // `i < 10` is one condition (the LT arm).
2908                assert_eq!(metric.abc.conditions_sum(), 1);
2909            },
2910        );
2911    }
2912
2913    /// The existing `for` test uses `i < 10`, which the `LT` token arm
2914    /// counts on its own — `groovy_walk_for_statement` never
2915    /// contributes there, so it stayed uncovered. A bare-identifier
2916    /// condition has no comparison token, so the count can only come
2917    /// from the walker.
2918    #[cfg(feature = "groovy")]
2919    #[test]
2920    fn groovy_for_with_bare_identifier_condition() {
2921        check_metrics::<GroovyParser>(
2922            "void f(boolean go) {
2923                for (int i = 0; go; i++) {
2924                    println(i)
2925                }
2926            }",
2927            "foo.groovy",
2928            |metric| {
2929                // `go` is the whole condition and counts once.
2930                assert_eq!(metric.abc.conditions_sum(), 1);
2931                // `int i = 0` (EQ) + `i++` (PLUSPLUS) = 2, as in
2932                // `groovy_for_with_variable_declaration`.
2933                assert_eq!(metric.abc.assignments_sum(), 2);
2934            },
2935        );
2936    }
2937
2938    /// The same slot with the initialiser hoisted out of the header.
2939    /// Under the pre-#1276 positional cascade this was a distinct code
2940    /// path — the condition moved from child(4) to child(3) — and the
2941    /// pair is kept as a shape guard now that the walker reads the
2942    /// `condition` field and cannot see the difference.
2943    #[cfg(feature = "groovy")]
2944    #[test]
2945    fn groovy_for_with_empty_initializer_counts_the_condition() {
2946        check_metrics::<GroovyParser>(
2947            "void f(boolean go) {
2948                int i = 0
2949                for (; go; i++) {
2950                    println(i)
2951                }
2952            }",
2953            "foo.groovy",
2954            |metric| {
2955                assert_eq!(metric.abc.conditions_sum(), 1);
2956                // `int i = 0` (EQ) + `i++` (PLUSPLUS), as above — the
2957                // initialiser just moved out of the loop header.
2958                assert_eq!(metric.abc.assignments_sum(), 2);
2959            },
2960        );
2961    }
2962
2963    /// Issue #1276 changed Groovy's answer here, exactly as it changed
2964    /// Java's: the positional cascade counted a `;` / `)` landing at
2965    /// child(4) as a vacuously-true condition, so `for (;;)` scored
2966    /// one. An omitted test is not a decision, and every other impl
2967    /// scores it zero. See `java_empty_for_condition_counts_nothing`.
2968    #[cfg(feature = "groovy")]
2969    #[test]
2970    fn groovy_empty_for_condition_counts_nothing() {
2971        check_metrics::<GroovyParser>("void f() { for (;;) { break } }", "foo.groovy", |metric| {
2972            assert_eq!(metric.abc.conditions_sum(), 0);
2973        });
2974        // The second spelling that moved. Unlike Java's, Groovy's
2975        // `local_variable_declaration` does not swallow its `;`, so the
2976        // old cascade found `;` at child(3) and `;` at child(4) here
2977        // too, and counted one.
2978        check_metrics::<GroovyParser>(
2979            "void f() { for (int i = 0; ; i++) { break } }",
2980            "foo.groovy",
2981            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
2982        );
2983    }
2984
2985    /// The cascade's other defect, shared with Java: a comment in the
2986    /// header shifted every child index, so the condition went unread.
2987    /// Reading the `condition` field cannot shift.
2988    #[cfg(feature = "groovy")]
2989    #[test]
2990    fn groovy_for_condition_survives_a_header_comment() {
2991        check_metrics::<GroovyParser>(
2992            "void f(boolean go) { for (; /* n */ go; ) { break } }",
2993            "foo.groovy",
2994            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
2995        );
2996        // Unchanged control: the same loop without the comment.
2997        check_metrics::<GroovyParser>(
2998            "void f(boolean go) { for (; go; ) { break } }",
2999            "foo.groovy",
3000            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
3001        );
3002    }
3003
3004    /// C#'s `csharp_walk_for_statement` reads the loop condition off
3005    /// the named `condition` field and routes a parenthesised or
3006    /// `!`-prefixed one through `csharp_inspect_container`. Every other
3007    /// C# `for` test uses a comparison (`i < n`), which the `LT` token
3008    /// arm counts without entering the walker.
3009    #[cfg(feature = "csharp")]
3010    #[test]
3011    fn csharp_for_with_negated_condition() {
3012        check_metrics::<CsharpParser>(
3013            "class A {
3014                void M(bool done) {
3015                    for (int i = 0; !done; i++) { System.Console.WriteLine(i); }
3016                }
3017            }",
3018            "foo.cs",
3019            |metric| {
3020                // `!done` unwraps to the `done` terminal: one condition,
3021                // and no comparison token to double-count it.
3022                assert_eq!(metric.abc.conditions_sum(), 1);
3023                // `int i = 0` + `i++`.
3024                assert_eq!(metric.abc.assignments_sum(), 2);
3025                assert_eq!(metric.abc.branches_sum(), 1);
3026            },
3027        );
3028    }
3029
3030    #[cfg(feature = "groovy")]
3031    #[test]
3032    fn groovy_eq_arm_counts_outside_final_declarations() {
3033        // Bare reassignment of an already-declared variable: the `=`
3034        // belongs to no `final` declaration, so it counts. Mirrors
3035        // `java_eq_arm_counts_outside_final_declarations`.
3036        check_metrics::<GroovyParser>(
3037            "void f(int x) {
3038                x = 42
3039            }",
3040            "foo.groovy",
3041            |metric| {
3042                assert_eq!(metric.abc.assignments_sum(), 1);
3043                assert_eq!(metric.abc.branches_sum(), 0);
3044                assert_eq!(metric.abc.conditions_sum(), 0);
3045            },
3046        );
3047    }
3048
3049    #[cfg(feature = "groovy")]
3050    #[test]
3051    fn groovy_final_field_initializer_does_not_suppress_the_closure_body() {
3052        // The Groovy spelling of
3053        // `java_final_initializer_does_not_suppress_the_assignments_inside_it`:
3054        // a `final` field's closure body opens no space, and the sentinel
3055        // stack suppressed its `x = 1` with the declarator's own `=`.
3056        check_metrics::<GroovyParser>(
3057            "class K {
3058                int x
3059                final Closure c = { x = 1 }
3060                Closure d = { x = 2 }
3061                final int q = 3
3062            }",
3063            "foo.groovy",
3064            |metric| {
3065                // `x = 1`, `d = {…}`, `x = 2`; the two `final`
3066                // initializers are suppressed. Pre-fix: 2.
3067                assert_eq!(metric.abc.assignments_sum(), 3);
3068            },
3069        );
3070    }
3071
3072    #[cfg(feature = "groovy")]
3073    #[test]
3074    fn groovy_final_local_is_an_error_at_the_pinned_grammar() {
3075        // `groovy_eq_initializes_final_binding` lists
3076        // `local_variable_declaration` for symmetry with Java, but at
3077        // dekobon-tree-sitter-groovy 0.2.2 a `final` local never
3078        // reaches it: the parser emits an `ERROR` node for the
3079        // declaration, with or without a terminator, so both of its `=`
3080        // tokens count. This pins the grammar limitation so a bump that
3081        // starts parsing the local shows up here rather than as a silent
3082        // change in what the predicate suppresses.
3083        let source = b"class K { int x; def m() { final int a = 0; x = 1 } }";
3084        let parser = GroovyParser::new(
3085            source.to_vec(),
3086            &std::path::PathBuf::from("foo.groovy"),
3087            None,
3088        );
3089        assert!(
3090            ast_has_kind_id(&parser, u16::MAX),
3091            "a `final` local should still parse to an ERROR node; if the grammar \
3092             now accepts it, re-derive the local half of the predicate",
3093        );
3094        check_metrics::<GroovyParser>(
3095            "class K { int x; def m() { final int a = 0; x = 1 } }",
3096            "foo.groovy",
3097            |metric| {
3098                assert_eq!(metric.abc.assignments_sum(), 2);
3099            },
3100        );
3101    }
3102
3103    #[cfg(feature = "csharp")]
3104    #[test]
3105    fn csharp_const_initializer_shapes() {
3106        // `csharp_eq_initializes_const_binding` reads the `const`
3107        // through the `modifier` node one hop above the
3108        // `variable_declaration`, for a local and a field alike, behind
3109        // other modifiers; `readonly` is not `const` and its initializer
3110        // counts. A lambda's body opens its own space in C#, so unlike
3111        // Java there was never a nested `=` to leak — the row pins that
3112        // the structural rule keeps the count the sentinel gave.
3113        check_metrics::<CsharpParser>(
3114            "class K {
3115                int x;
3116                private const int Q = 1;
3117                public static readonly int R = 2;
3118                void M() { const int q = 3; System.Action a = () => { x = 4; }; }
3119            }",
3120            "foo.cs",
3121            |metric| {
3122                // `R = 2`, `a = () => …`, `x = 4`.
3123                assert_eq!(metric.abc.assignments_sum(), 3);
3124            },
3125        );
3126    }
3127
3128    #[cfg(feature = "csharp")]
3129    #[test]
3130    fn csharp_constant_declarations() {
3131        check_metrics::<CsharpParser>(
3132            "class A {
3133                private const int X1 = 0, Y1 = 0;
3134                public const float PI = 3.14f;
3135                const string HELLO = \"Hello,\";
3136                protected string world = \" world!\";
3137                public float e = 2.718f;
3138                private int x2 = 1, y2 = 2;
3139                void M() {
3140                    const int Z1 = 0, Z2 = 0, Z3 = 0;
3141                    const float T = 0.0f;
3142                    int z1 = 1, z2 = 2, z3 = 3;
3143                }
3144            }",
3145            "foo.cs",
3146            |metric| insta::assert_json_snapshot!(metric.abc),
3147        );
3148    }
3149
3150    #[cfg(feature = "csharp")]
3151    #[test]
3152    fn csharp_declarations_with_conditions() {
3153        check_metrics::<CsharpParser>(
3154            "class A {
3155                bool a = (1 == 2);
3156                bool b = (1 < 2);
3157                bool c = !true;
3158                bool d = !false;
3159            }",
3160            "foo.cs",
3161            |metric| insta::assert_json_snapshot!(metric.abc),
3162        );
3163    }
3164
3165    #[cfg(feature = "csharp")]
3166    #[test]
3167    fn csharp_assignments_with_conditions() {
3168        check_metrics::<CsharpParser>(
3169            "class A {
3170                void M() {
3171                    int a = 0;
3172                    a += 1;
3173                    a -= 2;
3174                    a *= 3;
3175                    a /= 4;
3176                    a %= 5;
3177                    a++;
3178                    a--;
3179                }
3180            }",
3181            "foo.cs",
3182            |metric| insta::assert_json_snapshot!(metric.abc),
3183        );
3184    }
3185
3186    #[cfg(feature = "csharp")]
3187    #[test]
3188    fn csharp_methods_arguments_with_conditions() {
3189        check_metrics::<CsharpParser>(
3190            "class A {
3191                void M(int x, int y) {
3192                    F(x == y, x < y, !x.Equals(y));
3193                }
3194                void F(bool a, bool b, bool c) {}
3195            }",
3196            "foo.cs",
3197            |metric| insta::assert_json_snapshot!(metric.abc),
3198        );
3199    }
3200
3201    #[cfg(feature = "csharp")]
3202    #[test]
3203    fn csharp_if_single_conditions() {
3204        check_metrics::<CsharpParser>(
3205            "class A {
3206                void M(int x) {
3207                    if (x > 0) { System.Console.WriteLine(\"a\"); }
3208                    if (x < 0) { System.Console.WriteLine(\"b\"); }
3209                    if (x == 0) { System.Console.WriteLine(\"c\"); }
3210                }
3211            }",
3212            "foo.cs",
3213            |metric| insta::assert_json_snapshot!(metric.abc),
3214        );
3215    }
3216
3217    #[cfg(feature = "csharp")]
3218    #[test]
3219    fn csharp_if_multiple_conditions() {
3220        check_metrics::<CsharpParser>(
3221            "class A {
3222                void M(int x, int y) {
3223                    if (x > 0 && y > 0) { System.Console.WriteLine(\"a\"); }
3224                    if (x < 0 || y < 0) { System.Console.WriteLine(\"b\"); }
3225                }
3226            }",
3227            "foo.cs",
3228            |metric| insta::assert_json_snapshot!(metric.abc),
3229        );
3230    }
3231
3232    #[cfg(feature = "csharp")]
3233    #[test]
3234    fn csharp_while_and_do_while_conditions() {
3235        check_metrics::<CsharpParser>(
3236            "class A {
3237                void M(int x) {
3238                    while (x > 0) { x--; }
3239                    do { x++; } while (x < 10);
3240                }
3241            }",
3242            "foo.cs",
3243            |metric| insta::assert_json_snapshot!(metric.abc),
3244        );
3245    }
3246
3247    #[cfg(feature = "csharp")]
3248    #[test]
3249    fn csharp_return_with_conditions() {
3250        check_metrics::<CsharpParser>(
3251            "class A {
3252                bool M(int x) {
3253                    return (x > 0);
3254                }
3255                bool N(int x) {
3256                    return !(x < 0);
3257                }
3258            }",
3259            "foo.cs",
3260            |metric| insta::assert_json_snapshot!(metric.abc),
3261        );
3262    }
3263
3264    // C# `switch` *expression* arms scored zero ABC conditions before
3265    // #456 — they carry no `case` / `default` token, so the token-driven
3266    // `csharp_count_token_condition` never saw them, even though C#
3267    // cyclomatic counts each non-discard arm. Revert-verified: adding the
3268    // gated `SwitchExpressionArm` arm is what lifts this from 0 to 2. The
3269    // bare `_ =>` discard arm is excluded (the `default:` analogue),
3270    // mirroring the cyclomatic gate (lesson 11).
3271    #[cfg(feature = "csharp")]
3272    #[test]
3273    fn csharp_switch_expression_arm_counts_condition() {
3274        check_metrics::<CsharpParser>(
3275            "class A {
3276                int M(int x) {
3277                    return x switch { 1 => 10, 2 => 20, _ => 0 };
3278                }
3279            }",
3280            "foo.cs",
3281            |metric| {
3282                // arm `1 =>` (+1) + arm `2 =>` (+1) + `_ =>` discard (+0).
3283                assert_eq!(metric.abc.conditions_sum(), 2);
3284            },
3285        );
3286    }
3287
3288    // Cross-language parity (lesson 11): a C# `switch` expression and the
3289    // equivalent Java arrow-`switch` must report the same ABC condition
3290    // count on equivalent code. Both have two concrete case arms and no
3291    // fallback arm, so both must count exactly 2. `check_metrics` takes a
3292    // non-capturing `fn` pointer, so the shared expected value (2) is
3293    // asserted in each callback rather than compared across closures; the
3294    // matching constant is what enforces parity. This guards against the
3295    // C# fix drifting away from the Java arrow-case treatment.
3296    #[cfg(all(feature = "csharp", feature = "java"))]
3297    #[test]
3298    fn csharp_java_switch_arm_abc_parity() {
3299        // C# switch expression: two arms, no fallback → 2 conditions.
3300        check_metrics::<CsharpParser>(
3301            "class A {
3302                int M(int x) {
3303                    return x switch { 1 => 10, 2 => 20 };
3304                }
3305            }",
3306            "foo.cs",
3307            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3308        );
3309
3310        // Equivalent Java arrow-`switch`: two case arms, no default → 2.
3311        check_metrics::<JavaParser>(
3312            "class A {
3313                int m(int x) {
3314                    return switch (x) { case 1 -> 10; case 2 -> 20; };
3315                }
3316            }",
3317            "foo.java",
3318            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3319        );
3320    }
3321
3322    // Issue #469: the `default` arm of a C-family `switch` is the
3323    // unconditional fallthrough and must NOT count as an ABC condition,
3324    // mirroring cyclomatic — which counts only the `Case` arms, never
3325    // the `Default` token.
3326    //
3327    // expected: each fixture is a single function whose switch has two
3328    // concrete `case` arms plus one `default`. ABC must count exactly
3329    // the two case arms (conditions = 2), matching cyclomatic's two
3330    // case-arm decisions. Pre-fix, every language below scored 3 (the
3331    // `Default` token leaked into the condition tally) — revert-verified
3332    // against the pre-#469 condition arms. We anchor on the integer
3333    // `conditions_sum()` headline (the value the public JSON serializes;
3334    // float magnitude is bit-brittle and excluded by the snapshot
3335    // policy). The cyclomatic side is pinned separately in
3336    // `java_csharp_cpp_switch_default_cyclomatic_parity` below, where
3337    // the per-space `cyclomatic()` decision count is isolated.
3338    #[cfg(feature = "java")]
3339    #[test]
3340    fn java_switch_default_not_a_condition() {
3341        // Classic statement `default:`.
3342        check_metrics::<JavaParser>(
3343            "class A {
3344                int m(int x) {
3345                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3346                }
3347            }",
3348            "foo.java",
3349            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3350        );
3351        // Arrow `default ->` — shares the same `Default` token.
3352        check_metrics::<JavaParser>(
3353            "class A {
3354                int m(int x) {
3355                    return switch (x) { case 1 -> 1; case 2 -> 2; default -> 0; };
3356                }
3357            }",
3358            "foo.java",
3359            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3360        );
3361    }
3362
3363    // Over-exclusion guard (issue #469): a statement `switch` with two
3364    // `case` arms and NO `default` must still count both cases. This
3365    // pins that the fix excludes only the `Default` token, never a
3366    // `Case` arm — the count is identical before and after #469 (two
3367    // cases → 2), so it would catch a fix that over-eagerly dropped a
3368    // real case (e.g. treating the trailing case as a fallthrough).
3369    // expected: case 1 (+1) + case 2 (+1) = 2.
3370    #[cfg(feature = "java")]
3371    #[test]
3372    fn java_switch_without_default_counts_all_cases() {
3373        check_metrics::<JavaParser>(
3374            "class A {
3375                int m(int x) {
3376                    switch (x) { case 1: return 1; case 2: return 2; }
3377                    return -1;
3378                }
3379            }",
3380            "foo.java",
3381            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3382        );
3383    }
3384
3385    #[cfg(feature = "csharp")]
3386    #[test]
3387    fn csharp_switch_default_not_a_condition() {
3388        check_metrics::<CsharpParser>(
3389            "class A {
3390                int M(int x) {
3391                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3392                }
3393            }",
3394            "foo.cs",
3395            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3396        );
3397    }
3398
3399    /// Regression #1450 / #1451: `goto case 2;` spells the same `case`
3400    /// keyword token (id 114) as a real arm, from a second production
3401    /// (`goto_statement`), and scored an ABC condition it does not earn —
3402    /// an unconditional jump to an arm is not a decision.
3403    ///
3404    /// `jmp` differs from `ctl` by exactly one statement, so the
3405    /// `goto case` is the only thing that can separate their scores
3406    /// (`.claude/rules/grammar-dispatch.md` §11 — an arm and a
3407    /// `goto case` are independent paths into the same counter, and a
3408    /// fixture carrying only arms cannot show the gate works). `dfl` is
3409    /// the control that was already correct, by accident rather than
3410    /// design: `goto default;` spells `Default`, which neither metric
3411    /// counts because it is the switch's unconditional fallthrough
3412    /// (#456, #469).
3413    ///
3414    /// [`assert_every_member_scores`] covers cyclomatic as well as ABC,
3415    /// through its per-member §8 parity assertion, and would catch the
3416    /// gate being removed from *either* side alone: dropping ABC's puts
3417    /// `jmp` at conditions 3 against the asserted 2, and dropping
3418    /// cyclomatic's leaves conditions 2 against a decision count of 3.
3419    /// Removing both restores the parity at `3 == 4 - 1` and is caught by
3420    /// the conditions value, which is why 2 is named rather than derived.
3421    ///
3422    /// The `Case` anchor is the second axis. Every member scores 2 from
3423    /// its two arms alone, so deleting `goto case 2;` leaves every
3424    /// assertion below satisfied and the construct under test gone; the
3425    /// anchor's count of 7 — six arms plus the one jump — fails by name
3426    /// instead.
3427    #[cfg(feature = "csharp")]
3428    #[test]
3429    fn csharp_goto_case_is_not_a_condition() {
3430        let src = "class A {
3431                int ctl(int x) { switch (x) { case 1: return 1; case 2: return 2; default: return 0; } }
3432                int jmp(int x) { switch (x) { case 1: goto case 2; case 2: return 2; default: return 0; } }
3433                int dfl(int x) { switch (x) { case 1: goto default; case 2: return 2; default: return 0; } }
3434            }";
3435        assert_csharp_fixture_spells(
3436            src,
3437            &[
3438                (Csharp::Case as u16, 7, "`case` tokens (6 arms + 1 jump)"),
3439                (Csharp::SwitchSection as u16, 9, "switch arms"),
3440                (Csharp::GotoStatement as u16, 2, "`goto` statements"),
3441                (
3442                    Csharp::Default as u16,
3443                    4,
3444                    "`default` tokens (3 arms + 1 jump)",
3445                ),
3446            ],
3447        );
3448        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
3449            assert_every_member_scores(
3450                &space.spaces[0],
3451                3,
3452                2,
3453                "one condition per `case` arm, none for a `goto` into one",
3454            );
3455        });
3456    }
3457
3458    #[cfg(feature = "cpp")]
3459    #[test]
3460    fn cpp_switch_default_not_a_condition() {
3461        // C++ (and plain C, which shares this grammar) already excluded
3462        // `default`; this pins the cross-language parity invariant.
3463        check_metrics::<CppParser>(
3464            "void f(int x) {
3465                 switch (x) { case 1: return; case 2: return; default: return; }
3466             }",
3467            "foo.cpp",
3468            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3469        );
3470    }
3471
3472    #[cfg(feature = "objc")]
3473    #[test]
3474    fn objc_abc() {
3475        // ObjC ABC reuses the C/C++ walker with two additions: a message
3476        // send `[obj msg]` is a call (B), and `@try` / `@catch` count as
3477        // conditions (C) like C++ try/catch.
3478        //   A: `int total = 0`, `int i = 0`, `i++`, `total = total + …`,
3479        //      `total = -1` = 5.
3480        //   B: `[self valueAt:i]`, `[self risky]` = 2 message sends.
3481        //   C: `i < n`, `@try`, `@catch`, `total >= 0` = 4.
3482        check_metrics::<ObjcParser>(
3483            "@implementation Foo\n\
3484             - (int)bar:(int)n {\n\
3485                 int total = 0;\n\
3486                 for (int i = 0; i < n; i++) {\n\
3487                     total = total + [self valueAt:i];\n\
3488                 }\n\
3489                 @try {\n\
3490                     [self risky];\n\
3491                 } @catch (NSException *e) {\n\
3492                     total = -1;\n\
3493                 }\n\
3494                 if (total >= 0) {\n\
3495                     return total;\n\
3496                 }\n\
3497                 return 0;\n\
3498             }\n\
3499             @end\n",
3500            "foo.m",
3501            |metric| {
3502                assert_eq!(metric.abc.assignments_sum(), 5);
3503                assert_eq!(metric.abc.branches_sum(), 2);
3504                assert_eq!(metric.abc.conditions_sum(), 4);
3505            },
3506        );
3507    }
3508
3509    #[cfg(feature = "objc")]
3510    #[test]
3511    fn objc_abc_conditions() {
3512        // Exercises the condition-slot arms shared with C/C++: a `while`
3513        // head, a `&&` chain, a `do … while` trailing condition, and a
3514        // `return <comparison>`. ObjC routes these through the same
3515        // grammar-agnostic `cpp_inspect_*` helpers.
3516        //   A: `p++`, `p--` = 2.   B: no calls = 0.
3517        //   C: `p != 0`, `*p > 0`, `*p < 9`, `*p == 0` = 4.
3518        check_metrics::<ObjcParser>(
3519            "@implementation Foo\n\
3520             - (int)g:(int *)p {\n\
3521                 while (p != 0 && *p > 0) {\n\
3522                     p++;\n\
3523                 }\n\
3524                 do {\n\
3525                     p--;\n\
3526                 } while (*p < 9);\n\
3527                 return *p == 0;\n\
3528             }\n\
3529             @end\n",
3530            "foo.m",
3531            |metric| {
3532                assert_eq!(metric.abc.assignments_sum(), 2);
3533                assert_eq!(metric.abc.branches_sum(), 0);
3534                assert_eq!(metric.abc.conditions_sum(), 4);
3535            },
3536        );
3537    }
3538
3539    #[cfg(feature = "objc")]
3540    #[test]
3541    fn objc_abc_message_send_unary_condition() {
3542        // A negated boolean passed as a message-send argument is a unary
3543        // condition (Fitzpatrick Rule 9), the same as in a C-call argument.
3544        // Message args are direct children of `message_expression` (no
3545        // `argument_list`), so they are inspected in the `MessageExpression`
3546        // arm. Here: `[self use:!a]` (1 call + 1 unary condition) +
3547        // `cFunc(!a)` (1 call + 1 unary condition) → B=2, C=2.
3548        check_metrics::<ObjcParser>(
3549            "@implementation Foo\n\
3550             - (void)bar:(int)a {\n\
3551                 [self use:!a];\n\
3552                 cFunc(!a);\n\
3553             }\n\
3554             @end\n",
3555            "foo.m",
3556            |metric| {
3557                assert_eq!(metric.abc.branches_sum(), 2);
3558                assert_eq!(metric.abc.conditions_sum(), 2);
3559            },
3560        );
3561    }
3562
3563    #[cfg(feature = "objc")]
3564    #[test]
3565    fn objc_message_send_is_a_bool_terminal_in_condition_slots() {
3566        // `[obj ok]` is Objective-C's call, so in a condition slot it is
3567        // the unary condition `ok()` is (Fitzpatrick Rule 9). Until
3568        // `message_expression` joined `cpp_bool_terminal_kinds!` every
3569        // row here scored zero conditions where its C-call twin scored
3570        // one — and #1276's `for` slot inherited the gap. Each row is a
3571        // message send as the *whole* slot (grammar-dispatch §11: a
3572        // comparison would be counted by its own operator arm anyway),
3573        // and branches are asserted beside conditions so the arm cannot
3574        // be read as counting the call twice. The last three are the
3575        // over-count guards: a value position counts nothing, as `ok()`'s
3576        // does not.
3577        let cases = [
3578            ("if ([self ok]) {}", 1, 1),
3579            ("while (![self ok]) {}", 1, 1),
3580            ("do {} while ([self ok]);", 1, 1),
3581            ("for (; [self ok]; ) {}", 1, 1),
3582            ("x = [self ok] ? 1 : 2;", 2, 1),
3583            ("if ([self ok] && [o ok]) {}", 2, 2),
3584            ("return [self ok];", 0, 1),
3585            ("[self use:[self ok]];", 0, 2),
3586            ("[self use:![self ok]];", 1, 2),
3587        ];
3588        let mut ran = 0;
3589        for (body, conditions, branches) in cases {
3590            let src = format!("@implementation Foo\n- (int)bar {{\n    {body}\n}}\n@end\n");
3591            let abc = metrics_verbatim(
3592                LANG::Objc,
3593                src.as_bytes(),
3594                MetricsOptions::default().with_only(&[crate::Metric::Abc]),
3595            )
3596            .abc;
3597            assert_eq!(abc.conditions_sum(), conditions, "`{body}` conditions");
3598            assert_eq!(abc.branches_sum(), branches, "`{body}` branches");
3599            ran += 1;
3600        }
3601        assert_eq!(ran, cases.len());
3602        // Both answers are present, so a walker stuck at 0 or at 1 cannot
3603        // pass half the table silently.
3604        assert!(cases.iter().any(|&(_, c, _)| c == 0));
3605        assert!(cases.iter().any(|&(_, c, _)| c == 2));
3606    }
3607
3608    #[cfg(feature = "objc")]
3609    #[test]
3610    fn objc_message_send_condition_agrees_with_c_call() {
3611        // The intra-ObjC parity C++ cannot express: a message send and a
3612        // C call in the same slot score alike. Non-degenerate by the
3613        // `assert_eq!(…, 1)` on the reference.
3614        for (send, call) in [
3615            ("if ([a ok]) {}", "if (ok()) {}"),
3616            ("for (; [a ok]; ) {}", "for (; ok(); ) {}"),
3617            ("x = [a ok] ? 1 : 2;", "x = ok() ? 1 : 2;"),
3618        ] {
3619            let wrap =
3620                |body: &str| format!("@implementation Foo\n- (int)bar {{\n    {body}\n}}\n@end\n");
3621            let reference = abc_conditions(LANG::Objc, &wrap(call));
3622            assert!(reference >= 1, "`{call}` must count at least the slot");
3623            assert_eq!(
3624                abc_conditions(LANG::Objc, &wrap(send)),
3625                reference,
3626                "`{send}`"
3627            );
3628        }
3629    }
3630
3631    #[cfg(feature = "groovy")]
3632    #[test]
3633    fn groovy_switch_default_not_a_condition() {
3634        check_metrics::<GroovyParser>(
3635            "class A {
3636                int m(int x) {
3637                    switch (x) { case 1: return 1; case 2: return 2; default: return 0 }
3638                }
3639            }",
3640            "foo.groovy",
3641            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3642        );
3643    }
3644
3645    #[cfg(feature = "javascript")]
3646    #[test]
3647    fn js_switch_default_not_a_condition() {
3648        check_metrics::<JavascriptParser>(
3649            "function f(x) {
3650                 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3651             }",
3652            "foo.js",
3653            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3654        );
3655    }
3656
3657    #[cfg(feature = "typescript")]
3658    #[test]
3659    fn ts_switch_default_not_a_condition() {
3660        check_metrics::<TypescriptParser>(
3661            "function f(x: number): number {
3662                 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3663             }",
3664            "foo.ts",
3665            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3666        );
3667    }
3668
3669    // Cross-language parity (lesson 11): the equivalent statement-`switch`
3670    // with a `default` arm reports the same ABC condition count across
3671    // Java / C# / C++. All three have two concrete case arms plus a
3672    // fallthrough `default`, so all three must count exactly 2 conditions
3673    // (the `default` excluded). `check_metrics` takes a non-capturing
3674    // `fn` pointer, so the shared expected value is asserted in each
3675    // callback; the matching constant is what enforces parity.
3676    #[cfg(all(feature = "cpp", feature = "csharp", feature = "java"))]
3677    #[test]
3678    fn java_csharp_cpp_switch_default_abc_parity() {
3679        check_metrics::<JavaParser>(
3680            "class A {
3681                int m(int x) {
3682                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3683                }
3684            }",
3685            "foo.java",
3686            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3687        );
3688        check_metrics::<CsharpParser>(
3689            "class A {
3690                int M(int x) {
3691                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3692                }
3693            }",
3694            "foo.cs",
3695            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3696        );
3697        check_metrics::<CppParser>(
3698            "void f(int x) {
3699                 switch (x) { case 1: return; case 2: return; default: return; }
3700             }",
3701            "foo.cpp",
3702            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3703        );
3704    }
3705
3706    // Pins the ABC-vs-cyclomatic agreement the fix is about (lesson 11):
3707    // on the method's own function space, the cyclomatic decision count
3708    // (`cyclomatic()` minus the per-space base of 1) must equal the ABC
3709    // `conditions()` for the same switch. Both must be 2 — the two case
3710    // arms — with the `default` excluded from each. Revert-verified: pre-
3711    // #469 ABC `conditions()` was 3 here while cyclomatic stayed at 2.
3712    #[cfg(all(feature = "cpp", feature = "csharp", feature = "java"))]
3713    #[test]
3714    fn java_csharp_cpp_switch_default_cyclomatic_parity() {
3715        check_func_space::<JavaParser, _>(
3716            "class A {
3717                int m(int x) {
3718                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3719                }
3720            }",
3721            "foo.java",
3722            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3723        );
3724        check_func_space::<CsharpParser, _>(
3725            "class A {
3726                int M(int x) {
3727                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3728                }
3729            }",
3730            "foo.cs",
3731            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3732        );
3733        check_func_space::<CppParser, _>(
3734            "void f(int x) {
3735                 switch (x) { case 1: return; case 2: return; default: return; }
3736             }",
3737            "foo.cpp",
3738            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3739        );
3740    }
3741
3742    // Issue #473: PHP `switch` `default:` (`DefaultStatement`) is the
3743    // unconditional fallthrough, not a condition. ABC `conditions()` must
3744    // equal the cyclomatic decision count (`cyclomatic() - 1`) on the
3745    // function's own space — both 2 for the two `case` arms, with the
3746    // `default` excluded. Revert-verified: re-adding `DefaultStatement` to
3747    // the PHP ABC condition arm makes `conditions()` 3 here while cyclomatic
3748    // stays at 2, failing the invariant.
3749    #[cfg(feature = "php")]
3750    #[test]
3751    fn php_switch_default_not_a_condition() {
3752        check_func_space::<PhpParser, _>(
3753            "<?php
3754            function f($x) {
3755                switch ($x) {
3756                    case 1: return 1;
3757                    case 2: return 2;
3758                    default: return 0;
3759                }
3760            }",
3761            "foo.php",
3762            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3763        );
3764    }
3765
3766    // Issue #473: PHP `match` `default =>` (`MatchDefaultExpression`) is the
3767    // unconditional fallthrough, mirroring the switch `default:` case above.
3768    // ABC `conditions()` must equal the cyclomatic decision count
3769    // (`cyclomatic() - 1`) — both 2 for the two non-default match arms.
3770    // Revert-verified: re-adding `MatchDefaultExpression` to the PHP ABC
3771    // condition arm makes `conditions()` 3 here while cyclomatic stays at 2.
3772    #[cfg(feature = "php")]
3773    #[test]
3774    fn php_match_default_not_a_condition() {
3775        check_func_space::<PhpParser, _>(
3776            "<?php
3777            function g($x) {
3778                return match ($x) {
3779                    1 => \"a\",
3780                    2 => \"b\",
3781                    default => \"z\",
3782                };
3783            }",
3784            "foo.php",
3785            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3786        );
3787    }
3788
3789    #[cfg(feature = "csharp")]
3790    #[test]
3791    fn csharp_if_bare_identifier_condition() {
3792        check_metrics::<CsharpParser>(
3793            "class A {
3794                void M(bool x) {
3795                    if (x) { System.Console.WriteLine(\"a\"); }
3796                }
3797            }",
3798            "foo.cs",
3799            |metric| {
3800                // `if (x)` contributes 1 condition (bare identifier).
3801                // `System.Console.WriteLine(...)` is the only call → 1 branch.
3802                // `*_sum()` is what the public JSON serializes as the
3803                // headline value (see `crate::wire::Abc`).
3804                assert_eq!(metric.abc.conditions_sum(), 1);
3805                assert_eq!(metric.abc.branches_sum(), 1);
3806                assert_eq!(metric.abc.assignments_sum(), 0);
3807            },
3808        );
3809    }
3810
3811    #[cfg(feature = "csharp")]
3812    #[test]
3813    fn csharp_while_bare_identifier_condition() {
3814        check_metrics::<CsharpParser>(
3815            "class A {
3816                void M(bool x) {
3817                    while (x) { x = false; }
3818                }
3819            }",
3820            "foo.cs",
3821            |metric| {
3822                // `while (x)` contributes 1 condition; `x = false` is 1 assignment.
3823                assert_eq!(metric.abc.conditions_sum(), 1);
3824                assert_eq!(metric.abc.assignments_sum(), 1);
3825                assert_eq!(metric.abc.branches_sum(), 0);
3826            },
3827        );
3828    }
3829
3830    #[cfg(feature = "csharp")]
3831    #[test]
3832    fn csharp_do_while_bare_identifier_condition() {
3833        check_metrics::<CsharpParser>(
3834            "class A {
3835                void M(bool x) {
3836                    do { x = true; } while (x);
3837                }
3838            }",
3839            "foo.cs",
3840            |metric| {
3841                // `do { ... } while (x)` contributes 1 condition;
3842                // `x = true` is 1 assignment.
3843                assert_eq!(metric.abc.conditions_sum(), 1);
3844                assert_eq!(metric.abc.assignments_sum(), 1);
3845                assert_eq!(metric.abc.branches_sum(), 0);
3846            },
3847        );
3848    }
3849
3850    #[cfg(feature = "csharp")]
3851    #[test]
3852    fn csharp_if_unary_not_condition() {
3853        // Two cases share one test:
3854        //
3855        //   if (!x) { … }  — IfStatement is a known-boolean parent, so
3856        //   the unary `!` arm in `csharp_inspect_container` is *one of
3857        //   two* ways `has_boolean_content` gets set to true (the parent
3858        //   seed sets it before the `!` does). A regression that broke
3859        //   only the `is_not` branch wouldn't show up here.
3860        //
3861        //   return !x;  — ReturnStatement is *not* in the boolean-context
3862        //   seed list (BinaryExpression | IfStatement | WhileStatement |
3863        //   DoStatement | ForStatement | ConditionalExpression). So the
3864        //   `!` wrapper is the *only* path that sets
3865        //   `has_boolean_content = true`. Asserting the `return !x;`
3866        //   case isolates the unary-unwrap logic from the parent-seed
3867        //   path.
3868        check_metrics::<CsharpParser>(
3869            "class A {
3870                void M(bool x) {
3871                    if (!x) { System.Console.WriteLine(\"a\"); }
3872                }
3873                bool N(bool x) {
3874                    return !x;
3875                }
3876            }",
3877            "foo.cs",
3878            |metric| {
3879                // `if (!x)` contributes 1 condition (PrefixUnaryExpression
3880                // path with parent IfStatement seeding has_boolean_content).
3881                // `return !x;` contributes 1 condition (parent doesn't seed
3882                // — the unary `!` is the only path that sets the flag).
3883                // → 2 conditions total. 1 branch from WriteLine().
3884                assert_eq!(metric.abc.conditions_sum(), 2);
3885                assert_eq!(metric.abc.branches_sum(), 1);
3886                assert_eq!(metric.abc.assignments_sum(), 0);
3887            },
3888        );
3889    }
3890
3891    #[cfg(feature = "csharp")]
3892    #[test]
3893    fn csharp_if_double_parenthesized_condition() {
3894        // Audit-tests follow-up: with only the
3895        // `csharp_prefix_unary_expr_kinds!()` arm covered by
3896        // `csharp_if_unary_not_condition`, the
3897        // `csharp_paren_expr_kinds!()` delegation arm in
3898        // `csharp_count_condition` was a pure dead-code candidate —
3899        // disabling it caused zero existing tests to fail (verified
3900        // 2026-05-26).
3901        //
3902        // `if ((x))` puts a `ParenthesizedExpression` at child(2) of
3903        // the IfStatement (child(1) is the literal `(`, child(2) is
3904        // the inner parenthesised expression, child(3) is the literal
3905        // `)`). `csharp_count_condition` must route that case to
3906        // `csharp_inspect_container`, which then sees parent =
3907        // IfStatement, seeds `has_boolean_content = true`, walks to
3908        // the inner Identifier, and counts it. A regression that
3909        // removed the paren arm would silently score 0.
3910        check_metrics::<CsharpParser>(
3911            "class A {
3912                void M(bool x) {
3913                    if ((x)) { System.Console.WriteLine(\"a\"); }
3914                }
3915            }",
3916            "foo.cs",
3917            |metric| {
3918                assert_eq!(metric.abc.conditions_sum(), 1);
3919                assert_eq!(metric.abc.branches_sum(), 1);
3920                assert_eq!(metric.abc.assignments_sum(), 0);
3921            },
3922        );
3923    }
3924
3925    #[cfg(feature = "csharp")]
3926    #[test]
3927    fn csharp_bool_returning_terminal_kinds_count() {
3928        // Regression for issue #372 (lesson #19): before the fix,
3929        // `csharp_count_condition` / `csharp_inspect_container` only
3930        // recognised invocation / identifier / boolean literal as
3931        // terminal-bool operands, so the five idiomatic boolean
3932        // expressions in the `if (...)` slots below silently scored
3933        // zero conditions:
3934        //
3935        //   - `cfg.flag`        — MemberAccessExpression
3936        //   - `await c.Check()` — AwaitExpression
3937        //   - `(bool)v`         — CastExpression
3938        //   - `v is not null`   — IsPatternExpression
3939        //   - `flags[0]`        — ElementAccessExpression
3940        //
3941        // The `is` test has since moved out of the terminal set to an
3942        // unconditional arm (#1461), so it no longer depends on the
3943        // slot to be counted. The total is unchanged, which is the
3944        // point of that change rather than an accident of it.
3945        //
3946        // expected: 5 conditions (one per `if`), 0 assignments,
3947        // 1 branch (the single `c.Check()` invocation; the other
3948        // `if`-condition expressions are not invocations).
3949        check_metrics::<CsharpParser>(
3950            "using System.Threading.Tasks;
3951            class A {
3952                async Task M(object v, bool[] flags, Cfg cfg, C c) {
3953                    if (cfg.flag) { }
3954                    if (await c.Check()) { }
3955                    if ((bool)v) { }
3956                    if (v is not null) { }
3957                    if (flags[0]) { }
3958                }
3959            }
3960            class Cfg { public bool flag; }
3961            class C { public Task<bool> Check() => null; }",
3962            "foo.cs",
3963            |metric| {
3964                assert_eq!(metric.abc.conditions_sum(), 5);
3965                assert_eq!(metric.abc.assignments_sum(), 0);
3966                assert_eq!(metric.abc.branches_sum(), 1);
3967            },
3968        );
3969    }
3970
3971    #[cfg(feature = "csharp")]
3972    #[test]
3973    fn csharp_if_method_call_condition() {
3974        check_metrics::<CsharpParser>(
3975            "class A {
3976                void M(string s) {
3977                    if (s.StartsWith(\"x\")) { System.Console.WriteLine(\"a\"); }
3978                }
3979            }",
3980            "foo.cs",
3981            |metric| {
3982                // `if (s.StartsWith("x"))` contributes 1 condition
3983                // (InvocationExpression) plus 1 branch for the call itself,
3984                // plus 1 branch for WriteLine.
3985                assert_eq!(metric.abc.conditions_sum(), 1);
3986                assert_eq!(metric.abc.branches_sum(), 2);
3987                assert_eq!(metric.abc.assignments_sum(), 0);
3988            },
3989        );
3990    }
3991
3992    #[cfg(feature = "csharp")]
3993    #[test]
3994    fn csharp_if_while_boolean_literal_condition() {
3995        // Regression for #371: the tree-sitter-c-sharp grammar wraps a
3996        // bare `true` / `false` literal used as the condition of
3997        // `if` / `while` / `do` / `?:` in a `boolean_literal` node,
3998        // not the leaf `true` / `false` tokens. `csharp_count_condition`
3999        // must therefore match `BooleanLiteral` (the wrapper),
4000        // mirroring the existing `csharp_walk_for_statement` arm.
4001        // Without that, every literal-condition statement scored 0
4002        // conditions. The sibling `csharp_count_unary_conditions`
4003        // arm is covered separately by
4004        // `csharp_short_circuit_with_boolean_literal_operand` and
4005        // `csharp_inspect_container` is covered by
4006        // `csharp_declarations_with_conditions` (`!true` / `!false`).
4007        check_metrics::<CsharpParser>(
4008            "class A {
4009                void M() {
4010                    if (true) { System.Console.WriteLine(\"a\"); }
4011                    if (false) { System.Console.WriteLine(\"b\"); }
4012                    while (true) { break; }
4013                    do { break; } while (false);
4014                    int t = true ? 1 : 0;
4015                }
4016            }",
4017            "foo.cs",
4018            |metric| {
4019                // Five literal-condition statements contribute 5
4020                // `BooleanLiteral` conditions (one per if/if/while/
4021                // do-while/ternary), plus the ternary's `?` token
4022                // adds one more via `csharp_count_token_condition`
4023                // → 6 total. The two `System.Console.WriteLine`
4024                // calls contribute 2 branches; the `int t = …`
4025                // initializer contributes 1 assignment.
4026                assert_eq!(metric.abc.conditions_sum(), 6);
4027                assert_eq!(metric.abc.branches_sum(), 2);
4028                assert_eq!(metric.abc.assignments_sum(), 1);
4029            },
4030        );
4031    }
4032
4033    #[cfg(feature = "csharp")]
4034    #[test]
4035    fn csharp_short_circuit_with_boolean_literal_operand() {
4036        // Regression for #371 (companion to
4037        // `csharp_if_while_boolean_literal_condition`): a bare
4038        // `true` / `false` operand of `&&` / `||` lands in
4039        // `csharp_count_unary_conditions`, which iterates the parent
4040        // BinaryExpression's children. That helper must match the
4041        // `BooleanLiteral` wrapper just like `csharp_count_condition`
4042        // does — otherwise the operand silently scores zero. Mutation-
4043        // verified: removing `BooleanLiteral` from the
4044        // `csharp_count_unary_conditions` arm leaves every other test
4045        // in the suite passing, so this is the only test guarding
4046        // that helper's literal-operand path.
4047        check_metrics::<CsharpParser>(
4048            "class A {
4049                void M(bool x) {
4050                    if (x && true) { System.Console.WriteLine(\"a\"); }
4051                    if (false || x) { System.Console.WriteLine(\"b\"); }
4052                }
4053            }",
4054            "foo.cs",
4055            |metric| {
4056                // `&&` and `||` themselves are NOT in
4057                // `csharp_count_token_condition`'s match list — they
4058                // route through `csharp_walk_for_conditions::AMPAMP|
4059                // PIPEPIPE`, which calls
4060                // `csharp_count_unary_conditions` on the parent
4061                // BinaryExpression. Each invocation counts every
4062                // child that matches the terminal-operand kinds and
4063                // whose parent is a BinaryExpression. For
4064                // `x && true`: 1 (Identifier x) + 1 (BooleanLiteral
4065                // true) = 2. For `false || x`: 1 (BooleanLiteral
4066                // false) + 1 (Identifier x) = 2. Total 4. Without
4067                // the BooleanLiteral arm only the two Identifier
4068                // counts would land, giving 2.
4069                assert_eq!(metric.abc.conditions_sum(), 4);
4070                assert_eq!(metric.abc.branches_sum(), 2);
4071                assert_eq!(metric.abc.assignments_sum(), 0);
4072            },
4073        );
4074    }
4075
4076    #[cfg(feature = "csharp")]
4077    #[test]
4078    fn csharp_return_without_conditions() {
4079        check_metrics::<CsharpParser>(
4080            "class A {
4081                int M() { return 42; }
4082                string N() { return \"hi\"; }
4083            }",
4084            "foo.cs",
4085            |metric| insta::assert_json_snapshot!(metric.abc),
4086        );
4087    }
4088
4089    #[cfg(feature = "csharp")]
4090    #[test]
4091    fn csharp_lambda_expressions_return_with_conditions() {
4092        check_metrics::<CsharpParser>(
4093            "class A {
4094                public void M() {
4095                    System.Func<int, bool> f = x => (x > 0);
4096                    System.Func<int, bool> g = x => !(x < 0);
4097                }
4098            }",
4099            "foo.cs",
4100            |metric| insta::assert_json_snapshot!(metric.abc),
4101        );
4102    }
4103
4104    #[cfg(feature = "csharp")]
4105    #[test]
4106    fn csharp_for_with_variable_declaration() {
4107        check_metrics::<CsharpParser>(
4108            "class A {
4109                void M() {
4110                    for (int i = 0; i < 10; i++) {
4111                        System.Console.WriteLine(i);
4112                    }
4113                }
4114            }",
4115            "foo.cs",
4116            |metric| insta::assert_json_snapshot!(metric.abc),
4117        );
4118    }
4119
4120    #[cfg(feature = "csharp")]
4121    #[test]
4122    fn csharp_for_without_variable_declaration() {
4123        check_metrics::<CsharpParser>(
4124            "class A {
4125                void M() {
4126                    int i;
4127                    for (i = 0; i < 10; i++) {
4128                        System.Console.WriteLine(i);
4129                    }
4130                }
4131            }",
4132            "foo.cs",
4133            |metric| insta::assert_json_snapshot!(metric.abc),
4134        );
4135    }
4136
4137    #[cfg(feature = "csharp")]
4138    #[test]
4139    fn csharp_for_identifier_condition() {
4140        check_metrics::<CsharpParser>(
4141            "class A {
4142                void M(bool ready) {
4143                    for (; ready ;) { }
4144                }
4145            }",
4146            "foo.cs",
4147            |metric| {
4148                // expected: assignments=0 (no `=` / `++` / `--`),
4149                // branches=0 (no invocation / object creation),
4150                // conditions=1 (bare-identifier for-loop condition).
4151                // Averages divide by 3 spaces (top-level + class + method).
4152                insta::assert_json_snapshot!(
4153                    metric.abc,
4154                    @r#"
4155                {
4156                  "assignments": 0,
4157                  "branches": 0,
4158                  "conditions": 1,
4159                  "magnitude": 1.0,
4160                  "value": 0.0,
4161                  "assignments_average": 0.0,
4162                  "branches_average": 0.0,
4163                  "conditions_average": 0.3333333333333333,
4164                  "assignments_min": 0,
4165                  "assignments_max": 0,
4166                  "branches_min": 0,
4167                  "branches_max": 0,
4168                  "conditions_min": 0,
4169                  "conditions_max": 1
4170                }
4171                "#
4172                );
4173            },
4174        );
4175    }
4176
4177    #[cfg(feature = "csharp")]
4178    #[test]
4179    fn csharp_for_invocation_condition() {
4180        check_metrics::<CsharpParser>(
4181            "class A {
4182                bool Ok() { return true; }
4183                void M() {
4184                    for (; Ok() ;) { }
4185                }
4186            }",
4187            "foo.cs",
4188            |metric| {
4189                // expected: assignments=0, branches=1 (the `Ok()` call),
4190                // conditions=1 (invocation as for-loop condition).
4191                // Averages divide by 4 spaces (top-level + class + two
4192                // methods).
4193                insta::assert_json_snapshot!(
4194                    metric.abc,
4195                    @r#"
4196                {
4197                  "assignments": 0,
4198                  "branches": 1,
4199                  "conditions": 1,
4200                  "magnitude": 1.4142135623730951,
4201                  "value": 0.0,
4202                  "assignments_average": 0.0,
4203                  "branches_average": 0.25,
4204                  "conditions_average": 0.25,
4205                  "assignments_min": 0,
4206                  "assignments_max": 0,
4207                  "branches_min": 0,
4208                  "branches_max": 1,
4209                  "conditions_min": 0,
4210                  "conditions_max": 1
4211                }
4212                "#
4213                );
4214            },
4215        );
4216    }
4217
4218    // Regression coverage for #279: the C# grammar wraps a literal
4219    // `true` / `false` for-loop condition in a `boolean_literal` node.
4220    // The `BooleanLiteral` arm in the `ForStatement` dispatch must
4221    // attribute one condition; without it, `for (; true ;)` would
4222    // contribute 0 (the bug fixed by this commit also affected this
4223    // shape).
4224    #[cfg(feature = "csharp")]
4225    #[test]
4226    fn csharp_for_boolean_literal_condition() {
4227        check_metrics::<CsharpParser>(
4228            "class A {
4229                void M() {
4230                    for (; true ;) { }
4231                }
4232            }",
4233            "foo.cs",
4234            |metric| {
4235                // expected: assignments=0, branches=0,
4236                // conditions=1 (the `true` literal as condition).
4237                assert_eq!(metric.abc.conditions_sum(), 1);
4238                assert_eq!(metric.abc.assignments_sum(), 0);
4239                assert_eq!(metric.abc.branches_sum(), 0);
4240            },
4241        );
4242    }
4243
4244    // Regression coverage for #279: an empty for-loop condition such as
4245    // `for (; ;) {}` must contribute 0 to conditions — there is no
4246    // condition node to count.
4247    #[cfg(feature = "csharp")]
4248    #[test]
4249    fn csharp_for_empty_condition() {
4250        check_metrics::<CsharpParser>(
4251            "class A {
4252                void M() {
4253                    for (; ;) { }
4254                }
4255            }",
4256            "foo.cs",
4257            |metric| {
4258                // expected: assignments=0, branches=0, conditions=0
4259                // (no condition expression in `for (; ;)`).
4260                insta::assert_json_snapshot!(
4261                    metric.abc,
4262                    @r#"
4263                {
4264                  "assignments": 0,
4265                  "branches": 0,
4266                  "conditions": 0,
4267                  "magnitude": 0.0,
4268                  "value": 0.0,
4269                  "assignments_average": 0.0,
4270                  "branches_average": 0.0,
4271                  "conditions_average": 0.0,
4272                  "assignments_min": 0,
4273                  "assignments_max": 0,
4274                  "branches_min": 0,
4275                  "branches_max": 0,
4276                  "conditions_min": 0,
4277                  "conditions_max": 0
4278                }
4279                "#
4280                );
4281            },
4282        );
4283    }
4284
4285    #[cfg(feature = "csharp")]
4286    #[test]
4287    fn csharp_ternary_conditions() {
4288        check_metrics::<CsharpParser>(
4289            "class A {
4290                int Sign(int x) {
4291                    return (x > 0) ? 1 : (x < 0 ? -1 : 0);
4292                }
4293            }",
4294            "foo.cs",
4295            |metric| insta::assert_json_snapshot!(metric.abc),
4296        );
4297    }
4298
4299    #[cfg(feature = "csharp")]
4300    #[test]
4301    fn csharp_malformed_parenthesized_no_panic() {
4302        check_metrics::<CsharpParser>("class A { void M() { if (( }) }", "foo.cs", |metric| {
4303            // Don't panic on malformed source.
4304            assert_eq!(metric.abc.assignments(), 0);
4305            assert_eq!(metric.abc.branches(), 0);
4306        });
4307    }
4308
4309    #[cfg(feature = "csharp")]
4310    #[test]
4311    fn csharp_function_pointer_type_no_double_count() {
4312        // EC1 extension — `<` and `>` are also parameter-list delimiters
4313        // for unsafe function-pointer types. Since #1297 the arm is an
4314        // allowlist, so `FunctionPointerType` is excluded by being
4315        // absent from it rather than by being named in a deny set;
4316        // either way these brackets must not accumulate `conditions`.
4317        check_metrics::<CsharpParser>(
4318            "unsafe class A {
4319                public delegate*<int, int, int> Adder;
4320                public delegate*<string, void> Logger;
4321            }",
4322            "foo.cs",
4323            |metric| {
4324                assert_eq!(
4325                    metric.abc.conditions(),
4326                    0,
4327                    "function-pointer-type angle brackets must not count"
4328                );
4329            },
4330        );
4331    }
4332
4333    #[cfg(feature = "csharp")]
4334    #[test]
4335    fn csharp_generic_type_args_no_double_count() {
4336        // EC1 — `<` and `>` inside TypeArgumentList must not count as
4337        // boolean conditions.
4338        check_metrics::<CsharpParser>(
4339            "class A {
4340                void M(System.Collections.Generic.Dictionary<string, System.Collections.Generic.List<int>> d) {
4341                    System.Console.WriteLine(d);
4342                }
4343            }",
4344            "foo.cs",
4345            |metric| insta::assert_json_snapshot!(metric.abc),
4346        );
4347    }
4348
4349    // #1297: `operator <` names the operator a type *defines*; applying
4350    // one is what a decision looks like. tree-sitter-c-sharp spells the
4351    // declaration's operator with the same bare token as a comparison,
4352    // and the previous denylist named only `type_argument_list`,
4353    // `type_parameter_list` and `function_pointer_type`, so every
4354    // comparison-operator overload scored a condition.
4355    //
4356    // The fixture carries the two overloads plus one `a < b` inside a
4357    // `binary_expression` and one `x is > 0 ? 2 : 3`, which the grammar
4358    // parses as a `relational_pattern` whose operand is the ternary (see
4359    // the assertion). Every mis-aim lands on its own number: 6
4360    // with neither gate, 4 if `RelationalPattern` is readmitted to the
4361    // allowlist (#1383 dropped it), 2 if the gate swallows
4362    // `BinaryExpression` too (only the ternary `?` and the `is` test
4363    // survive), 0 if the fixture stops parsing. Each is one higher than
4364    // before #1461, which added the `is` test as a count no gate on the
4365    // `<` / `>` token can reach.
4366    #[cfg(feature = "csharp")]
4367    #[test]
4368    fn csharp_operator_declaration_is_not_a_condition() {
4369        check_func_space::<CsharpParser, _>(
4370            "class V {
4371                public static bool operator <(V a, V b) { return true; }
4372                public static bool operator >(V a, V b) { return true; }
4373                int m(int a, int b, int x) {
4374                    if (a < b) { return 1; }
4375                    return x is > 0 ? 2 : 3;
4376                }
4377            }",
4378            "foo.cs",
4379            |space| {
4380                // Assert the claim per space rather than through the
4381                // file total, which is 2 with the two overloads and 2
4382                // without them — an aggregate assertion would pass on a
4383                // fixture that had lost the very construct under test.
4384                let class = &space.spaces[0];
4385                assert_eq!(class.spaces.len(), 3, "two overloads plus `m`");
4386                for (i, member) in class.spaces.iter().enumerate().take(2) {
4387                    assert_eq!(
4388                        member.metrics.abc.conditions(),
4389                        0,
4390                        "operator declaration {i} must score no condition"
4391                    );
4392                }
4393                // 3: the `a < b` comparison, the ternary `?`, and the
4394                // `is` test. tree-sitter-c-sharp 0.23.5 parses
4395                // `x is > 0 ? 2 : 3` as `x is > (0 ? 2 : 3)` — the ternary
4396                // is the pattern's operand, so the pattern sits in no
4397                // decision slot and its `>` still scores nothing (#1383),
4398                // while the ternary's condition is the literal `0`.
4399                // The third count is the enclosing `is_pattern_expression`
4400                // itself, which since #1461 scores by use rather than only
4401                // inside a boolean slot. C# itself binds the source
4402                // `(x is > 0) ? 2 : 3`; a grammar that agrees still reads
4403                // 3 here, by the same three counts in a different
4404                // arrangement.
4405                assert_eq!(class.spaces[2].metrics.abc.conditions(), 3);
4406            },
4407        );
4408    }
4409
4410    // #1420, the other four spellings of the test above. C# overloads six
4411    // comparison operators and #1297 gated two: `<=` `>=` `==` `!=` are
4412    // distinct tokens that reach a different arm of
4413    // `csharp_count_token_condition` and each still scored one spurious
4414    // condition on its declaration — measured 1 apiece against `<` / `>`'s
4415    // 0, on a class where `cyclomatic()` is 1 for all six.
4416    //
4417    // Each token gets its own member so a partial fix names the spelling
4418    // it missed: gating `EQEQ | BANGEQ` and forgetting `GTEQ | LTEQ` fails
4419    // here by operator, where the file total cannot tell the two apart —
4420    // it reads 4 before the fix and 0 after, so any two-of-four fix halves
4421    // it and still looks like movement.
4422    //
4423    // `n` is the deliberate control (§11): the same four tokens *applied*,
4424    // inside `binary_expression` parents, which must keep scoring one
4425    // condition each. Without it the gate could be satisfied by refusing
4426    // to count these tokens at all.
4427    #[cfg(feature = "csharp")]
4428    #[test]
4429    fn csharp_comparison_operator_overloads_are_not_conditions() {
4430        let src = "class V {
4431                public static bool operator <=(V a, V b) { return true; }
4432                public static bool operator >=(V a, V b) { return true; }
4433                public static bool operator ==(V a, V b) { return true; }
4434                public static bool operator !=(V a, V b) { return true; }
4435                int n(int a, int b) {
4436                    if (a <= b) { return 1; }
4437                    if (a >= b) { return 2; }
4438                    if (a == b) { return 3; }
4439                    if (a != b) { return 4; }
4440                    return 0;
4441                }
4442            }";
4443        // Two of each token: one declaring, one applying. Trimming either
4444        // half out collapses this test into the other half's control and
4445        // is what this anchor exists to catch.
4446        assert_csharp_fixture_spells(
4447            src,
4448            &[
4449                (
4450                    Csharp::OperatorDeclaration as u16,
4451                    4,
4452                    "operator declarations",
4453                ),
4454                (Csharp::LTEQ as u16, 2, "`<=`"),
4455                (Csharp::GTEQ as u16, 2, "`>=`"),
4456                (Csharp::EQEQ as u16, 2, "`==`"),
4457                (Csharp::BANGEQ as u16, 2, "`!=`"),
4458            ],
4459        );
4460        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
4461            let class = &space.spaces[0];
4462            assert_eq!(class.spaces.len(), 5, "four overloads plus `n`");
4463            for (member, spelling) in class.spaces.iter().zip(["<=", ">=", "==", "!="]) {
4464                assert_eq!(
4465                    member.metrics.abc.conditions(),
4466                    0,
4467                    "`operator {spelling}` declares an operator, it does not apply one"
4468                );
4469                assert_eq!(
4470                    member.metrics.abc.conditions(),
4471                    member.metrics.cyclomatic.cyclomatic() - 1,
4472                    "`operator {spelling}`: §8 parity with the cyclomatic decision count"
4473                );
4474            }
4475            // The control, found by name so a reordered fixture cannot
4476            // silently re-point it at an overload. 0 here would mean the
4477            // gate swallowed the applying form along with the declaring
4478            // one.
4479            let control = child_space(class, "n");
4480            assert_eq!(
4481                control.metrics.abc.conditions(),
4482                4,
4483                "`n` applies all four tokens: one condition each"
4484            );
4485        });
4486    }
4487
4488    // The allowlist #1420 installed is the only thing keeping `#if A == B`
4489    // a condition, and it keeps it for a reason no arm states: the grammar
4490    // aliases `preproc_binary_expression` onto `binary_expression`, so the
4491    // preprocessor's `==` arrives with kind id `BinaryExpression` (369)
4492    // and not with one of its own. Before #1420 the token was ungated and
4493    // no fact about its parent mattered; now a grammar bump that gave the
4494    // alias a distinct id would stop counting every `#if` equality with
4495    // nothing red. This is that pin.
4496    //
4497    // `BinaryExpression2` (469) is the pre-alias symbol the arm also
4498    // lists, defensively, per `.claude/rules/grammar-dispatch.md` §1. §2
4499    // asks that such an entry be kept *and* have its unreachability
4500    // asserted, so that a pin promoting the symbol changes behaviour
4501    // loudly rather than invisibly.
4502    #[cfg(feature = "csharp")]
4503    #[test]
4504    fn csharp_preproc_equality_counts_through_the_binary_expression_alias() {
4505        let src = "class P {
4506            #if A == B
4507                int x;
4508            #endif
4509            #if C != D
4510                int y;
4511            #endif
4512            }";
4513        let parser = CsharpParser::new(
4514            src.as_bytes().to_vec(),
4515            std::path::Path::new("foo.cs"),
4516            None,
4517        );
4518        assert!(
4519            ast_has_kind_id(&parser, Csharp::BinaryExpression as u16),
4520            "the `#if` expressions must parse as `binary_expression` — \
4521             the alias is what the arm's allowlist matches"
4522        );
4523        assert!(
4524            !ast_has_kind_id(&parser, Csharp::BinaryExpression2 as u16),
4525            "`BinaryExpression2` is the pre-alias symbol and must stay \
4526             unreachable; if the grammar starts emitting it, re-derive the \
4527             allowlist rather than trusting this arm"
4528        );
4529        // 2, one per `#if` expression. The two directives carry no
4530        // declarations the walk would score, so nothing else contributes.
4531        check_metrics::<CsharpParser>(src, "foo.cs", |metric| {
4532            assert_eq!(metric.abc.conditions_sum(), 2);
4533        });
4534    }
4535
4536    // #1383: a `relational_pattern`'s operator is not a condition of
4537    // its own — the `switch_expression_arm` that owns it already scores
4538    // the decision, exactly as it does for the constant arm `5 => 1`.
4539    // Counting both charged a relational arm twice, and twice C#'s own
4540    // cyclomatic decision count.
4541    //
4542    // Per `.claude/rules/grammar-dispatch.md` §11 the `operator <`
4543    // fixture above cannot prove this alone: its `is > 0` sits beside a
4544    // `binary_expression` comparison that supplies a plausible total on
4545    // its own. Here the pattern operators are the *only* `<` / `>` /
4546    // `>=` / `<=` in the file, so a readmitted `RelationalPattern`
4547    // parent is the only thing that can lift the count.
4548    //
4549    // Both spellings are covered, and asserted per member rather than
4550    // through a total, because they reached two different arms when this
4551    // was written: `n`'s `>` / `<` were gated by a `GT | LT` parent
4552    // allowlist and `g`'s `>=` / `<=` by a separate `RelationalPattern`
4553    // denial, so fixing one and not the other left the *other method* at
4554    // 4. #1420 merged all six comparison tokens onto the one allowlist,
4555    // which removes that particular way to get it half right — but the
4556    // per-member shape still earns its keep, since a total of 6 is what
4557    // both `{2, 2, 2}` and a regressed `{4, 2, 0}` report:
4558    //
4559    // | state | `n` | `g` | `c` |
4560    // |---|---|---|---|
4561    // | `relational_pattern` excluded (shipped) | 2 | 2 | 2 |
4562    // | readmitted to the allowlist             | 4 | 4 | 2 |
4563    //
4564    // `c` is the constant-pattern control and reads 2 in both rows: it is
4565    // what the relational methods are supposed to agree with.
4566    #[cfg(feature = "csharp")]
4567    #[test]
4568    fn csharp_relational_pattern_does_not_double_count_its_arm() {
4569        let src = "class A {
4570                int n(int x) => x switch { > 5 => 1, < 0 => 2, _ => 3 };
4571                int g(int x) => x switch { >= 5 => 1, <= 0 => 2, _ => 3 };
4572                int c(int x) => x switch { 5 => 1, 0 => 2, _ => 3 };
4573            }";
4574        assert_csharp_fixture_spells(
4575            src,
4576            &[
4577                (Csharp::RelationalPattern as u16, 4, "relational patterns"),
4578                (Csharp::GT as u16, 1, "`>`"),
4579                (Csharp::LT as u16, 1, "`<`"),
4580                (Csharp::GTEQ as u16, 1, "`>=`"),
4581                (Csharp::LTEQ as u16, 1, "`<=`"),
4582            ],
4583        );
4584        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
4585            // Per space, not through the root: the root can only see
4586            // the sum, and a `{4, 2, 0}` regression sums to the same 6
4587            // as the correct `{2, 2, 2}`. `cyclomatic()` parity is also
4588            // only expressible per space — `cyclomatic_sum()` folds in
4589            // a base of 1 for each one.
4590            assert_every_member_scores(&space.spaces[0], 3, 2, "one condition per non-discard arm");
4591        });
4592    }
4593
4594    // The `is` half of #1383, and the one whose over-count came from a
4595    // different owner: `if (x is > 0)` scored the `IfStatement`
4596    // condition slot *and* the pattern's `>`. `f`'s guard is the
4597    // deliberate control — a `binary_expression` comparison in the same
4598    // slot still scores exactly one, so the fix cannot be read as
4599    // "stop counting `if` conditions".
4600    //
4601    // The two combinator methods pin the compound forms the issue asks
4602    // about. `and` / `or` are keyword tokens C# ABC never listed and C#
4603    // cyclomatic never counts, so the arm stays the single decision
4604    // however many relational operands it carries — which is exactly
4605    // what would regress if a later fix re-derived the gate from the
4606    // operand instead of the parent.
4607    #[cfg(feature = "csharp")]
4608    #[test]
4609    fn csharp_is_pattern_and_combinators_score_one_decision() {
4610        let src = "class A {
4611                int m(int x) { if (x is > 0) { return 1; } return 0; }
4612                int f(int x) { if (x > 0) { return 1; } return 0; }
4613                int a(int x) { if (x is > 0 and < 9) { return 1; } return 0; }
4614                int o(int x) { if (x is < 0 or >= 100) { return 1; } return 0; }
4615                int n(int x) { if (x is not > 5) { return 1; } return 0; }
4616            }";
4617        assert_csharp_fixture_spells(
4618            src,
4619            &[
4620                (Csharp::RelationalPattern as u16, 6, "relational patterns"),
4621                (Csharp::IsPatternExpression as u16, 4, "`is` pattern tests"),
4622                (Csharp::AndPattern as u16, 1, "`and`"),
4623                (Csharp::OrPattern as u16, 1, "`or`"),
4624                (Csharp::NegatedPattern as u16, 1, "`not`"),
4625                (Csharp::GTEQ as u16, 1, "`>=` (only `o` carries one)"),
4626            ],
4627        );
4628        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
4629            assert_every_member_scores(&space.spaces[0], 5, 1, "the `if` is the only decision");
4630        });
4631    }
4632
4633    // #1383's second, quieter effect, and the one its issue does not
4634    // mention: a relational pattern outside any decision slot went from
4635    // 1 to 0 as well, because the gate is on the operator's parent and
4636    // not on what encloses the pattern. That left `q` — the equivalent
4637    // binary comparison `x > 5` in the same slot — scoring 1 against
4638    // these four's 0, so a relational pattern read one *lower* than the
4639    // comparison it is sugar for. #1383 recorded that as a deliberate
4640    // exception to Fitzpatrick Rule 5 and kept it, because the only
4641    // route it had was to readmit the pattern's `<` / `>` token, which
4642    // would have scored the decision-slot case twice.
4643    //
4644    // #1461 closed it from the other end, and the test now pins the
4645    // agreement rather than the exception. The enclosing
4646    // `is_pattern_expression` scores by use, so `x is > 5` reads 1
4647    // wherever it is written — level with `q`, and level with the plain
4648    // type test `x is int` (`t`, whose `is_expression` moved for the
4649    // same reason). The pattern's own `>` still scores nothing, which
4650    // is what keeps the whole test at 1 rather than 2: one relational
4651    // operation, one condition.
4652    //
4653    // The four probes stay because they are four different enclosings —
4654    // a declarator, a bare `return`, an argument, a type test — and the
4655    // point was never that the pattern is special in one of them.
4656    #[cfg(feature = "csharp")]
4657    #[test]
4658    fn csharp_relational_pattern_scores_one_wherever_it_is_written() {
4659        let src = "class A {
4660                static bool M(bool b) { return b; }
4661                bool p(int x) { bool b = x is > 5; return b; }
4662                bool q(int x) { bool b = x > 5; return b; }
4663                bool r(int x) { return x is > 5; }
4664                bool s(int x) { return M(x is > 5); }
4665                bool t(object x) { bool b = x is int; return b; }
4666            }";
4667        assert_csharp_fixture_spells(
4668            src,
4669            &[
4670                (Csharp::RelationalPattern as u16, 3, "relational patterns"),
4671                (Csharp::IsPatternExpression as u16, 3, "`is` pattern tests"),
4672                (Csharp::IsExpression as u16, 1, "the `is int` control"),
4673            ],
4674        );
4675        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
4676            let class = &space.spaces[0];
4677            assert_eq!(class.spaces.len(), 6, "`M` plus five probes");
4678            let by_name = |n: &str| {
4679                class
4680                    .spaces
4681                    .iter()
4682                    .find(|s| s.name.as_deref() == Some(n))
4683                    .unwrap_or_else(|| panic!("fixture lost `{n}`"))
4684                    .metrics
4685                    .abc
4686                    .conditions()
4687            };
4688            // Named rather than indexed: the claim is about which
4689            // spelling scores what, so a reordering of the fixture must
4690            // not silently re-point the assertions.
4691            for probe in ["p", "r", "s"] {
4692                assert_eq!(by_name(probe), 1, "`{probe}`: the `is` test, once");
4693            }
4694            assert_eq!(by_name("t"), 1, "the bare type test, the same once");
4695            assert_eq!(by_name("q"), 1, "the comparison it is sugar for");
4696            assert_eq!(by_name("M"), 0, "no relational operator anywhere");
4697        });
4698    }
4699
4700    // The boundary of #1383: the pattern's operator stops counting, a
4701    // *binary* operator in the arm's `when` guard keeps counting (`w`).
4702    // Both sit under the same `switch_expression_arm`, so this is what
4703    // stops a later "patterns don't count" pass from suppressing the
4704    // guard too.
4705    //
4706    // A relational *pattern* in the guard (`g`) is the other side of that
4707    // line: the operator itself still scores nothing, because the gate is
4708    // on the operator's parent — but since #1422 the guard is a condition
4709    // slot, and an `is` test is one of `csharp_bool_terminal_kinds!()`, so
4710    // the slot pays for it. That is what closed the gap this test used to
4711    // carry a `FIXME(#1422)` for: `when n is > 5` now reads level with
4712    // `when n > 5` instead of one below it.
4713    //
4714    // §8 holds throughout, which it did not before #1422. The old table
4715    // here recorded the defect: neither metric modelled the guard, so
4716    // ABC's count was whatever operator token happened to sit inside it
4717    // and cyclomatic's was nothing at all. Both tiers measured, before
4718    // and after:
4719    //
4720    // | guard | `conditions()` | `cyclomatic() - 1` |
4721    // |---|---|---|
4722    // | `when x % 2 == 0` (this fixture) | 3 → 3 | 2 → 3 |
4723    // | `when x > 2`                     | 3 → 3 | 2 → 3 |
4724    // | `when IsEven(x)`                 | 2 → 3 | 2 → 3 |
4725    //
4726    // The `==` is the guard's own operator and the only `==` in the
4727    // file, so trimming the `when` clause out of `w` drops its count to
4728    // 2 rather than leaving the assertion satisfied by something else;
4729    // `assert_csharp_fixture_spells` pins both guards by kind against
4730    // the same decay.
4731    #[cfg(feature = "csharp")]
4732    #[test]
4733    fn csharp_switch_arm_guard_operator_still_counts() {
4734        let src = "class A {
4735                int w(int x) => x switch { > 0 when x % 2 == 0 => 1, > 0 => 2, _ => 3 };
4736                int g(int x) => x switch { int n when n is > 5 => 1, _ => 0 };
4737            }";
4738        assert_csharp_fixture_spells(
4739            src,
4740            &[
4741                (Csharp::WhenClause as u16, 2, "`when` guards"),
4742                (Csharp::EQEQ as u16, 1, "the guard's `==`"),
4743                (Csharp::RelationalPattern as u16, 3, "relational patterns"),
4744            ],
4745        );
4746        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
4747            let m = &space.spaces[0].spaces[0];
4748            assert_eq!(m.name.as_deref(), Some("w"));
4749            // 4 if the guard slot started counting a `binary_expression`
4750            // on top of its `==`, 2 if the `==` were suppressed along
4751            // with the pattern operators — both are live regressions.
4752            assert_eq!(
4753                m.metrics.abc.conditions(),
4754                3,
4755                "two arms plus the guard's `==`"
4756            );
4757            assert_eq!(m.metrics.cyclomatic.cyclomatic(), 4);
4758
4759            let g = &space.spaces[0].spaces[1];
4760            assert_eq!(g.name.as_deref(), Some("g"));
4761            assert_eq!(
4762                g.metrics.abc.conditions(),
4763                2,
4764                "one arm plus the guard slot, which scores the `is` test"
4765            );
4766            assert_eq!(g.metrics.cyclomatic.cyclomatic(), 3);
4767        });
4768    }
4769
4770    // #1422's headline: four spellings of one guard, previously worth
4771    // two different ABC numbers because nothing modelled the guard and
4772    // ABC counted whatever operator token happened to sit inside it —
4773    // `when x % 2 == 0` and `when x > 2` scored 1 through the
4774    // comparison-token arm, `when IsEven(x)` scored 0. Modelled as a
4775    // condition slot each contributes exactly 1, and the cyclomatic
4776    // `WhenClause` arm lifts the decision count to match.
4777    //
4778    // `none` is the unguarded control and the anchor: a two-arm switch
4779    // over the same relational patterns carrying no guard at all, so
4780    // every member here is exactly one guard away from it. Without that
4781    // row a fix that scored the *arms* differently would satisfy the
4782    // four guarded rows alike. Its arms run `> 2` before `> 0` because
4783    // the reverse order is a C# compile error (CS8510, the later arm
4784    // subsumed) — the guarded members escape that only because a guard
4785    // can fail, which is what makes their repeated `> 0` legal.
4786    //
4787    // `paren` is not a fourth spelling for its own sake — it is the only
4788    // member reaching the `WhenClause` seed added to
4789    // `csharp_inspect_container`, since `when_clause` wraps a
4790    // parenthesised guard in a real `parenthesized_expression` rather
4791    // than the anonymous parens `catch_filter_clause` uses.
4792    //
4793    // `nullc` is #1459's row and the counter-example that falsified the
4794    // claim above when it was first written. A `??` guard is a
4795    // `binary_expression`, which the slot declines — it leaves an
4796    // operator guard to the arm that already counts the operator — and
4797    // no ABC arm counted `??`, so this one spelling read 2 where the
4798    // other four read 3. Counting the token levels it without touching
4799    // the slot; an unconditional `+1` in the slot instead would have
4800    // taken `cmp` to 4, re-creating the double count #1422 removed.
4801    //
4802    // It is the one guarded member whose cyclomatic is not 4, and that
4803    // is not a discrepancy to fix: C# cyclomatic counts `??` as a
4804    // decision *in addition to* the guard clause, so the arm below
4805    // narrows the ABC-versus-cyclomatic gap from two to one rather than
4806    // closing it. The remaining one is the slot's standing policy of
4807    // leaving a `binary_expression` to its operators, and `cmp` pays it
4808    // too — it just happens to break even there.
4809    #[cfg(feature = "csharp")]
4810    #[test]
4811    fn csharp_switch_arm_guard_scores_one_condition_however_spelled() {
4812        let src = "class A {
4813                static bool IsEven(int x) { return true; }
4814                int tok(int x) => x switch { > 0 when x % 2 == 0 => 1, > 0 => 2, _ => 3 };
4815                int cmp(int x) => x switch { > 0 when x > 2 => 1, > 0 => 2, _ => 3 };
4816                int call(int x) => x switch { > 0 when IsEven(x) => 1, > 0 => 2, _ => 3 };
4817                int paren(int x) => x switch { > 0 when (IsEven(x)) => 1, > 0 => 2, _ => 3 };
4818                int nullc(int x, bool? b) => x switch { > 0 when b ?? false => 1, > 0 => 2, _ => 3 };
4819                int none(int x) => x switch { > 2 => 1, > 0 => 2, _ => 3 };
4820            }";
4821        assert_csharp_fixture_spells(
4822            src,
4823            &[
4824                (Csharp::WhenClause as u16, 5, "`when` guards"),
4825                (Csharp::EQEQ as u16, 1, "`tok`'s `==`"),
4826                (Csharp::QMARKQMARK as u16, 1, "`nullc`'s `??`"),
4827                (
4828                    Csharp::ParenthesizedExpression as u16,
4829                    1,
4830                    "`paren`'s parenthesised guard",
4831                ),
4832                // By the call's *argument list*, which carries no
4833                // numeric-suffix alias, rather than by
4834                // `invocation_expression`, which has three
4835                // (`InvocationExpression` / `2` / `3`) and would pin this
4836                // anchor to whichever one the pinned grammar happens to
4837                // emit. `call` and `paren` hold the only two argument
4838                // lists in the fixture — `IsEven(int x)` is a
4839                // `parameter_list`.
4840                (Csharp::ArgumentList as u16, 2, "the `IsEven` calls"),
4841            ],
4842        );
4843        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
4844            assert_members_score(
4845                &space.spaces[0],
4846                &[
4847                    ("IsEven", 0, 1),
4848                    ("tok", 3, 4),
4849                    ("cmp", 3, 4),
4850                    ("call", 3, 4),
4851                    ("paren", 3, 4),
4852                    // Was 2 — the guard spelling nothing counted.
4853                    // Cyclomatic 5 rather than 4 because `??` is a
4854                    // decision there on top of the guard clause.
4855                    ("nullc", 3, 5),
4856                    ("none", 2, 3),
4857                ],
4858            );
4859        });
4860    }
4861
4862    // The bare type test `x is int` is `is_expression` (391); only once
4863    // a pattern is involved (`x is int y`, `x is null`) does the grammar
4864    // emit `is_pattern_expression` (392). They are distinct kinds, and
4865    // `csharp_bool_terminal_kinds!()` listed only the second, so the two
4866    // spellings of one test disagreed: `if (x is int)` scored zero
4867    // conditions against a cyclomatic decision of one while
4868    // `if (x is int y)` scored one.
4869    //
4870    // Found by the whole-branch review of this batch, and it falsified
4871    // #1422's own claim in the arm above that a guard "scores one
4872    // however it is spelled" — `when x is int` read 1 where
4873    // `when IsEven(x)` read 2, which is the spelling-dependence that
4874    // fix exists to remove. It is the C# half of the gap #1421 closed
4875    // for Kotlin by adding `IsExpression | InExpression` there. Both
4876    // languages' type tests have since moved out of the terminal set to
4877    // an unconditional arm (#1461), which leaves every number here
4878    // unchanged — the slot no longer counts the test and the arm does.
4879    //
4880    // The plain `if` members are the control that keeps this honest: a
4881    // guard-only fixture could be satisfied by a guard-specific rule,
4882    // and the defect was never guard-specific. `q` and `iq` are the
4883    // already-correct `is_pattern_expression` twins, so a regression
4884    // that reintroduced the asymmetry fails on the pair rather than on
4885    // an absolute number.
4886    #[cfg(feature = "csharp")]
4887    #[test]
4888    fn csharp_bare_is_type_test_scores_one_condition() {
4889        let src = "class A {
4890                static bool IsEven(int x) { return true; }
4891                int g(object x) => x switch { object o when x is int => 1, _ => 0 };
4892                int gq(object x) => x switch { object o when x is int y => 1, _ => 0 };
4893                int gc(object x) => x switch { object o when IsEven(1) => 1, _ => 0 };
4894                int p(object x) { if (x is int) { return 1; } return 0; }
4895                int q(object x) { if (x is int y) { return 1; } return 0; }
4896            }";
4897        assert_csharp_fixture_spells(
4898            src,
4899            &[
4900                (Csharp::IsExpression as u16, 2, "the bare `is` type tests"),
4901                (
4902                    Csharp::IsPatternExpression as u16,
4903                    2,
4904                    "the designation-pattern twins",
4905                ),
4906                (Csharp::WhenClause as u16, 3, "`when` guards"),
4907            ],
4908        );
4909        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
4910            assert_members_score(
4911                &space.spaces[0],
4912                &[
4913                    ("IsEven", 0, 1),
4914                    // Level with `gq` and `gc`: every guard spelling
4915                    // scores one, which is what #1422 claims.
4916                    ("g", 2, 3),
4917                    ("gq", 2, 3),
4918                    ("gc", 2, 3),
4919                    // Level with `q`: the two spellings of one type
4920                    // test agree outside a guard too.
4921                    ("p", 1, 2),
4922                    ("q", 1, 2),
4923                ],
4924            );
4925        });
4926    }
4927
4928    // The null-forgiving `b!` is transparent: it scores whatever the
4929    // operand it wraps scores (#1463).
4930    //
4931    // `postfix_unary_expression` was in neither
4932    // `csharp_bool_terminal_kinds!()` nor the wrapper peel, so every
4933    // spelling below scored **zero** conditions against a cyclomatic
4934    // decision of one, while the bare `b` control scored one. In a
4935    // codebase with nullable reference types enabled the suffix is
4936    // everywhere, so the gap is not an exotic corner.
4937    //
4938    // Both walker paths, per `.claude/rules/grammar-dispatch.md` §11:
4939    // `p*` go through the `if` predicate slot
4940    // (`csharp_count_condition`) and `c*` through the `&&` chain slot
4941    // (`csharp_count_unary_conditions`), which reach
4942    // `csharp_inspect_container` independently. A fixture of only one
4943    // reads correct with the other path dead.
4944    //
4945    // The controls are load-bearing in both directions. `p` / `c` pin
4946    // what the wrapper must score, so a regression fails on the pair
4947    // rather than on an absolute number; and the cyclomatic column pins
4948    // that only `conditions` moved — the suffix is not a decision in any
4949    // language, so a fix that moved both together would be a different
4950    // bug.
4951    #[cfg(feature = "csharp")]
4952    #[test]
4953    fn csharp_null_forgiving_operand_scores_like_its_operand() {
4954        let src = "class A {
4955                int p(bool b) { if (b) { return 1; } return 0; }
4956                int pn(bool b) { if (b!) { return 1; } return 0; }
4957                int pp(bool b) { if ((b!)) { return 1; } return 0; }
4958                int pd(bool b) { if (b!!) { return 1; } return 0; }
4959                int pb(bool b) { if (!b!) { return 1; } return 0; }
4960                int c(bool b) { if (b && b) { return 1; } return 0; }
4961                int cn(bool b) { if (b! && b) { return 1; } return 0; }
4962                int cp(bool b) { if ((b!) && b) { return 1; } return 0; }
4963            }";
4964        assert_csharp_fixture_spells(
4965            src,
4966            &[
4967                // Seven, not five: `b!!` is two nested nodes, and each
4968                // chain member carries one. Trimming a `!` out of any
4969                // member fails here by name instead of turning that
4970                // member into a silent copy of its control.
4971                (
4972                    Csharp::PostfixUnaryExpression as u16,
4973                    7,
4974                    "the null-forgiving suffixes",
4975                ),
4976                // The `if (…)` parens are anonymous tokens rather than
4977                // nodes in this grammar, so these two are `pp`'s and
4978                // `cp`'s explicit ones — the wrappers that make the peel
4979                // chain rather than peel once.
4980                (
4981                    Csharp::ParenthesizedExpression as u16,
4982                    2,
4983                    "the explicit parenthesised spellings",
4984                ),
4985                (Csharp::PrefixUnaryExpression as u16, 1, "`pb`'s `!`"),
4986            ],
4987        );
4988        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
4989            assert_members_score(
4990                &space.spaces[0],
4991                &[
4992                    ("p", 1, 2),
4993                    ("pn", 1, 2),
4994                    ("pp", 1, 2),
4995                    ("pd", 1, 2),
4996                    ("pb", 1, 2),
4997                    ("c", 2, 3),
4998                    ("cn", 2, 3),
4999                    ("cp", 2, 3),
5000                ],
5001            );
5002        });
5003    }
5004
5005    // The null-forgiving operand is read by position, but from the end
5006    // the grammar cannot move: a `postfix_unary_expression` *starts* at
5007    // its operand, so `child(0)` is the operand whatever `extra` sits
5008    // between it and the operator. The operator is identified by
5009    // membership rather than by index for the same reason.
5010    //
5011    // The other two wrappers have no such luck and are pinned here as a
5012    // *measured* gap rather than left to be discovered: the C# grammar
5013    // gives `parenthesized_expression`, `prefix_unary_expression` and
5014    // `postfix_unary_expression` an empty `fields` map in
5015    // node-types.json, so the field read Kotlin's and Groovy's
5016    // equivalents use is unavailable and `child(1)` is a comment
5017    // whenever one is written there. That is #1455, which predates
5018    // #1463 and is a separate change; what this test adds is that the
5019    // new arm does not join it.
5020    #[cfg(feature = "csharp")]
5021    #[test]
5022    fn csharp_null_forgiving_operand_survives_an_interposed_comment() {
5023        let src = "class A {
5024                int p(bool b) { if (b!) { return 1; } return 0; }
5025                int pc(bool b) { if (b /*c*/ !) { return 1; } return 0; }
5026                int n(bool b) { if (!b) { return 1; } return 0; }
5027                int nc(bool b) { if (! /*c*/ b) { return 1; } return 0; }
5028                int r(bool b) { if ((b)) { return 1; } return 0; }
5029                int rc(bool b) { if (( /*c*/ b)) { return 1; } return 0; }
5030            }";
5031        assert_csharp_fixture_spells(
5032            src,
5033            &[
5034                (
5035                    Csharp::Comment as u16,
5036                    3,
5037                    "the interposed comments — without them every row is its own control",
5038                ),
5039                (Csharp::PostfixUnaryExpression as u16, 2, "`p` and `pc`"),
5040                (Csharp::PrefixUnaryExpression as u16, 2, "`n` and `nc`"),
5041                (
5042                    Csharp::ParenthesizedExpression as u16,
5043                    2,
5044                    "`r` and `rc` — the `if` parens are anonymous tokens",
5045                ),
5046            ],
5047        );
5048        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5049            assert_members_score(
5050                &space.spaces[0],
5051                &[
5052                    ("p", 1, 2),
5053                    // The row #1463 fixes and the only one of the three
5054                    // pairs that agrees with its control.
5055                    ("pc", 1, 2),
5056                    ("n", 1, 2),
5057                    // #1455: `child(1)` of the prefix wrapper is the
5058                    // comment. Delete this expectation, not the row,
5059                    // when that issue is fixed.
5060                    ("nc", 0, 2),
5061                    ("r", 1, 2),
5062                    // #1455 again, through the paren wrapper.
5063                    ("rc", 0, 2),
5064                ],
5065            );
5066        });
5067    }
5068
5069    // The statement `switch` reaches the same `when_clause` kind through
5070    // a different parent — `switch_section` rather than
5071    // `switch_expression_arm` — which makes it an independent path in
5072    // the sense of `.claude/rules/grammar-dispatch.md` §11: the arms
5073    // themselves are counted by two different owners (a `Case` token
5074    // here, the `SwitchExpressionArm` node there), so a guard rule
5075    // written against one shape could be dead for the other and every
5076    // expression-form fixture would still read correct.
5077    #[cfg(feature = "csharp")]
5078    #[test]
5079    fn csharp_statement_switch_section_guard_counts() {
5080        let src = "class A {
5081                static bool IsEven(int x) { return true; }
5082                int guarded(int x) {
5083                    switch (x) {
5084                        case > 0 when IsEven(x): return 1;
5085                        case > 0: return 2;
5086                        default: return 3;
5087                    }
5088                }
5089                int plain(int x) {
5090                    switch (x) {
5091                        case > 2: return 1;
5092                        case > 0: return 2;
5093                        default: return 3;
5094                    }
5095                }
5096            }";
5097        assert_csharp_fixture_spells(
5098            src,
5099            &[
5100                (Csharp::WhenClause as u16, 1, "the `case … when` guard"),
5101                (Csharp::SwitchSection as u16, 6, "switch sections"),
5102                (
5103                    Csharp::SwitchExpressionArm as u16,
5104                    0,
5105                    "expression arms (this fixture is the statement form)",
5106                ),
5107            ],
5108        );
5109        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5110            assert_members_score(
5111                &space.spaces[0],
5112                &[("IsEven", 0, 1), ("guarded", 3, 4), ("plain", 2, 3)],
5113            );
5114        });
5115    }
5116
5117    // The sibling #1422's issue body does not mention, measured to have
5118    // the identical defect: `catch (E e) when (…)` is a second guard
5119    // spelling on its own `catch_filter_clause` kind, and it scored
5120    // conditions 3 / 2 / 2 for the comparison / call spellings and the
5121    // unfiltered control while `cyclomatic()` sat at 2 for all three.
5122    //
5123    // ABC runs one above `cyclomatic() - 1` on every row, guarded and
5124    // unguarded alike, and that offset is not this fix's: C# ABC counts
5125    // the `Try` token as a condition (Fitzpatrick Rule 5, the same
5126    // family as `else`) where cyclomatic counts only the `catch`. The
5127    // guard moves both tiers by exactly one, which is what `none`
5128    // anchors — the offset is a constant here, not something a guard
5129    // introduces.
5130    //
5131    // `paren` carries the *double* parenthesis on purpose. The grammar
5132    // spells `catch_filter_clause`'s own parentheses as anonymous tokens
5133    // the way `if_statement` does, so `when (IsEven(x))` hands the slot a
5134    // bare `invocation_expression` and never reaches the
5135    // `CatchFilterClause` seed in `csharp_inspect_container`; only a
5136    // second pair of parentheses produces a `parenthesized_expression`
5137    // there.
5138    #[cfg(feature = "csharp")]
5139    #[test]
5140    fn csharp_catch_filter_guard_scores_one_condition_however_spelled() {
5141        let src = "class A {
5142                static bool IsEven(int x) { return true; }
5143                int tok(int x) { try { return 1; } catch (System.Exception e) when (x % 2 == 0) { return 2; } }
5144                int cmp(int x) { try { return 1; } catch (System.Exception e) when (x > 0) { return 2; } }
5145                int call(int x) { try { return 1; } catch (System.Exception e) when (IsEven(x)) { return 2; } }
5146                int paren(int x) { try { return 1; } catch (System.Exception e) when ((IsEven(x))) { return 2; } }
5147                int none(int x) { try { return 1; } catch (System.Exception e) { return 2; } }
5148            }";
5149        assert_csharp_fixture_spells(
5150            src,
5151            &[
5152                (
5153                    Csharp::CatchFilterClause as u16,
5154                    4,
5155                    "`catch … when` filters",
5156                ),
5157                (Csharp::CatchClause as u16, 5, "catch clauses"),
5158                (Csharp::WhenClause as u16, 0, "switch guards (none here)"),
5159                (
5160                    Csharp::ParenthesizedExpression as u16,
5161                    1,
5162                    "`paren`'s inner parentheses",
5163                ),
5164            ],
5165        );
5166        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5167            assert_members_score(
5168                &space.spaces[0],
5169                &[
5170                    ("IsEven", 0, 1),
5171                    ("tok", 3, 3),
5172                    ("cmp", 3, 3),
5173                    ("call", 3, 3),
5174                    ("paren", 3, 3),
5175                    ("none", 2, 2),
5176                ],
5177            );
5178        });
5179    }
5180
5181    // A guarded discard is where the two rules meet. `_ when g =>` is
5182    // already exempt from the `default:` exclusion —
5183    // `csharp_switch_expression_arm_is_bare_discard` looks for a
5184    // `when_clause` for exactly this reason — so after #1422 it scores
5185    // twice: once as an arm that is no longer unconditional, once for
5186    // the guard that makes it conditional. `bare` is the control that
5187    // keeps the exclusion itself pinned: drop the guard and the arm goes
5188    // back to costing nothing.
5189    #[cfg(feature = "csharp")]
5190    #[test]
5191    fn csharp_guarded_discard_arm_scores_arm_and_guard() {
5192        let src = "class A {
5193                static bool IsEven(int x) { return true; }
5194                int guarded(int x) => x switch { > 0 => 1, _ when IsEven(x) => 2, _ => 3 };
5195                int bare(int x) => x switch { > 0 => 1, _ => 3 };
5196            }";
5197        assert_csharp_fixture_spells(
5198            src,
5199            &[
5200                (Csharp::WhenClause as u16, 1, "the guarded discard"),
5201                (Csharp::Discard as u16, 3, "discard patterns"),
5202            ],
5203        );
5204        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5205            assert_members_score(
5206                &space.spaces[0],
5207                &[("IsEven", 0, 1), ("guarded", 3, 4), ("bare", 1, 2)],
5208            );
5209        });
5210    }
5211
5212    // A comment inside the guard must not cost it its condition.
5213    // tree-sitter `extra`s are *named* nodes, so `comment` can be the
5214    // first named child of either guard clause — and a slot that read
5215    // only the first named child scored `when /*c*/ IsEven(x)` as 1
5216    // where `when IsEven(x)` scores 2, silently reinstating the very
5217    // spelling-dependence #1422 removes and breaking the §8 parity the
5218    // fix establishes. Found in review of #1422 before it shipped.
5219    //
5220    // This is the C# instance of the class #1181 fixed for the ternary,
5221    // where a comment between a token and its operand shifted every
5222    // positional read. Each commented member is asserted against the
5223    // same literal pair as its uncommented twin in the same table, so a
5224    // regression that moved only the commented spelling fails here; the
5225    // literals themselves are pinned by
5226    // `csharp_switch_arm_guard_scores_one_condition_however_spelled` and
5227    // its catch-filter sibling.
5228    #[cfg(feature = "csharp")]
5229    #[test]
5230    fn csharp_guard_keeps_its_condition_across_a_comment() {
5231        let src = "class A {
5232                static bool IsEven(int x) => true;
5233                int plain(int x) => x switch { > 0 when IsEven(x) => 1, _ => 0 };
5234                int cmt(int x) => x switch { > 0 when /*c*/ IsEven(x) => 1, _ => 0 };
5235                int cplain(int x) { try { return 1; } catch (System.Exception e) when (IsEven(x)) { return 2; } }
5236                int ccmt(int x) { try { return 1; } catch (System.Exception e) when (/*c*/ IsEven(x)) { return 2; } }
5237            }";
5238        assert_csharp_fixture_spells(
5239            src,
5240            &[
5241                (Csharp::Comment as u16, 2, "the in-guard comments"),
5242                (Csharp::WhenClause as u16, 2, "switch guards"),
5243                (Csharp::CatchFilterClause as u16, 2, "catch filters"),
5244            ],
5245        );
5246        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5247            assert_members_score(
5248                &space.spaces[0],
5249                &[
5250                    ("IsEven", 0, 1),
5251                    ("plain", 2, 3),
5252                    ("cmt", 2, 3),
5253                    ("cplain", 3, 3),
5254                    ("ccmt", 3, 3),
5255                ],
5256            );
5257        });
5258    }
5259
5260    // Why the guard is a condition *slot* and not a flat +1: a compound
5261    // guard keeps its sub-structure. `when a > 1 && b < 2` scores the
5262    // two comparisons through the token arm and nothing extra from the
5263    // slot, because a `binary_expression` is not one of
5264    // `csharp_bool_terminal_kinds!()` — so it reads one above `single`'s
5265    // lone comparison rather than collapsing to the same number, and
5266    // cyclomatic agrees because it counts the `&&`.
5267    #[cfg(feature = "csharp")]
5268    #[test]
5269    fn csharp_compound_guard_keeps_its_sub_structure() {
5270        let src = "class A {
5271                int compound(int a, int b) => a switch { > 0 when a > 1 && b < 2 => 1, _ => 0 };
5272                int single(int a, int b) => a switch { > 0 when a > 1 => 1, _ => 0 };
5273            }";
5274        assert_csharp_fixture_spells(
5275            src,
5276            &[
5277                (Csharp::WhenClause as u16, 2, "`when` guards"),
5278                (Csharp::AMPAMP as u16, 1, "`compound`'s `&&`"),
5279                (Csharp::LT as u16, 1, "`compound`'s `<`"),
5280            ],
5281        );
5282        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5283            assert_members_score(&space.spaces[0], &[("compound", 3, 4), ("single", 2, 3)]);
5284        });
5285    }
5286
5287    // #1275: tree-sitter-c-sharp spells `int?`, `where T : class?`, the
5288    // ternary and `a?.b` with one and the same bare `?` token, so the
5289    // unguarded `QMARK` arm scored the two type-syntax forms as
5290    // decisions.
5291    //
5292    // The fixture carries two `nullable_type` `?`, one
5293    // `type_parameter_constraint` `?`, and one real ternary over a real
5294    // `>` comparison, so every way of getting the gate wrong lands on
5295    // its own number: 5 pre-fix, 3 if only `NullableType` is denied, 4
5296    // if only `TypeParameterConstraint` is, 1 if the gate swallows the
5297    // genuine ternary, 0 if the fixture stops parsing. Asserting 0 on a
5298    // nullable-only body would have been vacuous — an unparsable file
5299    // scores 0 too.
5300    #[cfg(feature = "csharp")]
5301    #[test]
5302    fn csharp_nullable_type_syntax_is_not_a_condition() {
5303        check_metrics::<CsharpParser>(
5304            "class A {
5305                int M<T>(int? p, int a, int b) where T : class? {
5306                    int? q = null;
5307                    return a > b ? 1 : 2;
5308                }
5309            }",
5310            "foo.cs",
5311            |metric| {
5312                assert_eq!(metric.abc.conditions_sum(), 2);
5313            },
5314        );
5315    }
5316
5317    // The polarity pin for #1275's C# half. `csharp_count_token_condition`
5318    // denies the two type-syntax parents rather than allowing the two
5319    // decision-bearing ones, so that `a?.b` and `a?[0]` keep counting by
5320    // construction: both spell their operator as the same bare `?` under
5321    // `conditional_access_expression`, and C# cyclomatic counts that node
5322    // (`safe_navigation_chain_parity` pins C# at one decision per
5323    // operator alongside every other safe-navigation language).
5324    //
5325    // This test is what stops a later "make all three languages
5326    // consistent" pass from flipping C# to a `ConditionalExpression`
5327    // allowlist: that would silently drop both `?` counts here while
5328    // every other ABC test still passed. The third condition is the
5329    // `??`, which #1459 added to close the gap this comment used to
5330    // record as out of scope for #1275 — so a flipped `QMARK` polarity
5331    // now reads 1 rather than 0, and the discrimination is unchanged.
5332    #[cfg(feature = "csharp")]
5333    #[test]
5334    fn csharp_conditional_access_still_counts_as_a_condition() {
5335        let src = "class A {
5336                object M(string s, int[] xs) {
5337                    return s?.Length ?? xs?[0];
5338                }
5339            }";
5340        // The total is now fed by two token arms rather than one, so it
5341        // carries the per-source claim only alongside these counts: 3 is
5342        // two bare `?` plus one `??`, and without them a `QMARK` arm
5343        // that had stopped counting `a?[0]` while `??` gained a second
5344        // count elsewhere would still read 3.
5345        assert_csharp_fixture_spells(
5346            src,
5347            &[
5348                (Csharp::QMARK as u16, 2, "the `?.` and `?[` operators"),
5349                (Csharp::QMARKQMARK as u16, 1, "the `??`"),
5350            ],
5351        );
5352        check_metrics::<CsharpParser>(src, "foo.cs", |metric| {
5353            assert_eq!(metric.abc.conditions_sum(), 3);
5354        });
5355    }
5356
5357    // The `QMARKQMARK` arm away from any guard, where nothing else can
5358    // supply the count (#1459). `coalesce`'s body holds no comparison,
5359    // no call and no control-flow keyword, so its one condition is the
5360    // `??` and nothing else; it read 0 before the arm while C#
5361    // cyclomatic scored the decision, which is the direction ABC is not
5362    // allowed to sit on.
5363    //
5364    // `plain` is the control that keeps this from being an assertion
5365    // about `return` or about the method shape: same body without the
5366    // operator, 0 conditions and 1 decision.
5367    #[cfg(feature = "csharp")]
5368    #[test]
5369    fn csharp_null_coalescing_is_a_condition() {
5370        let src = "class A {
5371                int coalesce(int? x) { return x ?? 0; }
5372                int plain(int x) { return x; }
5373            }";
5374        assert_csharp_fixture_spells(src, &[(Csharp::QMARKQMARK as u16, 1, "the `??`")]);
5375        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5376            assert_members_score(&space.spaces[0], &[("coalesce", 1, 2), ("plain", 0, 1)]);
5377        });
5378    }
5379
5380    // The other direction of #1275's C# gate, isolated: narrowing
5381    // `QMARK` must not swallow a real ternary. The fixture carries no
5382    // comparison operator at all, so the ternary is the only thing that
5383    // can put the count above the walker's own contribution — in the
5384    // test above, a surviving `>` would have masked an over-suppressed
5385    // `?` at 1 instead of 0 (grammar-dispatch §11). A nullable
5386    // parameter keeps the denied production live in the same parse, so
5387    // one fixture proves both directions: 3 pre-fix, 2 after, 1 if the
5388    // allowlist/denylist is aimed at `ConditionalExpression`.
5389    //
5390    // The 2 is the `?` token plus the ternary's condition slot, which
5391    // `csharp_walk_for_conditions` counts as a Fitzpatrick unary
5392    // condition. That is why this is not written as an
5393    // `assert_deepest_conditions_match_cyclomatic` parity pin the way
5394    // the Java `<` / `>` sibling is: C# cyclomatic scores this method 1
5395    // decision, and the divergence is the pre-existing unary-condition
5396    // rule, not the `?` gate.
5397    #[cfg(feature = "csharp")]
5398    #[test]
5399    fn csharp_ternary_still_counts_alongside_nullable_types() {
5400        check_metrics::<CsharpParser>(
5401            "class A {
5402                int M(int? n, bool c) {
5403                    return c ? 1 : 2;
5404                }
5405            }",
5406            "foo.cs",
5407            |metric| {
5408                assert_eq!(metric.abc.conditions_sum(), 2);
5409            },
5410        );
5411    }
5412
5413    #[cfg(feature = "csharp")]
5414    #[test]
5415    fn csharp_aliased_invocation_expression_branches() {
5416        // Regression for issue #94 (lesson #2): the C# grammar emits three
5417        // aliased `kind_id`s for `invocation_expression`. Code that matches
5418        // only the unsuffixed `Csharp::InvocationExpression` undercounts ABC
5419        // branches whenever the AST emits an aliased variant. The three
5420        // method calls live in `M`, so the per-method maximum (visible at
5421        // the unit-space aggregate as `branches_max`) must be 3.
5422        check_metrics::<CsharpParser>(
5423            "class A {
5424                void M() {
5425                    System.Console.WriteLine(1);
5426                    System.Console.WriteLine(2);
5427                    System.Console.WriteLine(3);
5428                }
5429            }",
5430            "foo.cs",
5431            |metric| {
5432                assert_eq!(metric.abc.branches_max(), 3);
5433                assert_eq!(metric.abc.conditions_max(), 0);
5434            },
5435        );
5436    }
5437
5438    #[cfg(feature = "php")]
5439    #[test]
5440    fn php_zero_abc() {
5441        check_metrics::<PhpParser>("<?php\n", "foo.php", |metric| {
5442            assert_eq!(metric.abc.assignments_sum(), 0);
5443            assert_eq!(metric.abc.branches_sum(), 0);
5444            assert_eq!(metric.abc.conditions_sum(), 0);
5445            insta::assert_json_snapshot!(metric.abc);
5446        });
5447    }
5448
5449    #[cfg(feature = "php")]
5450    #[test]
5451    fn php_simple_assignment() {
5452        check_metrics::<PhpParser>(
5453            "<?php
5454function f(): void {
5455    $a = 1;
5456    $b = 2;
5457}",
5458            "foo.php",
5459            |metric| insta::assert_json_snapshot!(metric.abc),
5460        );
5461    }
5462
5463    #[cfg(feature = "php")]
5464    #[test]
5465    fn php_augmented_assignment() {
5466        check_metrics::<PhpParser>(
5467            "<?php
5468function f(int $x): int {
5469    $a = 0;
5470    $a += $x;
5471    $a -= 1;
5472    $a *= 2;
5473    return $a;
5474}",
5475            "foo.php",
5476            |metric| insta::assert_json_snapshot!(metric.abc),
5477        );
5478    }
5479
5480    #[cfg(feature = "php")]
5481    #[test]
5482    fn php_const_excluded() {
5483        // Constant declarations and enum cases are NOT counted as
5484        // assignments — they declare immutable values.
5485        check_metrics::<PhpParser>(
5486            "<?php
5487class A {
5488    const PI = 3.14;
5489    const E = 2.71;
5490}
5491enum Color {
5492    case Red;
5493    case Green;
5494}",
5495            "foo.php",
5496            |metric| insta::assert_json_snapshot!(metric.abc),
5497        );
5498    }
5499
5500    #[cfg(feature = "php")]
5501    #[test]
5502    fn php_function_call() {
5503        check_metrics::<PhpParser>(
5504            "<?php
5505function f(): void {
5506    foo();
5507    bar(1, 2);
5508}",
5509            "foo.php",
5510            |metric| insta::assert_json_snapshot!(metric.abc),
5511        );
5512    }
5513
5514    #[cfg(feature = "php")]
5515    #[test]
5516    fn php_method_call() {
5517        check_metrics::<PhpParser>(
5518            "<?php
5519function f($obj): void {
5520    $obj->m1();
5521    $obj->m2(1);
5522}",
5523            "foo.php",
5524            |metric| insta::assert_json_snapshot!(metric.abc),
5525        );
5526    }
5527
5528    #[cfg(feature = "php")]
5529    #[test]
5530    fn php_static_call() {
5531        check_metrics::<PhpParser>(
5532            "<?php
5533function f(): void {
5534    Foo::bar();
5535    Foo::baz(1);
5536}",
5537            "foo.php",
5538            |metric| insta::assert_json_snapshot!(metric.abc),
5539        );
5540    }
5541
5542    #[cfg(feature = "php")]
5543    #[test]
5544    fn php_nullsafe_call() {
5545        check_metrics::<PhpParser>(
5546            "<?php
5547function f($obj): void {
5548    $obj?->m1();
5549    $obj?->m2(1);
5550}",
5551            "foo.php",
5552            |metric| insta::assert_json_snapshot!(metric.abc),
5553        );
5554    }
5555
5556    #[cfg(feature = "php")]
5557    #[test]
5558    fn php_object_creation() {
5559        check_metrics::<PhpParser>(
5560            "<?php
5561function f(): void {
5562    new Foo();
5563    new Bar(1);
5564}",
5565            "foo.php",
5566            |metric| insta::assert_json_snapshot!(metric.abc),
5567        );
5568    }
5569
5570    #[cfg(feature = "php")]
5571    #[test]
5572    fn php_comparison_eq() {
5573        check_metrics::<PhpParser>(
5574            "<?php
5575function f(int $a, int $b): bool {
5576    return $a == $b || $a != $b;
5577}",
5578            "foo.php",
5579            |metric| insta::assert_json_snapshot!(metric.abc),
5580        );
5581    }
5582
5583    #[cfg(feature = "php")]
5584    #[test]
5585    fn php_comparison_strict() {
5586        check_metrics::<PhpParser>(
5587            "<?php
5588function f(int $a, int $b): bool {
5589    return $a === $b || $a !== $b;
5590}",
5591            "foo.php",
5592            |metric| insta::assert_json_snapshot!(metric.abc),
5593        );
5594    }
5595
5596    #[cfg(feature = "php")]
5597    #[test]
5598    fn php_spaceship() {
5599        check_metrics::<PhpParser>(
5600            "<?php
5601function f(int $a, int $b): int {
5602    return $a <=> $b;
5603}",
5604            "foo.php",
5605            |metric| insta::assert_json_snapshot!(metric.abc),
5606        );
5607    }
5608
5609    #[cfg(feature = "php")]
5610    #[test]
5611    fn php_instanceof() {
5612        check_metrics::<PhpParser>(
5613            "<?php
5614function f($x): bool {
5615    return $x instanceof Foo;
5616}",
5617            "foo.php",
5618            |metric| insta::assert_json_snapshot!(metric.abc),
5619        );
5620    }
5621
5622    #[cfg(feature = "php")]
5623    #[test]
5624    fn php_complex_function() {
5625        // One snippet exercising A, B, C buckets together.
5626        check_metrics::<PhpParser>(
5627            "<?php
5628function f(int $a, int $b): int {
5629    $sum = $a + $b;
5630    $prod = $a * $b;
5631    if ($sum > 0 && $prod === 0) {
5632        return foo($sum);
5633    }
5634    return bar()->double();
5635}",
5636            "foo.php",
5637            |metric| insta::assert_json_snapshot!(metric.abc),
5638        );
5639    }
5640
5641    #[cfg(feature = "php")]
5642    #[test]
5643    fn php_if_boolean_literal_condition() {
5644        check_metrics::<PhpParser>(
5645            "<?php\n\
5646             function f() {\n\
5647             \x20   if (true) {}                 // +1c\n\
5648             \x20   if (!false) {}               // +1c\n\
5649             \x20   while (true) {}              // +1c\n\
5650             \x20   do {} while (false);         // +1c\n\
5651             }\n",
5652            "foo.php",
5653            |metric| {
5654                assert_eq!(metric.abc.conditions_sum(), 4);
5655                insta::assert_json_snapshot!(metric.abc);
5656            },
5657        );
5658    }
5659
5660    #[cfg(feature = "php")]
5661    #[test]
5662    fn php_methods_arguments_with_conditions() {
5663        check_metrics::<PhpParser>(
5664            "<?php\n\
5665             function f($a, $b) {\n\
5666             \x20   m($a, $b);                   // +1b\n\
5667             \x20   m(!$a, !$b);                 // +1b +2c\n\
5668             }\n",
5669            "foo.php",
5670            |metric| {
5671                assert_eq!(metric.abc.branches_sum(), 2);
5672                assert_eq!(metric.abc.conditions_sum(), 2);
5673                insta::assert_json_snapshot!(metric.abc);
5674            },
5675        );
5676    }
5677
5678    #[cfg(feature = "php")]
5679    #[test]
5680    fn php_return_with_conditions() {
5681        check_metrics::<PhpParser>(
5682            "<?php\n\
5683             function m1($z) { return !($z >= 0); }\n\
5684             function m2($x) { return (((!$x))); }\n\
5685             function m3($x, $y) { return $x && $y; }\n",
5686            "foo.php",
5687            |metric| {
5688                // m1: `>=` (1). m2: walker unwraps to $x (1).
5689                // m3: `&&` walker counts both (2). Sum: 4.
5690                assert_eq!(metric.abc.conditions_sum(), 4);
5691                insta::assert_json_snapshot!(metric.abc);
5692            },
5693        );
5694    }
5695
5696    #[cfg(feature = "php")]
5697    #[test]
5698    fn php_name2_hidden_rule_drift_marker() {
5699        // Drift marker (findings.md round-2 #3): `Php::Name2` maps
5700        // to the hidden grammar rule `_name`. At the pinned
5701        // tree-sitter-php version it is never emitted as a concrete
5702        // node — the visible `Name` (= 1) carries every name.
5703        // We list `Name2` defensively in `php_bool_terminal_kinds!()`
5704        // (lesson 34); if a future grammar bump promotes `_name`
5705        // to a visible rule, this assertion fails loudly.
5706        let src = "<?php\nfunction f($x) { if ($x) { foo($x); } }\n";
5707        let parser = PhpParser::new(
5708            src.as_bytes().to_vec(),
5709            &std::path::PathBuf::from("foo.php"),
5710            None,
5711        );
5712        assert!(!ast_has_kind_id(&parser, Php::Name2 as u16));
5713    }
5714
5715    #[cfg(feature = "php")]
5716    #[test]
5717    fn php_scoped_property_access_condition_counts() {
5718        // Regression for findings.md round-2 #1 (PHP):
5719        // `if (Config::$enabled) {}` parses with
5720        // `scoped_property_access_expression` as the condition
5721        // node (kind_id 333 at the pinned grammar version — the
5722        // `*2` alias). Pre-fix, neither `ScopedPropertyAccessExpression`
5723        // nor its alias was in `php_bool_terminal_kinds!()`. The
5724        // walker reached the access node, found it non-terminal,
5725        // and broke. Mirrors C#'s `MemberAccessExpression` rule
5726        // (lesson 19, #372).
5727        check_metrics::<PhpParser>(
5728            "<?php\n\
5729             class Config { public static $enabled = true; }\n\
5730             function f() { if (Config::$enabled) { } }\n",
5731            "foo.php",
5732            |metric| {
5733                assert_eq!(metric.abc.conditions_sum(), 1);
5734                insta::assert_json_snapshot!(metric.abc);
5735            },
5736        );
5737    }
5738
5739    #[cfg(feature = "php")]
5740    #[test]
5741    fn php_named_argument_unary_conditional_counts() {
5742        // Regression for the code-review finding: PHP 8 named-argument
5743        // syntax `m(name: !$a)` parses as `argument(name, ':',
5744        // unary_op_expression)`. Pre-fix, the count walker took
5745        // `argument.child(0)` (the name) and missed the value at the
5746        // last child. Now it picks the last named child as the value.
5747        check_metrics::<PhpParser>(
5748            "<?php\nfunction f($a) { m(name: !$a); }\n",
5749            "foo.php",
5750            |metric| {
5751                // 1 call (branch) + 1 unary-conditional named argument.
5752                assert_eq!(metric.abc.branches_sum(), 1);
5753                assert_eq!(metric.abc.conditions_sum(), 1);
5754                insta::assert_json_snapshot!(metric.abc);
5755            },
5756        );
5757    }
5758
5759    #[cfg(feature = "php")]
5760    #[test]
5761    fn php_low_precedence_keyword_logical_ops_trigger_walker() {
5762        // Regression: pre-fix, `$a or $b` reported 0 conditions
5763        // because the dispatcher only handled `AMPAMP|PIPEPIPE`,
5764        // skipping the PHP-specific `and` / `or` / `xor` keyword
5765        // forms even though they parse under the same
5766        // `binary_expression` shape.
5767        check_metrics::<PhpParser>(
5768            "<?php\n\
5769             function f($a, $b) {\n\
5770             \x20   return $a or $b;\n\
5771             }\n",
5772            "foo.php",
5773            |metric| {
5774                assert_eq!(metric.abc.conditions_sum(), 2);
5775                insta::assert_json_snapshot!(metric.abc);
5776            },
5777        );
5778    }
5779
5780    #[cfg(feature = "php")]
5781    #[test]
5782    fn php_if_multiple_conditions() {
5783        check_metrics::<PhpParser>(
5784            "<?php\n\
5785             function f($a, $b, $c, $d) {\n\
5786             \x20   if ($a || $b || $c || $d) {}     // +4c\n\
5787             \x20   if ($a && $b && $c) {}           // +3c\n\
5788             \x20   if (!$a && !$b) {}               // +2c\n\
5789             }\n",
5790            "foo.php",
5791            |metric| {
5792                assert_eq!(metric.abc.conditions_sum(), 9);
5793                insta::assert_json_snapshot!(metric.abc);
5794            },
5795        );
5796    }
5797
5798    #[cfg(feature = "php")]
5799    #[test]
5800    fn php_while_and_do_while_conditions() {
5801        check_metrics::<PhpParser>(
5802            "<?php\n\
5803             function f($a, $b) {\n\
5804             \x20   while ($a || $b) {}              // +2c\n\
5805             \x20   do {} while ($a && !$b);         // +2c\n\
5806             }\n",
5807            "foo.php",
5808            |metric| {
5809                assert_eq!(metric.abc.conditions_sum(), 4);
5810                insta::assert_json_snapshot!(metric.abc);
5811            },
5812        );
5813    }
5814
5815    #[cfg(feature = "php")]
5816    #[test]
5817    fn php_short_circuit_with_boolean_literal_operand() {
5818        check_metrics::<PhpParser>(
5819            "<?php\nfunction f($a) { return $a && true; }\n",
5820            "foo.php",
5821            |metric| {
5822                assert_eq!(metric.abc.conditions_sum(), 2);
5823                insta::assert_json_snapshot!(metric.abc);
5824            },
5825        );
5826    }
5827
5828    // Issue #1102, PHP half. See
5829    // `cpp_ternary_operand_slots_count_as_unary_conditions` for the
5830    // rule. PHP's ABC dispatcher has no `?`-token arm — the grammar
5831    // does emit the token, but the `conditional_expression` node is
5832    // what carries the tally's +1 — so the arm keeps that increment and
5833    // adds the operand slots.
5834    #[cfg(feature = "php")]
5835    #[test]
5836    fn php_ternary_operand_slots_count_as_unary_conditions() {
5837        // ternary (1) + condition `$a` (1) + `!$b` (1) + `!$c` (1) = 4.
5838        check_metrics::<PhpParser>(
5839            "<?php\nfunction f() { $x = $a ? !$b : !$c; }\n",
5840            "foo.php",
5841            |metric| assert_eq!(metric.abc.conditions_sum(), 4),
5842        );
5843        // No-double-count pin: ternary (1) + `>` (1) = 2, unchanged by
5844        // the fix.
5845        check_metrics::<PhpParser>(
5846            "<?php\nfunction f() { $x = ($a > 0) ? $b : -$b; }\n",
5847            "foo.php",
5848            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
5849        );
5850        // Nested (PHP 8 requires the inner ternary parenthesised): two
5851        // ternary nodes plus the two bare-variable conditions = 4.
5852        check_metrics::<PhpParser>(
5853            "<?php\nfunction f() { $x = $a ? ($b ? $c : $d) : $e; }\n",
5854            "foo.php",
5855            |metric| assert_eq!(metric.abc.conditions_sum(), 4),
5856        );
5857        // A negated condition is the only input reaching the walker's
5858        // `else` fallback — see the C++ sibling for why. ternary (1) +
5859        // `!$a` (1) = 2.
5860        check_metrics::<PhpParser>(
5861            "<?php\nfunction f() { $x = !$a ? $b : $c; }\n",
5862            "foo.php",
5863            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
5864        );
5865    }
5866
5867    // PHP's short ternary `$a ?: $b` elides the consequence, which the
5868    // grammar names `body` (not `consequence`) and marks optional. The
5869    // alternative lands at child(3), so addressing the slot by field
5870    // name rather than a fixed child(4) is what keeps `!$b` counted.
5871    #[cfg(feature = "php")]
5872    #[test]
5873    fn php_elided_ternary_body_still_walks_the_alternative() {
5874        // ternary (1) + condition `$a` (1) + `!$b` (1) = 3.
5875        check_metrics::<PhpParser>(
5876            "<?php\nfunction f() { $x = $a ?: !$b; }\n",
5877            "foo.php",
5878            |metric| assert_eq!(metric.abc.conditions_sum(), 3),
5879        );
5880    }
5881
5882    // Issue #1276, PHP half. The `for` header's condition slot was the
5883    // one condition slot `PhpCode::compute` never dispatched, so a
5884    // bare / negated / parenthesised loop condition scored zero while
5885    // the identical predicate in an `if` header scored one. Each
5886    // fixture below is a shape only the new arm can classify — a
5887    // comparison-shaped condition proves nothing here, because the `<`
5888    // token arm counts it either way (grammar-dispatch §11).
5889    #[cfg(feature = "php")]
5890    #[test]
5891    fn php_for_condition_slot_counts_unary_conditions() {
5892        // Bare variable: the whole condition, no operator token.
5893        check_metrics::<PhpParser>(
5894            "<?php\nfunction f($a) { for (; $a; ) {} }\n",
5895            "foo.php",
5896            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
5897        );
5898        // Negation: reaches the terminal through
5899        // `php_inspect_container`'s `!` unwrap.
5900        check_metrics::<PhpParser>(
5901            "<?php\nfunction f($a) { for (; !$a; ) {} }\n",
5902            "foo.php",
5903            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
5904        );
5905        // Parentheses: counts only because the `for_statement` parent
5906        // seeds `has_boolean_content`, the seed #1276 found dead.
5907        check_metrics::<PhpParser>(
5908            "<?php\nfunction f($a) { for (; ($a); ) {} }\n",
5909            "foo.php",
5910            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
5911        );
5912        // No-double-count pin: the `<` token arm already counted this
5913        // shape before the fix, and the walker must not add a second.
5914        // Two assignments (`$i = 0`, `$i++`) confirm the header parsed
5915        // as the three-clause form rather than degenerating.
5916        check_metrics::<PhpParser>(
5917            "<?php\nfunction f($n) { for ($i = 0; $i < $n; $i++) {} }\n",
5918            "foo.php",
5919            |metric| {
5920                assert_eq!(metric.abc.conditions_sum(), 1);
5921                assert_eq!(metric.abc.assignments_sum(), 2);
5922            },
5923        );
5924        // Empty condition: no `condition` field, no decision, zero.
5925        check_metrics::<PhpParser>(
5926            "<?php\nfunction f() { for (;;) { break; } }\n",
5927            "foo.php",
5928            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
5929        );
5930    }
5931
5932    // --- Kotlin ABC tests -------------------------------------------------
5933
5934    #[cfg(feature = "kotlin")]
5935    #[test]
5936    fn kotlin_empty_class() {
5937        check_metrics::<KotlinParser>("class C {}", "foo.kt", |metric| {
5938            assert_eq!(metric.abc.assignments_sum(), 0);
5939            assert_eq!(metric.abc.branches_sum(), 0);
5940            assert_eq!(metric.abc.conditions_sum(), 0);
5941            insta::assert_json_snapshot!(metric.abc);
5942        });
5943    }
5944
5945    #[cfg(feature = "kotlin")]
5946    #[test]
5947    fn kotlin_val_declarations_are_not_assignments() {
5948        // `val` introduces an immutable binding — the `=` initialising it
5949        // is not an assignment in the ABC sense.
5950        check_metrics::<KotlinParser>(
5951            "class C {
5952                val a: Int = 1
5953                val b: Int = 2
5954                val c: Int = 3
5955            }",
5956            "foo.kt",
5957            |metric| {
5958                assert_eq!(metric.abc.assignments_sum(), 0);
5959                assert_eq!(metric.abc.branches_sum(), 0);
5960                insta::assert_json_snapshot!(metric.abc);
5961            },
5962        );
5963    }
5964
5965    #[cfg(feature = "kotlin")]
5966    #[test]
5967    fn kotlin_var_declarations_count_assignment() {
5968        // `var` initialisers count as assignments (mutable binding).
5969        check_metrics::<KotlinParser>(
5970            "class C {
5971                var a: Int = 1
5972                var b: Int = 2
5973            }",
5974            "foo.kt",
5975            |metric| {
5976                assert_eq!(metric.abc.assignments_sum(), 2);
5977                insta::assert_json_snapshot!(metric.abc);
5978            },
5979        );
5980    }
5981
5982    #[cfg(feature = "kotlin")]
5983    #[test]
5984    fn kotlin_val_then_assignments_count() {
5985        // Regression for #455: a `val` initialiser must not suppress the
5986        // standalone `=` assignments that follow it. tree-sitter-kotlin
5987        // emits no `SEMI` token (even for explicit semicolons), so the
5988        // pre-#455 `SEMI`-cleared declaration stack never cleared and the
5989        // immutable-`val` sentinel leaked, reporting A=0 here.
5990        check_metrics::<KotlinParser>(
5991            "fun f() {
5992                val cfg = 0
5993                a = 1
5994                b = 2
5995            }",
5996            "foo.kt",
5997            |metric| {
5998                // val initialiser suppressed; `a = 1` and `b = 2` count.
5999                assert_eq!(metric.abc.assignments_sum(), 2);
6000                insta::assert_json_snapshot!(metric.abc);
6001            },
6002        );
6003    }
6004
6005    #[cfg(feature = "kotlin")]
6006    #[test]
6007    fn kotlin_var_then_assignments_count() {
6008        // Companion to the #455 regression: a `var` declaration leaves a
6009        // mutable-binding sentinel that *permits* the `=` — this path
6010        // accidentally masked the leak (its `Var` sentinel never suppressed
6011        // anything), so it must keep counting both the initialiser and the
6012        // following standalone assignments.
6013        check_metrics::<KotlinParser>(
6014            "fun f() {
6015                var cfg = 0
6016                a = 1
6017                b = 2
6018            }",
6019            "foo.kt",
6020            |metric| {
6021                // var initialiser (+1) plus `a = 1` and `b = 2` (+2).
6022                assert_eq!(metric.abc.assignments_sum(), 3);
6023                insta::assert_json_snapshot!(metric.abc);
6024            },
6025        );
6026    }
6027
6028    #[cfg(feature = "kotlin")]
6029    #[test]
6030    fn kotlin_augmented_assignments_count() {
6031        // Augmented operators (+=, -=, etc.) and ++/-- always count.
6032        check_metrics::<KotlinParser>(
6033            "fun m() {
6034                var x = 0
6035                x += 1
6036                x -= 2
6037                x *= 3
6038                x++
6039                --x
6040            }",
6041            "foo.kt",
6042            |metric| {
6043                // var declaration (var x = 0): +1
6044                // x += 1, x -= 2, x *= 3, x++, --x: +5
6045                assert_eq!(metric.abc.assignments_sum(), 6);
6046                insta::assert_json_snapshot!(metric.abc);
6047            },
6048        );
6049    }
6050
6051    #[cfg(feature = "kotlin")]
6052    #[test]
6053    fn kotlin_branches_call_expression() {
6054        check_metrics::<KotlinParser>(
6055            "fun m() {
6056                println(\"a\")
6057                println(\"b\")
6058                println(\"c\")
6059            }",
6060            "foo.kt",
6061            |metric| {
6062                assert_eq!(metric.abc.branches_sum(), 3);
6063                insta::assert_json_snapshot!(metric.abc);
6064            },
6065        );
6066    }
6067
6068    #[cfg(feature = "kotlin")]
6069    #[test]
6070    fn kotlin_object_construction_branch() {
6071        // Kotlin's object construction is just `Foo()` — a `CallExpression`.
6072        check_metrics::<KotlinParser>(
6073            "class P(val x: Int)
6074            fun m(): P = P(1)",
6075            "foo.kt",
6076            |metric| {
6077                assert_eq!(metric.abc.branches_sum(), 1);
6078                insta::assert_json_snapshot!(metric.abc);
6079            },
6080        );
6081    }
6082
6083    #[cfg(feature = "kotlin")]
6084    #[test]
6085    fn kotlin_comparisons_count_conditions() {
6086        check_metrics::<KotlinParser>(
6087            "fun m(a: Int, b: Int): Boolean {
6088                val r1 = a < b
6089                val r2 = a > b
6090                val r3 = a <= b
6091                val r4 = a >= b
6092                val r5 = a == b
6093                val r6 = a != b
6094                return r1 || r2 || r3 || r4 || r5 || r6
6095            }",
6096            "foo.kt",
6097            |metric| {
6098                // Six comparison operators in the `val` initialisers
6099                // (<, >, <=, >=, ==, !=) → 6, plus the six bare-identifier
6100                // operands of the `r1 || … || r6` return chain, each a
6101                // Fitzpatrick Rule 9 unary condition (issue #557) → 6.
6102                // Total 12. Before the Kotlin walker was wired the chain
6103                // operands were silently dropped and this read 6.
6104                assert_eq!(metric.abc.conditions_sum(), 12);
6105                insta::assert_json_snapshot!(metric.abc);
6106            },
6107        );
6108    }
6109
6110    #[cfg(feature = "kotlin")]
6111    #[test]
6112    fn kotlin_identity_equality_conditions() {
6113        // `===` / `!==` are referential equality in Kotlin; they count too.
6114        check_metrics::<KotlinParser>(
6115            "fun m(a: Any, b: Any): Boolean {
6116                return a === b || a !== b
6117            }",
6118            "foo.kt",
6119            |metric| {
6120                assert_eq!(metric.abc.conditions_sum(), 2);
6121                insta::assert_json_snapshot!(metric.abc);
6122            },
6123        );
6124    }
6125
6126    #[cfg(feature = "kotlin")]
6127    #[test]
6128    fn kotlin_else_branch_counts() {
6129        check_metrics::<KotlinParser>(
6130            "fun m(x: Int): Int {
6131                return if (x > 0) 1 else -1
6132            }",
6133            "foo.kt",
6134            |metric| {
6135                // condition: > (1) + else (1) = 2
6136                assert_eq!(metric.abc.conditions_sum(), 2);
6137                insta::assert_json_snapshot!(metric.abc);
6138            },
6139        );
6140    }
6141
6142    #[cfg(feature = "kotlin")]
6143    #[test]
6144    fn kotlin_when_entries_count() {
6145        check_metrics::<KotlinParser>(
6146            "fun m(x: Int): Int {
6147                return when (x) {
6148                    1 -> 10
6149                    2 -> 20
6150                    else -> 0
6151                }
6152            }",
6153            "foo.kt",
6154            |metric| {
6155                // Non-`else` WhenEntry arms count; the `else ->` fallback
6156                // arm does not (issue #456). Two case arms + zero for the
6157                // `else` arm = 2.
6158                assert_eq!(metric.abc.conditions_sum(), 2);
6159                insta::assert_json_snapshot!(metric.abc);
6160            },
6161        );
6162    }
6163
6164    // Pins the `else ->` exclusion directly: a `when` whose only fallback
6165    // is `else ->` must not count that arm. Revert-verified — gating the
6166    // `WhenEntry` arm on `!kotlin_when_entry_is_else` is what drops this
6167    // from 3 to 2 (issue #456, lesson 11). Mirrors the cyclomatic gate.
6168    #[cfg(feature = "kotlin")]
6169    #[test]
6170    fn kotlin_when_else_not_a_condition() {
6171        check_metrics::<KotlinParser>(
6172            "fun m(x: Int): Int {
6173                return when (x) { 1 -> 10; 2 -> 20; else -> 0 }
6174            }",
6175            "foo.kt",
6176            |metric| {
6177                // case `1 ->` (+1) + case `2 ->` (+1) + `else ->` (+0) = 2.
6178                assert_eq!(metric.abc.conditions_sum(), 2);
6179            },
6180        );
6181    }
6182
6183    // The Kotlin binding of the two assertions every #1421 fixture needs,
6184    // coupled on purpose. A per-member score table is worth only as much
6185    // as the anchor proving the fixture still spells what it claims to
6186    // (`.claude/rules/testing.md`, "Perturb the fixture as well as the
6187    // production line"), and the `when_subject` count is the anchor no
6188    // score can stand in for: a `when` that grew a subject scores its
6189    // entries the pre-#1421 way with every other row still satisfied.
6190    // Each fixture is one class, so the members are `spaces[0]`'s.
6191    #[cfg(feature = "kotlin")]
6192    #[track_caller]
6193    fn assert_kotlin_class_members(
6194        src: &str,
6195        kinds: &[(u16, usize, &str)],
6196        expected: &[(&str, u64, u64)],
6197    ) {
6198        assert_fixture_spells::<KotlinParser>(src, "foo.kt", kinds);
6199        check_func_space::<KotlinParser, _>(src, "foo.kt", |space| {
6200            assert_members_score(&space.spaces[0], expected);
6201        });
6202    }
6203
6204    // #1421's headline. A subject-less `when` arm's condition is an
6205    // ordinary boolean expression, so its comparison operator is already
6206    // an ABC condition through the token arms; the `WhenEntry` arm added
6207    // a second one on top, and `two` reported 4 against a decision count
6208    // of 2.
6209    //
6210    // `bare` is the control that isolates the defect rather than merely
6211    // observing it: same arm count, same entry shape, no comparison
6212    // operator anywhere — so it scored the right 2 before the fix and
6213    // after it. A fix that changed both members alike would be attacking
6214    // the entry count, which was never wrong.
6215    //
6216    // `ge` and `eq` exist because the issue named `<` and `>` only. All
6217    // six comparison spellings share the defect: `>=` and `==` reach
6218    // `stats.conditions` through the `LTEQ | GTEQ | EQEQ | …` arm rather
6219    // than the parent-gated `LT | GT` arm, and both scored 2 against a
6220    // decision count of 1.
6221    //
6222    // `iff` is the member that legitimately sits *above* its decision
6223    // count, and it is here so the table cannot be read as "§8 parity
6224    // everywhere". ABC counts the `else` (Fitzpatrick Rule 5) and
6225    // cyclomatic does not, so 2 conditions against 1 decision is correct
6226    // and unchanged by this fix.
6227    //
6228    // The `when_subject` anchor is the load-bearing one: the whole test
6229    // turns on these `when`s having no subject, and nothing in the
6230    // measured numbers would notice a fixture that grew one — a
6231    // subject-ful `when (x) { … }` scores its entries the old way and
6232    // `two` would read 4 again with every other row still satisfied.
6233    #[cfg(feature = "kotlin")]
6234    #[test]
6235    fn kotlin_subjectless_when_arm_counts_its_condition_once() {
6236        let src = "class K {
6237                fun f(): Boolean = true
6238                fun two(x: Int): Int = when { x > 5 -> 1; x < 0 -> 2; else -> 0 }
6239                fun bare(x: Boolean, y: Boolean): Int = when { x -> 1; y -> 2; else -> 0 }
6240                fun ge(x: Int): Int = when { x >= 5 -> 1; else -> 0 }
6241                fun eq(x: Int): Int = when { x == 5 -> 1; else -> 0 }
6242                fun andd(a: Int, b: Int): Int = when { a > 1 && b < 2 -> 1; else -> 0 }
6243                fun call(): Int = when { f() -> 1; else -> 0 }
6244                fun iff(x: Int): Int = if (x > 5) 1 else 0
6245            }";
6246        assert_kotlin_class_members(
6247            src,
6248            &[
6249                (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
6250                (Kotlin::WhenEntry as u16, 14, "`when` entries"),
6251                (
6252                    Kotlin::BinaryExpression as u16,
6253                    8,
6254                    "comparison / chain expressions",
6255                ),
6256                (Kotlin::AMPAMP as u16, 1, "`andd`'s `&&`"),
6257            ],
6258            &[
6259                // (member, abc.conditions, cyclomatic)
6260                ("f", 0, 1),
6261                // Was 4: two entries, two comparisons, counted twice.
6262                ("two", 2, 3),
6263                // The no-comparison control — 2 before and after.
6264                ("bare", 2, 3),
6265                // Was 2 each: the `GTEQ` / `EQEQ` spellings.
6266                ("ge", 1, 2),
6267                ("eq", 1, 2),
6268                // Was 3. A compound condition keeps both of its
6269                // comparisons — suppressing the operators instead
6270                // would have collapsed this one to 1.
6271                ("andd", 2, 3),
6272                // The condition slot scores a call directly, so a
6273                // spelling with no operator in it still reads 1.
6274                ("call", 1, 2),
6275                // Above its decision count on purpose: ABC counts
6276                // the `else`, cyclomatic does not.
6277                ("iff", 2, 2),
6278            ],
6279        );
6280    }
6281
6282    // #1459, the regression #1421 left behind. Turning the `when`
6283    // entry's blanket count into a condition slot moved the payment from
6284    // the entry to the condition — so every shape the slot cannot
6285    // classify went from 1 to 0, silently. Two shapes did:
6286    //
6287    // - `a!!`. `unary_expression` is one kind for *both* Kotlin unary
6288    //   spellings, and the slot routed it to the peel, so this reads as
6289    //   handled at every call site. The peel then looked for the operand
6290    //   at child(1) — right for the prefix `!x`, wrong for a postfix
6291    //   `x!!`, whose child(1) is the `!!` token — and stopped.
6292    // - `a as Boolean`. `as_expression` was not a wrapper the peel knew
6293    //   at all, so the slot fell off the end of its `else if`.
6294    //
6295    // The controls are `bare` / `cmp` / `call`, one per surviving path
6296    // (terminal, comparison token, invocation): a fix that moved the
6297    // entry count back would take those to 2 and fail here, which is
6298    // what distinguishes repairing the peel from reverting #1421.
6299    //
6300    // `safe` is the §5 row and the one measurement decided rather than
6301    // taste. `as?` is already a condition *token*
6302    // (`src/metrics/abc/kotlin.rs`), so a peel that treated `as` and
6303    // `as?` alike would score it 2 — the double count that arm's own
6304    // fix was avoiding. `kotlin_wrapper_operand` excludes `as?` for
6305    // that reason, and this member is what fails if the exclusion goes:
6306    // `(a as? Boolean)!!` is valid Kotlin whose peel reaches the cast
6307    // through two other wrappers, so it needs no invalid fixture to
6308    // discriminate.
6309    //
6310    // `iff` carries the same `a!!` through the `if` slot. That half is
6311    // not #1421 fallout — `if (a!!)` has read 0 since the slot landed in
6312    // #773 — but it is the same peel and the same edit, and a fix
6313    // narrowed to `when` entries would leave it at 0.
6314    //
6315    // Every member is valid Kotlin: `a!!` on a `Boolean?`, `as` on an
6316    // `Any`, and `(a as? Boolean)!!` back to a `Boolean`. The kind
6317    // anchors are what stop a later edit from trimming a spelling out
6318    // and turning its member into a silent copy of `bare`.
6319    #[cfg(feature = "kotlin")]
6320    #[test]
6321    fn kotlin_condition_slot_peels_null_assertions_and_casts() {
6322        let src = "class K {
6323                fun g(x: Boolean): Boolean = true
6324                fun bare(a: Boolean): Int = when { a -> 1; else -> 0 }
6325                fun cmp(a: Int): Int = when { a > 5 -> 1; else -> 0 }
6326                fun call(a: Boolean): Int = when { g(a) -> 1; else -> 0 }
6327                fun bang(a: Boolean?): Int = when { a!! -> 1; else -> 0 }
6328                fun paren(a: Boolean?): Int = when { (a!!) -> 1; else -> 0 }
6329                fun cast(a: Any): Int = when { a as Boolean -> 1; else -> 0 }
6330                fun chain(a: Any?): Int = when { a!! as Boolean -> 1; else -> 0 }
6331                fun safe(a: Any): Int = when { (a as? Boolean)!! -> 1; else -> 0 }
6332                fun cmt(a: Boolean): Int = when { ! /*c*/ a -> 1; else -> 0 }
6333                fun iff(a: Boolean?): Int { if (a!!) { return 1 }; return 0 }
6334            }";
6335        assert_kotlin_class_members(
6336            src,
6337            &[
6338                (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
6339                (
6340                    Kotlin::BANGBANG as u16,
6341                    5,
6342                    "the `!!` null assertions of `bang` / `paren` / `chain` / `safe` / `iff`",
6343                ),
6344                (
6345                    Kotlin::AsExpression as u16,
6346                    3,
6347                    "the casts of `cast` / `chain` / `safe`",
6348                ),
6349                (Kotlin::AsQMARK as u16, 1, "`safe`'s safe cast"),
6350                (
6351                    Kotlin::BlockComment as u16,
6352                    1,
6353                    "`cmt`'s interposed comment, spaces and all",
6354                ),
6355                (
6356                    Kotlin::ParenthesizedExpression as u16,
6357                    2,
6358                    "the parenthesised operands of `paren` / `safe`",
6359                ),
6360            ],
6361            &[
6362                // (member, abc.conditions, cyclomatic)
6363                ("g", 0, 1),
6364                // The three controls, unchanged by this fix.
6365                ("bare", 1, 2),
6366                ("cmp", 1, 2),
6367                ("call", 1, 2),
6368                // Each was 0: the peel stopped on the wrapper.
6369                ("bang", 1, 2),
6370                ("paren", 1, 2),
6371                ("cast", 1, 2),
6372                ("chain", 1, 2),
6373                // 1, not 2 — the `as?` token owns this one, and the
6374                // peel declines to charge for it a second time.
6375                ("safe", 1, 2),
6376                // The peel reads its operand through the `operator` /
6377                // `argument` fields, so an `extra` between the two no
6378                // longer displaces it. The spacing is load-bearing and
6379                // must not be tidied away: `!/*c*/a` unspaced parses
6380                // with the comment somewhere a `child(1)` read still
6381                // skips, so only the spaced spelling can tell the field
6382                // read from the positional one. Measured both ways.
6383                ("cmt", 1, 2),
6384                // The `if` slot, which has read 0 since #773.
6385                ("iff", 1, 2),
6386            ],
6387        );
6388    }
6389
6390    // The other half of #1421: a subject-ful entry must not move. Its
6391    // condition is a *pattern* matched against the subject, not an
6392    // independent boolean expression, so the implicit `subject ==
6393    // pattern` is the decision and the entry itself pays for it —
6394    // `range_test`, `type_test` and a bare constant carry no token the
6395    // comparison arms would count.
6396    //
6397    // `cmp` is the interesting row and the reason `nested` sits beside
6398    // it. `when (x) { y > 5 -> … }` is legal when `x` is `Boolean`, and
6399    // it reads 2 conditions against 1 decision — the entry's implicit
6400    // equality plus the `>` inside the operand. That is not the #1421
6401    // double-count reappearing: `nested` spells the same thing as an
6402    // `if` (`x == (y > 5)`) and ABC has always scored it 2 against the
6403    // same 1 decision. A comparison nested inside a comparison is two
6404    // comparisons; only one of them is a branch.
6405    #[cfg(feature = "kotlin")]
6406    #[test]
6407    fn kotlin_subjectful_when_arms_keep_the_per_entry_count() {
6408        let src = "class K {
6409                fun inn(a: Int): Int = when (a) { in 1..2 -> 1; else -> 0 }
6410                fun notin(a: Int): Int = when (a) { !in 1..2 -> 1; else -> 0 }
6411                fun iss(a: Any): Int = when (a) { is String -> 1; else -> 0 }
6412                fun konst(a: Int): Int = when (a) { 1 -> 10; 2 -> 20; else -> 0 }
6413                fun cmp(x: Boolean, y: Int): Int = when (x) { y > 5 -> 1; else -> 0 }
6414                fun nested(x: Boolean, y: Int): Int { if (x == (y > 5)) { return 1 }; return 0 }
6415            }";
6416        assert_kotlin_class_members(
6417            src,
6418            &[
6419                (Kotlin::WhenSubject as u16, 5, "`when` subjects"),
6420                (Kotlin::WhenEntry as u16, 11, "`when` entries"),
6421                (Kotlin::RangeTest as u16, 2, "`in` / `!in` patterns"),
6422                (Kotlin::TypeTest as u16, 1, "the `is` pattern"),
6423                (
6424                    Kotlin::BinaryExpression as u16,
6425                    3,
6426                    "`cmp`'s and `nested`'s comparisons",
6427                ),
6428            ],
6429            &[
6430                ("inn", 1, 2),
6431                ("notin", 1, 2),
6432                ("iss", 1, 2),
6433                ("konst", 2, 3),
6434                // Above its decision count, like `nested` below it
6435                // and for the same reason.
6436                ("cmp", 2, 2),
6437                ("nested", 2, 2),
6438            ],
6439        );
6440    }
6441
6442    // `kotlin_enclosing_when_has_subject` stops scanning at the `{`, so
6443    // that a subject-less `when` does not walk all N arms once per arm.
6444    // The bound has to clear the header first, and the header can carry
6445    // an `extra`: tree-sitter hangs `when /*c*/ (x)`'s comment off the
6446    // `when_expression` between the keyword and the subject, so a bound
6447    // one child tighter — `child(1)`, or a stop at the first *named*
6448    // child — reads `cmtSubject` as subject-less. It would then score
6449    // its `1` arm through the slot, where a `number_literal` is not a
6450    // Kotlin terminal, and report 0 conditions instead of 1. That is
6451    // what discriminates here; `cmtNone` is the other direction, where
6452    // the brace is all the bound has to stop on.
6453    //
6454    // The `block_comment` census is the fixture anchor: delete either
6455    // comment and the members still read 1, because a plain `when (x)`
6456    // and a plain `when {` both do. The count fails by name instead.
6457    #[cfg(feature = "kotlin")]
6458    #[test]
6459    fn kotlin_when_subject_scan_clears_a_comment_before_the_brace() {
6460        let src = "class K {
6461                fun cmtSubject(x: Int): Int = when /*c*/ (x) { 1 -> 1; else -> 0 }
6462                fun cmtNone(a: Boolean): Int = when /*c*/ { a -> 1; else -> 0 }
6463            }";
6464        assert_kotlin_class_members(
6465            src,
6466            &[
6467                (Kotlin::BlockComment as u16, 2, "the two header comments"),
6468                (Kotlin::WhenSubject as u16, 1, "`cmtSubject`'s subject"),
6469                (Kotlin::WhenEntry as u16, 4, "`when` entries"),
6470            ],
6471            &[("cmtSubject", 1, 2), ("cmtNone", 1, 2)],
6472        );
6473    }
6474
6475    // A subject-less condition reaches the slot through the same
6476    // `kotlin_inspect_container` unwrapping an `if` predicate does, which
6477    // is what separates this design from suppressing the entry's
6478    // operator. `notted` and `paren` unwrap to a bare terminal and score
6479    // through the slot; `notCmp` and `parenCmp` unwrap to a
6480    // `binary_expression`, score nothing from the slot, and take their
6481    // one condition from the `>` the token arm already owned. Both pairs
6482    // land on 1, which no blanket `+1` can produce for the second pair —
6483    // they read 2 before the fix.
6484    //
6485    // `paren` is the member that reaches the `WhenEntry` seed added to
6486    // `kotlin_inspect_container`'s boolean-context set; without it the
6487    // parenthesised operand is not in a slot the walker calls boolean and
6488    // `paren` drops to 0. `notted` cannot stand in for it — a `!`
6489    // operator proves boolean content on its own.
6490    #[cfg(feature = "kotlin")]
6491    #[test]
6492    fn kotlin_subjectless_when_condition_wrappers_count_once() {
6493        let src = "class K {
6494                fun notted(a: Boolean): Int = when { !a -> 1; else -> 0 }
6495                fun paren(a: Boolean): Int = when { (a) -> 1; else -> 0 }
6496                fun notCmp(a: Int): Int = when { !(a > 1) -> 1; else -> 0 }
6497                fun parenCmp(a: Int): Int = when { (a > 1) -> 1; else -> 0 }
6498                fun nest(x: Int, y: Int): Int =
6499                    when { x > 1 -> when { y > 2 -> 1; else -> 2 }; else -> 0 }
6500                fun blockBody(x: Int): Int { when { x > 5 -> { return 1 } }; return 0 }
6501            }";
6502        assert_kotlin_class_members(
6503            src,
6504            &[
6505                // Paired, not bare: "no subjects" alone is satisfied by a
6506                // fixture that lost its `when`s altogether.
6507                (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
6508                (Kotlin::WhenEntry as u16, 13, "`when` entries"),
6509                (Kotlin::UnaryExpression as u16, 2, "the two `!` operands"),
6510                (
6511                    Kotlin::ParenthesizedExpression as u16,
6512                    3,
6513                    "the three parenthesised operands",
6514                ),
6515                (Kotlin::BinaryExpression as u16, 5, "comparisons"),
6516            ],
6517            &[
6518                ("notted", 1, 2),
6519                ("paren", 1, 2),
6520                // Both were 2: the entry's blanket count plus the `>`.
6521                ("notCmp", 1, 2),
6522                ("parenCmp", 1, 2),
6523                // Was 4 — one entry and one comparison per `when`,
6524                // each counted twice.
6525                ("nest", 2, 3),
6526                // The `when`-as-statement shape: a `block` body and no
6527                // `else` arm at all. Reading the condition from the
6528                // `condition` field rather than by position is what keeps
6529                // the block out of the slot; it was 2 before the fix.
6530                ("blockBody", 1, 2),
6531            ],
6532        );
6533    }
6534
6535    // The one place this design deliberately does not reach parity, put
6536    // under test so the choice cannot drift unremarked. A `when` entry
6537    // may list alternatives (`when { a, b -> … }`, an implicit `or`), and
6538    // the `condition` field is `multiple` — but cyclomatic scores the
6539    // whole entry as one decision, so `bareAlts` is at parity only
6540    // because the slot reads the *first* alternative and stops. Routing
6541    // every alternative through the slot would push it to 2.
6542    //
6543    // `alts` is the row that cannot be brought to parity from the ABC
6544    // side at all: the comparison-token arms count `>` and `<` wherever
6545    // they appear, so it reads 2 against 1 decision no matter what the
6546    // entry slot does. It was 3 before #1421 and the remaining gap is
6547    // cyclomatic's single count per multi-alternative entry, not a
6548    // double-count here.
6549    //
6550    // `subjAlts` is the subject-ful control, unchanged and at parity: its
6551    // alternatives are constants carrying no token to count.
6552    #[cfg(feature = "kotlin")]
6553    #[test]
6554    fn kotlin_subjectless_when_multi_alternative_entry() {
6555        let src = "class K {
6556                fun alts(x: Int, y: Int): Int = when { x > 5, y < 0 -> 1; else -> 0 }
6557                fun bareAlts(x: Boolean, y: Boolean): Int = when { x, y -> 1; else -> 0 }
6558                fun subjAlts(a: Int): Int = when (a) { 1, 2 -> 10; else -> 0 }
6559            }";
6560        assert_kotlin_class_members(
6561            src,
6562            &[
6563                (Kotlin::WhenSubject as u16, 1, "`subjAlts`'s subject"),
6564                (Kotlin::WhenEntry as u16, 6, "`when` entries"),
6565                (
6566                    Kotlin::BinaryExpression as u16,
6567                    2,
6568                    "`alts`'s two comparisons",
6569                ),
6570            ],
6571            &[
6572                // Above its decision count: cyclomatic scores the entry
6573                // once, the token arms score both comparisons. Was 3.
6574                ("alts", 2, 2),
6575                ("bareAlts", 1, 2),
6576                ("subjAlts", 1, 2),
6577            ],
6578        );
6579    }
6580
6581    // `is_expression` and `in_expression` are the two relational forms
6582    // tree-sitter-kotlin-ng spells as their own production rather than as
6583    // a `binary_expression`, so no comparison-token arm sees them and
6584    // nothing counted them: `if (a is String)` scored 0 against a
6585    // decision count of 1, and the same test in a subject-less `when`
6586    // scored 1 only because the entry's blanket `+1` happened to cover
6587    // it. Removing that blanket without counting these two somewhere
6588    // would have regressed `whenIs` and `whenIn` to 0, and `isAnd` —
6589    // where the `is` test is one operand of a `&&` chain and so reaches
6590    // the walker rather than the slot — from 2 to 1.
6591    //
6592    // The `if` members are here because the fix was never `when`-
6593    // specific: they are the call sites that prove it reaches the `if` /
6594    // `while` predicate slot too.
6595    //
6596    // #1421 counted them through `kotlin_bool_terminal_kinds!()` and
6597    // #1461 moved them to an unconditional arm, which leaves every
6598    // number below unchanged — the slot no longer counts the test and
6599    // the arm does. `kotlin_is_and_in_score_outside_a_boolean_slot` is
6600    // the half this fixture cannot see, every member of it being inside
6601    // a slot.
6602    #[cfg(feature = "kotlin")]
6603    #[test]
6604    fn kotlin_is_and_in_expressions_are_unary_conditions() {
6605        let src = "class K {
6606                fun ifIs(a: Any): Int { if (a is String) { return 1 }; return 0 }
6607                fun ifIn(a: Int): Int { if (a in 1..2) { return 1 }; return 0 }
6608                fun whenIs(a: Any): Int = when { a is String -> 1; else -> 0 }
6609                fun whenIn(a: Int): Int = when { a in 1..2 -> 1; else -> 0 }
6610                fun isAnd(a: Any, b: Boolean): Int = when { a is String && b -> 1; else -> 0 }
6611            }";
6612        assert_kotlin_class_members(
6613            src,
6614            &[
6615                // Paired, not bare — see the wrappers test above.
6616                (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
6617                (Kotlin::WhenEntry as u16, 6, "`when` entries"),
6618                (Kotlin::IsExpression as u16, 3, "`is` tests"),
6619                (Kotlin::InExpression as u16, 2, "`in` tests"),
6620                (Kotlin::AMPAMP as u16, 1, "`isAnd`'s `&&`"),
6621            ],
6622            &[
6623                // Both were 0 — the predicate slot saw a kind it did
6624                // not classify and the token arms saw no operator.
6625                ("ifIs", 1, 2),
6626                ("ifIn", 1, 2),
6627                ("whenIs", 1, 2),
6628                ("whenIn", 1, 2),
6629                ("isAnd", 2, 3),
6630            ],
6631        );
6632    }
6633
6634    // Kotlin spells boolean `and` / `or` / `xor` as infix *functions*, so
6635    // `a and b` parses as `infix_expression` — not `binary_expression`,
6636    // and not any token arm. It belongs in
6637    // `kotlin_bool_terminal_kinds!()` for the same reason
6638    // `call_expression` does: `a and b` is `a.and(b)`.
6639    //
6640    // This was a regression of #1421, not a pre-existing gap, and it is
6641    // the third shape the blanket per-entry count turned out to be
6642    // propping up — after `is` / `in` and bare parens, which that fix
6643    // caught. Found by the whole-branch review. Measured on both sides:
6644    // `when { a and b -> … }` scored 1 before and 0 after, while the
6645    // `if (a and b)` it is supposed to agree with still scored 1.
6646    //
6647    // The `if` members are the anchor rather than a second `when`: the
6648    // whole premise of #1421 is that a subject-less arm scores like an
6649    // `if` predicate, so a fixture where the two disagree is the
6650    // statement of the bug. `andAmp` holds the pair level against the
6651    // `&&` spelling, which never regressed and would otherwise be the
6652    // only form under test.
6653    #[cfg(feature = "kotlin")]
6654    #[test]
6655    fn kotlin_infix_boolean_functions_are_unary_conditions() {
6656        let src = "class K {
6657                fun whenAnd(a: Boolean, b: Boolean): Int = when { a and b -> 1; else -> 0 }
6658                fun whenOr(a: Boolean, b: Boolean): Int = when { a or b -> 1; else -> 0 }
6659                fun whenXor(a: Boolean, b: Boolean): Int = when { a xor b -> 1; else -> 0 }
6660                fun whenAmp(a: Boolean, b: Boolean): Int = when { a && b -> 1; else -> 0 }
6661                fun ifAnd(a: Boolean, b: Boolean): Int { if (a and b) { return 1 }; return 0 }
6662            }";
6663        assert_kotlin_class_members(
6664            src,
6665            &[
6666                (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
6667                (Kotlin::InfixExpression as u16, 4, "the infix calls"),
6668                (Kotlin::AMPAMP as u16, 1, "`whenAmp`'s `&&`"),
6669            ],
6670            &[
6671                // 0 before this fix: the predicate slot saw a kind it
6672                // did not classify and no token arm matched.
6673                ("whenAnd", 1, 2),
6674                ("whenOr", 1, 2),
6675                ("whenXor", 1, 2),
6676                // Never regressed — `&&` is a `binary_expression` whose
6677                // operator the token arm owns. Holds the others level.
6678                ("whenAmp", 2, 3),
6679                // The agreement #1421 exists to establish: a
6680                // subject-less arm scores what the equivalent `if`
6681                // predicate scores.
6682                ("ifAnd", 1, 2),
6683            ],
6684        );
6685    }
6686
6687    #[cfg(feature = "kotlin")]
6688    #[test]
6689    fn kotlin_catch_block_counts() {
6690        check_metrics::<KotlinParser>(
6691            "fun m() {
6692                try {
6693                    println(\"ok\")
6694                } catch (e: Exception) {
6695                    println(\"err\")
6696                }
6697            }",
6698            "foo.kt",
6699            |metric| {
6700                // `try` (+1) and `catch` (+1) each contribute one condition,
6701                // matching Java / C# / C++ / Groovy (Fitzpatrick counts both
6702                // keywords). Before #696 Kotlin counted only the catch block.
6703                assert_eq!(metric.abc.conditions_sum(), 2);
6704                insta::assert_json_snapshot!(metric.abc);
6705            },
6706        );
6707    }
6708
6709    #[cfg(feature = "kotlin")]
6710    #[test]
6711    fn kotlin_elvis_and_safe_cast() {
6712        // `?:` (elvis) and `as?` (safe cast) are condition-like.
6713        check_metrics::<KotlinParser>(
6714            "fun m(s: String?): Int {
6715                val n = (s as? Int) ?: 0
6716                return n
6717            }",
6718            "foo.kt",
6719            |metric| {
6720                // as? (+1) + ?: (+1) = 2 conditions.
6721                assert_eq!(metric.abc.conditions_sum(), 2);
6722                insta::assert_json_snapshot!(metric.abc);
6723            },
6724        );
6725    }
6726
6727    #[cfg(feature = "kotlin")]
6728    #[test]
6729    fn kotlin_generic_brackets_not_conditions() {
6730        // `<` / `>` used as type-parameter brackets must not be counted.
6731        check_metrics::<KotlinParser>(
6732            "class Box<T>(val v: T)
6733            fun <T> wrap(x: T): Box<T> = Box(x)",
6734            "foo.kt",
6735            |metric| {
6736                // No comparisons — only generic brackets.
6737                assert_eq!(metric.abc.conditions_sum(), 0);
6738                insta::assert_json_snapshot!(metric.abc);
6739            },
6740        );
6741    }
6742
6743    // #1297: a qualified super call disambiguates its supertype with
6744    // the same two bare tokens a comparison uses — `super<A>.g()` is a
6745    // `super_expression`, which the previous denylist
6746    // (`type_arguments` / `type_parameters`) did not name, so it scored
6747    // two conditions. One `super<A>` against one genuine `a < b`
6748    // separates every mis-aim: 3 pre-fix, 1 once the arm allows
6749    // `BinaryExpression`, 0 if it allows the wrong parent or the
6750    // fixture stops parsing.
6751    #[cfg(feature = "kotlin")]
6752    #[test]
6753    fn kotlin_super_type_argument_is_not_a_condition() {
6754        check_metrics::<KotlinParser>(
6755            "class B : A() {
6756                override fun g(a: Int, b: Int): Int {
6757                    if (a < b) { return super<A>.g(a, b) }
6758                    return 0
6759                }
6760            }",
6761            "foo.kt",
6762            |metric| {
6763                assert_eq!(metric.abc.conditions_sum(), 1);
6764                // Non-vacuity guard: 1 is also what a body whose `if`
6765                // survived but whose super call did not would score, so
6766                // pin the call itself. Measured: dropping
6767                // `super<A>.g(a, b)` takes `branches_sum()` from 2 to 1 —
6768                // the remaining branch is the `A()` primary-constructor
6769                // delegation in the class header, which #1384 taught this
6770                // metric to count.
6771                assert_eq!(metric.abc.branches_sum(), 2);
6772            },
6773        );
6774    }
6775
6776    #[cfg(feature = "kotlin")]
6777    #[test]
6778    fn kotlin_class_with_methods_and_branches() {
6779        check_metrics::<KotlinParser>(
6780            "class C {
6781                var counter: Int = 0
6782                fun bump() {
6783                    counter += 1
6784                    println(counter)
6785                }
6786            }",
6787            "foo.kt",
6788            |metric| {
6789                // assignments: var counter = 0 (+1), counter += 1 (+1) = 2
6790                // branches: println(counter) = 1
6791                assert_eq!(metric.abc.assignments_sum(), 2);
6792                assert_eq!(metric.abc.branches_sum(), 1);
6793                assert_eq!(metric.abc.conditions_sum(), 0);
6794                insta::assert_json_snapshot!(metric.abc);
6795            },
6796        );
6797    }
6798
6799    #[cfg(feature = "kotlin")]
6800    #[test]
6801    fn kotlin_object_singleton_abc() {
6802        check_metrics::<KotlinParser>(
6803            "object Util {
6804                fun work(x: Int): Int {
6805                    var y = x
6806                    y += 1
6807                    if (y > 0) {
6808                        return y
6809                    }
6810                    return -1
6811                }
6812            }",
6813            "foo.kt",
6814            |metric| {
6815                // assignments: var y = x (+1), y += 1 (+1) = 2
6816                // branches: 0 (return is not a call)
6817                // conditions: y > 0 (+1) = 1
6818                assert_eq!(metric.abc.assignments_sum(), 2);
6819                assert_eq!(metric.abc.branches_sum(), 0);
6820                assert_eq!(metric.abc.conditions_sum(), 1);
6821                insta::assert_json_snapshot!(metric.abc);
6822            },
6823        );
6824    }
6825
6826    #[cfg(feature = "kotlin")]
6827    #[test]
6828    fn kotlin_interface_abc() {
6829        // Pure-abstract interface with no bodies — all-zero.
6830        check_metrics::<KotlinParser>(
6831            "interface I {
6832                fun work(): Int
6833                fun describe(): String
6834            }",
6835            "foo.kt",
6836            |metric| {
6837                assert_eq!(metric.abc.assignments_sum(), 0);
6838                assert_eq!(metric.abc.branches_sum(), 0);
6839                assert_eq!(metric.abc.conditions_sum(), 0);
6840                insta::assert_json_snapshot!(metric.abc);
6841            },
6842        );
6843    }
6844
6845    #[cfg(feature = "kotlin")]
6846    #[test]
6847    fn kotlin_nested_class_abc() {
6848        check_metrics::<KotlinParser>(
6849            "class Outer {
6850                var o: Int = 0
6851                class Nested {
6852                    var n: Int = 0
6853                    fun bump() { n += 1 }
6854                }
6855            }",
6856            "foo.kt",
6857            |metric| {
6858                // Outer: var o = 0 (+1)
6859                // Nested: var n = 0 (+1), n += 1 (+1) = 2
6860                // total assignments = 3
6861                assert_eq!(metric.abc.assignments_sum(), 3);
6862                insta::assert_json_snapshot!(metric.abc);
6863            },
6864        );
6865    }
6866
6867    #[cfg(feature = "kotlin")]
6868    #[test]
6869    fn kotlin_data_class_abc() {
6870        // `data class` with primary-constructor `val`s — no assignments
6871        // (vals don't count) and no body conditions.
6872        check_metrics::<KotlinParser>(
6873            "data class Point(val x: Int, val y: Int)",
6874            "foo.kt",
6875            |metric| {
6876                assert_eq!(metric.abc.assignments_sum(), 0);
6877                assert_eq!(metric.abc.branches_sum(), 0);
6878                assert_eq!(metric.abc.conditions_sum(), 0);
6879                insta::assert_json_snapshot!(metric.abc);
6880            },
6881        );
6882    }
6883
6884    #[cfg(feature = "kotlin")]
6885    #[test]
6886    fn kotlin_primary_constructor_default_value_not_assignment() {
6887        // Regression: default values on primary-constructor `val`
6888        // parameters are initialisers, not assignments. Without
6889        // `ClassParameter` pushing a declaration sentinel, the `=` token
6890        // here would be counted unconditionally as a standalone
6891        // assignment.
6892        check_metrics::<KotlinParser>("class C(val a: Int = 5)", "foo.kt", |metric| {
6893            // `val a = 5` → suppressed (Const sentinel).
6894            assert_eq!(metric.abc.assignments_sum(), 0);
6895            insta::assert_json_snapshot!(metric.abc);
6896        });
6897    }
6898
6899    #[cfg(feature = "kotlin")]
6900    #[test]
6901    fn kotlin_unary_conditions_in_chain() {
6902        // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
6903        // `&&` / `||` chain is one condition. `a && b || c` → a, b, c each
6904        // contribute one; the `&&` / `||` operators contribute nothing.
6905        // expected: 3 unary conditions, no comparisons, no `if`-keyword
6906        // condition in Kotlin (matches the Java byte-equivalent of 3).
6907        check_metrics::<KotlinParser>(
6908            "fun f(a: Boolean, b: Boolean, c: Boolean) {
6909                if (a && b || c) { println(\"x\") }
6910            }",
6911            "foo.kt",
6912            |metric| {
6913                assert_eq!(metric.abc.conditions_sum(), 3);
6914            },
6915        );
6916    }
6917
6918    #[cfg(feature = "kotlin")]
6919    #[test]
6920    fn kotlin_comparison_operands_add_nothing() {
6921        // Isolation check: comparison operands of a `&&` chain are nested
6922        // `binary_expression` nodes, not bare boolean leaves, so the
6923        // walker adds nothing — only the two `>` comparisons count.
6924        // expected: 2 (the two `>` tokens), walker contributes 0.
6925        check_metrics::<KotlinParser>(
6926            "fun g(x: Int, y: Int) {
6927                if (x > 0 && y > 0) { println(\"x\") }
6928            }",
6929            "foo.kt",
6930            |metric| {
6931                assert_eq!(metric.abc.conditions_sum(), 2);
6932            },
6933        );
6934    }
6935
6936    #[cfg(feature = "kotlin")]
6937    #[test]
6938    fn kotlin_negated_operand_is_unary_condition() {
6939        // A `!`-negated operand is still a unary condition: `a && !b`
6940        // unwraps the `unary_expression` to reach the inner identifier.
6941        // expected: 2 (`a` and the `!b` operand).
6942        check_metrics::<KotlinParser>(
6943            "fun f(a: Boolean, b: Boolean) {
6944                if (a && !b) { println(\"x\") }
6945            }",
6946            "foo.kt",
6947            |metric| {
6948                assert_eq!(metric.abc.conditions_sum(), 2);
6949            },
6950        );
6951    }
6952
6953    // `kotlin_inspect_container` has two callers, and #1459's fixture
6954    // members all reach it through the *slot*
6955    // (`kotlin_count_condition`). This is the other one: the `&&` / `||`
6956    // walker, which routes any named non-terminal operand through the
6957    // same peel. The two wrappers #1459 taught it therefore have a
6958    // second, structurally independent path into the count, and a
6959    // fixture covering only the slot leaves it untested
6960    // (`.claude/rules/grammar-dispatch.md` §11).
6961    //
6962    // Each chain is one identifier plus one wrapped operand, so 2 is
6963    // "the walker peeled the wrapper" and 1 is "it gave up on it" —
6964    // which is what both lines scored before #1459. The bare `a && b`
6965    // control is `kotlin_unary_conditions_in_chain`'s shape at 2, so a
6966    // regression cannot be read as the chain itself changing.
6967    #[cfg(feature = "kotlin")]
6968    #[test]
6969    fn kotlin_chain_operands_peel_null_assertions_and_casts() {
6970        // Anchored per row, because the assertion alone cannot tell the
6971        // wrapper from its operand: `a && b` scores the same 2 / 3 as
6972        // every row here, so trimming the `!!` or the cast out of a
6973        // fixture would leave this green with its subject gone
6974        // (`.claude/rules/testing.md`, "Perturb the fixture as well as
6975        // the production line").
6976        for (chain, spelling, anchors) in [
6977            (
6978                "a && b!!",
6979                "null assertion",
6980                &[(Kotlin::BANGBANG as u16, 1usize, "the `!!`")][..],
6981            ),
6982            (
6983                "a && b as Boolean",
6984                "cast",
6985                &[(Kotlin::AsExpression as u16, 1, "the `as` cast")][..],
6986            ),
6987            (
6988                "a && (b!!)",
6989                "parenthesised null assertion",
6990                &[
6991                    (Kotlin::BANGBANG as u16, 1, "the `!!`"),
6992                    (Kotlin::ParenthesizedExpression as u16, 1, "the parens"),
6993                ][..],
6994            ),
6995        ] {
6996            let src = format!("fun f(a: Boolean, b: Any) {{ if ({chain}) {{ println(\"x\") }} }}");
6997            assert_fixture_spells::<KotlinParser>(&src, "foo.kt", anchors);
6998            check_func_space::<KotlinParser, _>(&src, "foo.kt", |space| {
6999                let f = child_space(&space, "f");
7000                assert_eq!(
7001                    f.metrics.abc.conditions(),
7002                    2,
7003                    "`{chain}`: the identifier plus the {spelling} operand"
7004                );
7005                assert_eq!(f.metrics.cyclomatic.cyclomatic(), 3, "`{chain}`: decisions");
7006            });
7007        }
7008    }
7009
7010    #[cfg(feature = "kotlin")]
7011    #[test]
7012    fn kotlin_bare_if_predicate_is_one_condition() {
7013        // Issue #773: a bare-boolean `if` predicate (`if (flag)`) is one
7014        // Fitzpatrick unary condition. Before the Phase-2B arm it counted
7015        // 0, so `if (flag) 1 else -1` scored 1 (only the `else`) instead of
7016        // 2, dropping below Kotlin's own cyclomatic decision count.
7017        // expected: predicate (1) + else (1) = 2.
7018        check_metrics::<KotlinParser>(
7019            "fun m(flag: Boolean): Int { return if (flag) 1 else -1 }",
7020            "foo.kt",
7021            |metric| {
7022                assert_eq!(metric.abc.conditions_sum(), 2);
7023            },
7024        );
7025    }
7026
7027    #[cfg(feature = "kotlin")]
7028    #[test]
7029    fn kotlin_bare_while_predicate_is_one_condition() {
7030        // Issue #773: the bare predicate of a `while` loop counts one
7031        // condition via the `condition` field. expected: 1.
7032        check_metrics::<KotlinParser>(
7033            "fun m(running: Boolean) { while (running) { println(\"x\") } }",
7034            "foo.kt",
7035            |metric| {
7036                assert_eq!(metric.abc.conditions_sum(), 1);
7037            },
7038        );
7039    }
7040
7041    #[cfg(feature = "kotlin")]
7042    #[test]
7043    fn kotlin_bare_do_while_predicate_is_one_condition() {
7044        // Issue #773: the bare predicate of a `do`/`while` loop counts one
7045        // condition. expected: 1.
7046        check_metrics::<KotlinParser>(
7047            "fun m(ok: Boolean) { do { println(\"x\") } while (ok) }",
7048            "foo.kt",
7049            |metric| {
7050                assert_eq!(metric.abc.conditions_sum(), 1);
7051            },
7052        );
7053    }
7054
7055    #[cfg(feature = "kotlin")]
7056    #[test]
7057    fn kotlin_comparison_predicate_not_double_counted() {
7058        // Double-count guard (#773): a comparison predicate (`if (a == b)`)
7059        // is a nested `binary_expression` already counted by the `==` token
7060        // arm, so the Phase-2B condition-slot arm must add nothing here.
7061        // expected: `==` (1) + else (1) = 2 — unchanged by the new arm.
7062        check_metrics::<KotlinParser>(
7063            "fun m(a: Int, b: Int): Int { return if (a == b) 1 else -1 }",
7064            "foo.kt",
7065            |metric| {
7066                assert_eq!(metric.abc.conditions_sum(), 2);
7067            },
7068        );
7069    }
7070
7071    #[cfg(feature = "kotlin")]
7072    #[test]
7073    fn kotlin_short_circuit_predicate_not_double_counted() {
7074        // Double-count guard (#773): an `&&`/`||` predicate is counted by
7075        // the Rule 9 chain walker (each operand once); the Phase-2B arm
7076        // must add nothing for it. expected: `x` (1) + `y` (1) + else (1)
7077        // = 3 — unchanged by the new arm.
7078        check_metrics::<KotlinParser>(
7079            "fun m(x: Boolean, y: Boolean): Int { return if (x && y) 1 else -1 }",
7080            "foo.kt",
7081            |metric| {
7082                assert_eq!(metric.abc.conditions_sum(), 3);
7083            },
7084        );
7085    }
7086
7087    #[cfg(feature = "kotlin")]
7088    #[test]
7089    fn kotlin_parenthesised_bare_predicate_is_one_condition() {
7090        // A parenthesised bare predicate (`if ((flag))`) is unwrapped by
7091        // `kotlin_inspect_container` and still counts one condition (#773).
7092        // expected: 1.
7093        check_metrics::<KotlinParser>(
7094            "fun m(flag: Boolean) { if ((flag)) { println(\"x\") } }",
7095            "foo.kt",
7096            |metric| {
7097                assert_eq!(metric.abc.conditions_sum(), 1);
7098            },
7099        );
7100    }
7101
7102    // --- TypeScript / TSX ABC tests --------------------------------------
7103    //
7104    // Assignment, branch, condition counting per Fitzpatrick:
7105    // - Augmented assignment / `++` / `--` always count.
7106    // - Plain `=` counts unless inside `const` declaration.
7107    // - `call_expression` / `new_expression` count as branches.
7108    // - Comparison / equality operators, ternary `?`, `??`, control-flow
7109    //   arms (`else`, `case`, `default`, `catch`, `try`, `instanceof`),
7110    //   and `<`/`>` (outside `type_arguments` / `type_parameters`) count
7111    //   as conditions.
7112
7113    #[cfg(feature = "typescript")]
7114    #[test]
7115    fn typescript_assignments_basic() {
7116        check_metrics::<TypescriptParser>(
7117            "class C {
7118                m(): void {
7119                    let x = 0;          // +1 — only a `const` initializer is suppressed
7120                    x = 1;              // +1
7121                    x += 2;             // +1
7122                    x++;                // +1
7123                }
7124            }",
7125            "foo.ts",
7126            |metric| {
7127                assert_eq!(metric.abc.assignments_sum(), 4);
7128                insta::assert_json_snapshot!(metric.abc);
7129            },
7130        );
7131    }
7132
7133    #[cfg(feature = "typescript")]
7134    #[test]
7135    fn typescript_const_excluded_from_assignments() {
7136        check_metrics::<TypescriptParser>(
7137            "class C {
7138                m(): void {
7139                    const a = 1;        // suppressed (`const` initializer)
7140                    const b = 2;        // suppressed
7141                    let c = 3;          // +1 — `let` initializers count
7142                }
7143            }",
7144            "foo.ts",
7145            |metric| {
7146                assert_eq!(metric.abc.assignments_sum(), 1);
7147                insta::assert_json_snapshot!(metric.abc);
7148            },
7149        );
7150    }
7151
7152    // Regression cluster for #1277. The pre-fix implementation decided
7153    // "is this `=` a `const` initializer?" from a sentinel stack cleared
7154    // only on a `SEMI` token, so the answer for one statement depended on
7155    // the *previous* statement's terminator. Automatic semicolon
7156    // insertion makes that terminator optional in all four JS-family
7157    // languages. The replacement is structural — see
7158    // `impl_js_family_const_binding!` in `src/metrics/abc/js_family.rs`.
7159
7160    #[cfg(feature = "typescript")]
7161    #[test]
7162    fn typescript_asi_const_does_not_suppress_later_assignments() {
7163        check_metrics::<TypescriptParser>(
7164            "function f() {
7165                const a = 1
7166                x = 2
7167                y = 3
7168            }",
7169            "foo.ts",
7170            |metric| {
7171                // `const a = 1` suppressed; `x = 2` and `y = 3` count.
7172                // Pre-#1277 this reported 0: the unterminated `const`
7173                // never popped its sentinel.
7174                assert_eq!(metric.abc.assignments_sum(), 2);
7175            },
7176        );
7177    }
7178
7179    #[cfg(feature = "typescript")]
7180    #[test]
7181    fn typescript_semicolon_const_does_not_suppress_later_assignments() {
7182        // The semicolon-terminated spelling of the fixture above, which
7183        // must score the same as it. Pre-#1277 they scored 2 and 0 — the
7184        // pair is what pins the terminator out of the answer.
7185        check_metrics::<TypescriptParser>(
7186            "function f() {
7187                const a = 1;
7188                x = 2;
7189                y = 3;
7190            }",
7191            "foo.ts",
7192            |metric| {
7193                assert_eq!(metric.abc.assignments_sum(), 2);
7194            },
7195        );
7196    }
7197
7198    #[cfg(feature = "typescript")]
7199    #[test]
7200    fn typescript_as_const_does_not_suppress_later_assignments() {
7201        // The sentinel stack was also reachable from the other side: the
7202        // `const` token of a TypeScript `x as const` assertion promoted a
7203        // live `let` slot to `Const` and suppressed every `=` until the
7204        // next `;`. Structurally that `const` is a child of an
7205        // `as_expression`, not a declaration keyword.
7206        check_metrics::<TypescriptParser>(
7207            "function f(x: number) {
7208                let y = x as const
7209                w = 3
7210            }",
7211            "foo.ts",
7212            |metric| {
7213                // `let` initializer (+1) and `w = 3` (+1); pre-#1277: 1.
7214                assert_eq!(metric.abc.assignments_sum(), 2);
7215            },
7216        );
7217    }
7218
7219    #[cfg(feature = "typescript")]
7220    #[test]
7221    fn typescript_const_declarator_shapes_stay_suppressed() {
7222        // Shapes the sentinel stack handled implicitly, which the
7223        // structural predicate must reproduce: *every* declarator under a
7224        // `const`-bearing `lexical_declaration` is suppressed, including
7225        // the second element of a multi-declarator list, a destructuring
7226        // pattern, and the defaults inside one — `c = 5`, the nested
7227        // `e = 6` and its `= {}`, `g = 7`, and the rest element's `h = 8`,
7228        // each one more pattern layer the predicate's climb has to cross
7229        // — while `for (const x of xs)` carries no `=` at all. Only
7230        // `let i = 3` and `var j = 4` count — the deliberate deviation
7231        // documented on `js_abc_compute!`. Gating on
7232        // `variable_declaration` instead of `lexical_declaration`,
7233        // dropping the `const` check, or stopping the climb at the
7234        // declarator's own `=`, each flips one of these rows. The
7235        // `for`-of row is the exception: it carries no `=`, so no change
7236        // to the predicate can move it. It pins the grammar shape instead
7237        // — a future grammar emitting a `variable_declarator` there would
7238        // start suppressing something that never counted.
7239        check_metrics::<TypescriptParser>(
7240            "function f(o: any, xs: number[]) {
7241                const a = 1, b = 2
7242                const {c = 5, d: {e = 6} = {}} = o
7243                const [g = 7, ...[h = 8]] = xs
7244                let i = 3
7245                var j = 4
7246                for (const x of xs) { k(x) }
7247            }",
7248            "foo.ts",
7249            |metric| {
7250                assert_eq!(metric.abc.assignments_sum(), 2);
7251            },
7252        );
7253    }
7254
7255    #[cfg(feature = "typescript")]
7256    #[test]
7257    fn typescript_const_initializer_value_assignments_still_count() {
7258        // TypeScript half of
7259        // `javascript_const_initializer_value_assignments_still_count`.
7260        check_metrics::<TypescriptParser>(
7261            "function m(o: any, a: any, b: any) { const x = (o.p = 1); const y = a || (b = 2); }",
7262            "foo.ts",
7263            |metric| {
7264                assert_eq!(metric.abc.assignments_sum(), 2);
7265            },
7266        );
7267    }
7268
7269    #[cfg(feature = "typescript")]
7270    #[test]
7271    fn typescript_branches_function_calls() {
7272        check_metrics::<TypescriptParser>(
7273            "class C {
7274                m(): void {
7275                    foo();              // +1
7276                    bar(1, 2);          // +1
7277                    new Date();         // +1
7278                }
7279            }",
7280            "foo.ts",
7281            |metric| {
7282                assert_eq!(metric.abc.branches_sum(), 3);
7283                insta::assert_json_snapshot!(metric.abc);
7284            },
7285        );
7286    }
7287
7288    #[cfg(feature = "typescript")]
7289    #[test]
7290    fn typescript_conditions_comparison_operators() {
7291        check_metrics::<TypescriptParser>(
7292            "class C {
7293                m(x: number, y: number): boolean {
7294                    return x == y       // +1
7295                        || x === y      // +1
7296                        || x != y       // +1
7297                        || x !== y      // +1
7298                        || x < y        // +1
7299                        || x <= y       // +1
7300                        || x > y        // +1
7301                        || x >= y;      // +1
7302                }
7303            }",
7304            "foo.ts",
7305            |metric| {
7306                assert_eq!(metric.abc.conditions_sum(), 8);
7307                insta::assert_json_snapshot!(metric.abc);
7308            },
7309        );
7310    }
7311
7312    #[cfg(feature = "typescript")]
7313    #[test]
7314    fn typescript_conditions_control_flow_arms() {
7315        check_metrics::<TypescriptParser>(
7316            "class C {
7317                m(x: number): number {
7318                    try {                       // +1 (try)
7319                        if (x > 0) {            // +1 (>)
7320                            return 1;
7321                        } else {                // +1 (else)
7322                            return -1;
7323                        }
7324                    } catch (e) {               // +1 (catch)
7325                        return 0;
7326                    }
7327                }
7328            }",
7329            "foo.ts",
7330            |metric| {
7331                assert_eq!(metric.abc.conditions_sum(), 4);
7332                insta::assert_json_snapshot!(metric.abc);
7333            },
7334        );
7335    }
7336
7337    #[cfg(feature = "typescript")]
7338    #[test]
7339    fn typescript_conditions_switch_case() {
7340        check_metrics::<TypescriptParser>(
7341            "class C {
7342                m(x: number): number {
7343                    switch (x) {
7344                        case 1:                 // +1
7345                            return 1;
7346                        case 2:                 // +1
7347                            return 2;
7348                        default:                // +0 (fallthrough, #469)
7349                            return 0;
7350                    }
7351                }
7352            }",
7353            "foo.ts",
7354            |metric| {
7355                assert_eq!(metric.abc.conditions_sum(), 2);
7356                insta::assert_json_snapshot!(metric.abc);
7357            },
7358        );
7359    }
7360
7361    #[cfg(feature = "typescript")]
7362    #[test]
7363    fn typescript_ternary_and_nullish() {
7364        check_metrics::<TypescriptParser>(
7365            "class C {
7366                m(x: number | null): number {
7367                    return x !== null           // +1 (!==)
7368                        ? x                     // +1 (ternary ?)
7369                        : 0;
7370                }
7371                n(x: number | null): number {
7372                    return x ?? 0;              // +1 (??)
7373                }
7374            }",
7375            "foo.ts",
7376            |metric| {
7377                assert_eq!(metric.abc.conditions_sum(), 3);
7378                insta::assert_json_snapshot!(metric.abc);
7379            },
7380        );
7381    }
7382
7383    #[cfg(feature = "typescript")]
7384    #[test]
7385    fn typescript_instanceof_counts_as_condition() {
7386        check_metrics::<TypescriptParser>(
7387            "class C {
7388                m(o: unknown): boolean {
7389                    return o instanceof C;      // +1
7390                }
7391            }",
7392            "foo.ts",
7393            |metric| {
7394                assert_eq!(metric.abc.conditions_sum(), 1);
7395                insta::assert_json_snapshot!(metric.abc);
7396            },
7397        );
7398    }
7399
7400    #[cfg(feature = "typescript")]
7401    #[test]
7402    fn typescript_generic_lt_gt_not_a_condition() {
7403        // `<T>` in `class C<T>` and `Array<number>` should not contribute
7404        // to conditions even though the tokens are `<` and `>`.
7405        check_metrics::<TypescriptParser>(
7406            "class C<T> {
7407                xs: Array<number> = [];
7408                m(): void {
7409                    const arr: Array<string> = [];   // suppressed const
7410                    void arr;
7411                }
7412            }",
7413            "foo.ts",
7414            |metric| {
7415                assert_eq!(metric.abc.conditions_sum(), 0);
7416                insta::assert_json_snapshot!(metric.abc);
7417            },
7418        );
7419    }
7420
7421    // #1297, TypeScript half: the `<` / `>` arm flipped from denying
7422    // `type_arguments` / `type_parameters` to allowing
7423    // `binary_expression`, and this pins the direction that flip could
7424    // have broken. `Array<string>` keeps a generic in scope so the
7425    // fixture proves both sides at once, and the assertion is the
7426    // grammar-dispatch §8 pin: on the function's own space the ABC
7427    // condition count equals the cyclomatic decision count
7428    // (`cyclomatic()` minus the per-space base of 1). Both are 2, one
7429    // per `if`.
7430    //
7431    // As with the Lua fixture, `Array<string>` leaves no ABC trace once
7432    // excluded, so nothing here fails if a later edit drops the
7433    // generic; that the generic reaches the arm is established by the
7434    // revert test, which fails this test when the gate is removed.
7435    //
7436    // TypeScript gets no JSX fixture because the `.ts` dialect has no
7437    // JSX to exercise: it lexes `return <div>…</div>` as a type
7438    // assertion and a comparison chain, sometimes with `ERROR` nodes and
7439    // sometimes without, depending on what follows the tag. Either way
7440    // the tokens land under `type_arguments` and `binary_expression`
7441    // rather than the `jsx_*` productions, so the construct that
7442    // motivated #1297 is only reachable through
7443    // `tsx_jsx_elements_are_not_conditions` below.
7444    //
7445    // That mis-parse still over-counts — a `.ts` file containing JSX
7446    // scores conditions this gate cannot exclude, because the grammar
7447    // genuinely reports a `binary_expression`. It is a wrong-dialect
7448    // input rather than a defect in this arm, and no allowlist can
7449    // distinguish it; see the same caveat on Kotlin's arm.
7450    #[cfg(feature = "typescript")]
7451    #[test]
7452    fn typescript_comparison_operators_still_count_alongside_generics() {
7453        check_func_space::<TypescriptParser, _>(
7454            "function m(xs: Array<string>, a: number, b: number): number {
7455                if (a < b) { return 1; }
7456                if (a > b) { return 2; }
7457                void xs;
7458                return 0;
7459            }",
7460            "foo.ts",
7461            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
7462        );
7463    }
7464
7465    // #1275, TypeScript half. Eleven grammar productions emit a bare
7466    // `?` and only `ternary_expression` is a decision; the other ten are
7467    // type syntax. This fixture exercises five of them — `optional_
7468    // parameter`, `property_signature`, `method_signature`,
7469    // `abstract_method_signature` and `public_field_definition`, plus an
7470    // `optional_type` in a tuple — against one real ternary over one
7471    // real `>`.
7472    //
7473    // Six type-syntax `?` means the numbers separate cleanly: 8 pre-fix,
7474    // 2 once the allowlist is aimed at `TernaryExpression`, 1 if it is
7475    // aimed at anything else (the ternary stops counting too), 0 if the
7476    // fixture stops parsing. Any partial gate — one that named some of
7477    // the type-syntax parents in a denylist instead — lands between 3
7478    // and 7 and is equally visible.
7479    #[cfg(feature = "typescript")]
7480    #[test]
7481    fn typescript_optional_type_syntax_is_not_a_condition() {
7482        check_metrics::<TypescriptParser>(
7483            "interface I { a?: string; m?(x: number): void; }
7484            abstract class K { f?: number; abstract g?(): void; }
7485            type Tup = [number, string?];
7486            function h(x?: number, y: number = 0): number { return y > 1 ? 1 : 2; }",
7487            "foo.ts",
7488            |metric| {
7489                assert_eq!(metric.abc.conditions_sum(), 2);
7490            },
7491        );
7492    }
7493
7494    // The explicit half of #1275's TypeScript decision: a conditional
7495    // type (`T extends U ? X : Y`) is resolved by the type checker and
7496    // erased before runtime, so its `?` is not a condition. That falls
7497    // out of the `TernaryExpression` allowlist rather than being named,
7498    // which is exactly why it needs its own test — the choice reads as
7499    // an omission otherwise, and nothing else here would notice if a
7500    // later edit added `ConditionalType` to the allowlist "for
7501    // symmetry".
7502    //
7503    // The real ternary below keeps the expectation off zero: 3 pre-fix,
7504    // 2 with the conditional type excluded, 1 if the ternary is
7505    // swallowed too.
7506    #[cfg(feature = "typescript")]
7507    #[test]
7508    fn typescript_conditional_type_is_not_a_condition() {
7509        check_metrics::<TypescriptParser>(
7510            "type Cond<T> = T extends string ? number : boolean;
7511            function pick(a: number, b: number): number { return a > b ? 1 : 2; }",
7512            "foo.ts",
7513            |metric| {
7514                assert_eq!(metric.abc.conditions_sum(), 2);
7515            },
7516        );
7517    }
7518
7519    // grammar-dispatch §2. `Typescript::QMARK2` is the enum entry for
7520    // the external scanner's `_ternary_qmark` token, and
7521    // tree-sitter-typescript's public symbol map folds it back onto
7522    // `anon_sym_QMARK`, so `kind_id()` never reports it — every ternary
7523    // `?` in the fixture below arrives as plain `QMARK`. Listing
7524    // `QMARK2` in the gated arm would therefore be dead code, and
7525    // leaving it out is safe only for as long as that mapping holds.
7526    // This pins both halves so a grammar bump that starts exposing the
7527    // alias fails here rather than silently zeroing every TypeScript
7528    // ternary.
7529    //
7530    // The fixture's only `?` is the ternary's, deliberately. An optional
7531    // parameter (`b?: number`) would emit a `QMARK` of its own and
7532    // satisfy the positive assertion on its own, leaving it true no
7533    // matter what id the ternary's `?` came back as — decoration rather
7534    // than the non-vacuity guard it is here for.
7535    #[cfg(feature = "typescript")]
7536    #[test]
7537    fn typescript_ternary_qmark_alias_stays_unreachable() {
7538        let parser = TypescriptParser::new(
7539            "function f(a: boolean): number { return a ? 1 : 2; }\n"
7540                .as_bytes()
7541                .to_vec(),
7542            std::path::Path::new("foo.ts"),
7543            None,
7544        );
7545        assert!(ast_has_kind_id(&parser, Typescript::QMARK as u16));
7546        assert!(!ast_has_kind_id(&parser, Typescript::QMARK2 as u16));
7547    }
7548
7549    #[cfg(feature = "typescript")]
7550    #[test]
7551    fn typescript_abstract_class_abc() {
7552        // Abstract methods have no body — they contribute nothing.
7553        check_metrics::<TypescriptParser>(
7554            "abstract class C {
7555                abstract a(): void;
7556                m(x: number): number {
7557                    if (x > 0) return 1;        // +1 condition
7558                    return 0;
7559                }
7560            }",
7561            "foo.ts",
7562            |metric| {
7563                assert_eq!(metric.abc.conditions_sum(), 1);
7564                assert_eq!(metric.abc.branches_sum(), 0);
7565                insta::assert_json_snapshot!(metric.abc);
7566            },
7567        );
7568    }
7569
7570    #[cfg(feature = "typescript")]
7571    #[test]
7572    fn typescript_interface_abc_zero() {
7573        check_metrics::<TypescriptParser>(
7574            "interface I {
7575                a(): void;
7576                b(): number;
7577                p: string;
7578            }",
7579            "foo.ts",
7580            |metric| {
7581                assert_eq!(metric.abc.assignments_sum(), 0);
7582                assert_eq!(metric.abc.branches_sum(), 0);
7583                assert_eq!(metric.abc.conditions_sum(), 0);
7584                insta::assert_json_snapshot!(metric.abc);
7585            },
7586        );
7587    }
7588
7589    #[cfg(feature = "typescript")]
7590    #[test]
7591    fn typescript_arrow_field_contributes_abc() {
7592        // Arrow function class members are function spaces; their
7593        // assignments/branches/conditions are counted.
7594        check_metrics::<TypescriptParser>(
7595            "class C {
7596                arrow = (x: number) => {
7597                    if (x > 0) {                // +1 condition
7598                        return foo();           // +1 branch
7599                    }
7600                    return 0;
7601                };
7602            }",
7603            "foo.ts",
7604            |metric| {
7605                assert_eq!(metric.abc.conditions_sum(), 1);
7606                assert_eq!(metric.abc.branches_sum(), 1);
7607                insta::assert_json_snapshot!(metric.abc);
7608            },
7609        );
7610    }
7611
7612    #[cfg(feature = "typescript")]
7613    #[test]
7614    fn typescript_parameter_property_init_not_assignment() {
7615        // Parameter properties don't introduce a `=` token themselves;
7616        // only the explicit `let z = 0` body assignment is counted.
7617        // The class field initializer `f: number = 0` likewise has a `=`
7618        // that DOES count (matches `typescript_assignments_basic`).
7619        check_metrics::<TypescriptParser>(
7620            "class C {
7621                f: number = 0;
7622                constructor(public x: number, private y: string) {
7623                    let z = 0;
7624                }
7625            }",
7626            "foo.ts",
7627            |metric| {
7628                // f's initializer + `let z = 0` = 2 assignments; the
7629                // parameter properties contribute zero.
7630                assert_eq!(metric.abc.assignments_sum(), 2);
7631                insta::assert_json_snapshot!(metric.abc);
7632            },
7633        );
7634    }
7635
7636    // TSX parity
7637
7638    #[cfg(feature = "typescript")]
7639    #[test]
7640    fn tsx_assignments_basic() {
7641        check_metrics::<TsxParser>(
7642            "class C {
7643                m(): void {
7644                    let x = 0;
7645                    x = 1;
7646                    x += 2;
7647                    x++;
7648                }
7649            }",
7650            "foo.tsx",
7651            |metric| {
7652                assert_eq!(metric.abc.assignments_sum(), 4);
7653                insta::assert_json_snapshot!(metric.abc);
7654            },
7655        );
7656    }
7657
7658    #[cfg(feature = "typescript")]
7659    #[test]
7660    fn tsx_const_excluded_from_assignments() {
7661        check_metrics::<TsxParser>(
7662            "class C {
7663                m(): void {
7664                    const a = 1;
7665                    let b = 2;
7666                }
7667            }",
7668            "foo.tsx",
7669            |metric| {
7670                assert_eq!(metric.abc.assignments_sum(), 1);
7671                insta::assert_json_snapshot!(metric.abc);
7672            },
7673        );
7674    }
7675
7676    #[cfg(feature = "typescript")]
7677    #[test]
7678    fn tsx_branches_function_calls() {
7679        check_metrics::<TsxParser>(
7680            "class C {
7681                m(): void {
7682                    foo();
7683                    new Date();
7684                }
7685            }",
7686            "foo.tsx",
7687            |metric| {
7688                assert_eq!(metric.abc.branches_sum(), 2);
7689                insta::assert_json_snapshot!(metric.abc);
7690            },
7691        );
7692    }
7693
7694    #[cfg(feature = "typescript")]
7695    #[test]
7696    fn tsx_conditions_comparison_operators() {
7697        check_metrics::<TsxParser>(
7698            "class C {
7699                m(x: number, y: number): boolean {
7700                    return x == y || x < y || x >= y;
7701                }
7702            }",
7703            "foo.tsx",
7704            |metric| {
7705                assert_eq!(metric.abc.conditions_sum(), 3);
7706                insta::assert_json_snapshot!(metric.abc);
7707            },
7708        );
7709    }
7710
7711    #[cfg(feature = "typescript")]
7712    #[test]
7713    fn tsx_conditions_control_flow_arms() {
7714        check_metrics::<TsxParser>(
7715            "class C {
7716                m(x: number): number {
7717                    try {
7718                        if (x > 0) return 1;
7719                        else return -1;
7720                    } catch (e) {
7721                        return 0;
7722                    }
7723                }
7724            }",
7725            "foo.tsx",
7726            |metric| {
7727                assert_eq!(metric.abc.conditions_sum(), 4);
7728                insta::assert_json_snapshot!(metric.abc);
7729            },
7730        );
7731    }
7732
7733    #[cfg(feature = "typescript")]
7734    #[test]
7735    fn tsx_conditions_switch_case() {
7736        check_metrics::<TsxParser>(
7737            "class C {
7738                m(x: number): number {
7739                    switch (x) {
7740                        case 1: return 1;       // +1
7741                        case 2: return 2;       // +1
7742                        default: return 0;      // +0 (fallthrough, #469)
7743                    }
7744                }
7745            }",
7746            "foo.tsx",
7747            |metric| {
7748                assert_eq!(metric.abc.conditions_sum(), 2);
7749                insta::assert_json_snapshot!(metric.abc);
7750            },
7751        );
7752    }
7753
7754    #[cfg(feature = "typescript")]
7755    #[test]
7756    fn tsx_ternary_and_nullish() {
7757        check_metrics::<TsxParser>(
7758            "class C {
7759                m(x: number | null): number {
7760                    return x !== null ? x : 0;
7761                }
7762                n(x: number | null): number { return x ?? 0; }
7763            }",
7764            "foo.tsx",
7765            |metric| {
7766                assert_eq!(metric.abc.conditions_sum(), 3);
7767                insta::assert_json_snapshot!(metric.abc);
7768            },
7769        );
7770    }
7771
7772    #[cfg(feature = "typescript")]
7773    #[test]
7774    fn tsx_instanceof_counts_as_condition() {
7775        check_metrics::<TsxParser>(
7776            "class C { m(o: unknown): boolean { return o instanceof C; } }",
7777            "foo.tsx",
7778            |metric| {
7779                assert_eq!(metric.abc.conditions_sum(), 1);
7780                insta::assert_json_snapshot!(metric.abc);
7781            },
7782        );
7783    }
7784
7785    #[cfg(feature = "typescript")]
7786    #[test]
7787    fn tsx_generic_lt_gt_not_a_condition() {
7788        check_metrics::<TsxParser>(
7789            "class C<T> { xs: Array<number> = []; }",
7790            "foo.tsx",
7791            |metric| {
7792                assert_eq!(metric.abc.conditions_sum(), 0);
7793                insta::assert_json_snapshot!(metric.abc);
7794            },
7795        );
7796    }
7797
7798    // #1297: a JSX tag delimiter is markup punctuation, not a
7799    // comparison. The `<` / `>` arm denied only `type_arguments` and
7800    // `type_parameters`, so every tag bracket scored a condition —
7801    // `jsx_opening_element` contributes both tokens, `jsx_closing_element`
7802    // a `>` (its `</` is one token) and `jsx_self_closing_element` a `<`
7803    // (its `/>` is one token).
7804    //
7805    // The fixture pairs seven such brackets — two elements' opening and
7806    // closing tags, plus one self-closing tag — against one genuine
7807    // `a < b`, so every way of mis-aiming the gate lands on its own
7808    // number: 8 pre-fix, 1 once it allows `BinaryExpression`, 0 if it
7809    // allows the wrong parent or the fixture stops parsing. Asserting 0
7810    // on a JSX-only body would not have separated those last two.
7811    #[cfg(feature = "typescript")]
7812    #[test]
7813    fn tsx_jsx_elements_are_not_conditions() {
7814        check_metrics::<TsxParser>(
7815            "function f(a: number, b: number) {
7816                if (a < b) { return <div className=\"x\"><span>hi</span></div>; }
7817                return <br />;
7818            }",
7819            "foo.tsx",
7820            |metric| {
7821                assert_eq!(metric.abc.conditions_sum(), 1);
7822                // Fixture-presence anchor: the `className="x"` attribute
7823                // `=` is this fixture's only assignment, so the test
7824                // fails if a later edit drops the JSX. Without it the
7825                // surviving `a < b` alone also scores 1, and the
7826                // conditions assertion would pass while proving nothing.
7827                assert_eq!(metric.abc.assignments_sum(), 1);
7828            },
7829        );
7830    }
7831
7832    // #1275 in the second expansion of `ts_abc_compute!`. TSX shares
7833    // TypeScript's `?` productions and its own `TernaryExpression` /
7834    // `QMARK` ids, so the gate is a distinct instantiation and needs its
7835    // own fixture — a passing TypeScript test says nothing about the
7836    // macro's other expansion. Four type-syntax `?` plus one `>` and one
7837    // ternary: 6 pre-fix, 2 after, 1 if the allowlist is misaimed.
7838    #[cfg(feature = "typescript")]
7839    #[test]
7840    fn tsx_optional_type_syntax_is_not_a_condition() {
7841        check_metrics::<TsxParser>(
7842            "interface I { a?: string; m?(x: number): void; }
7843            class K { f?: number; }
7844            function h(x?: number, y: number = 0): number { return y > 1 ? 1 : 2; }",
7845            "foo.tsx",
7846            |metric| {
7847                assert_eq!(metric.abc.conditions_sum(), 2);
7848            },
7849        );
7850    }
7851
7852    // The TSX half of `typescript_conditional_type_is_not_a_condition`.
7853    // `ts_abc_compute!` expands twice and the two expansions are
7854    // independent code (grammar-dispatch §11): adding `ConditionalType`
7855    // to the allowlist "for symmetry" fails only the TypeScript test
7856    // without this one, which reads as the TSX expansion being fine
7857    // rather than untested. `conditional_type` is in the tsx grammar's
7858    // `?` set exactly as it is in typescript's.
7859    #[cfg(feature = "typescript")]
7860    #[test]
7861    fn tsx_conditional_type_is_not_a_condition() {
7862        check_metrics::<TsxParser>(
7863            "type Cond<T> = T extends string ? number : boolean;
7864            function pick(a: number, b: number): number { return a > b ? 1 : 2; }",
7865            "foo.tsx",
7866            |metric| {
7867                assert_eq!(metric.abc.conditions_sum(), 2);
7868            },
7869        );
7870    }
7871
7872    // The TSX half of `typescript_ternary_qmark_alias_stays_unreachable`
7873    // — the tsx grammar declares the same `_ternary_qmark` external and
7874    // maps it back onto `anon_sym_QMARK` at its own id. Same
7875    // single-`?` fixture rule; see that test for why.
7876    #[cfg(feature = "typescript")]
7877    #[test]
7878    fn tsx_ternary_qmark_alias_stays_unreachable() {
7879        let parser = TsxParser::new(
7880            "function f(a: boolean): number { return a ? 1 : 2; }\n"
7881                .as_bytes()
7882                .to_vec(),
7883            std::path::Path::new("foo.tsx"),
7884            None,
7885        );
7886        assert!(ast_has_kind_id(&parser, Tsx::QMARK as u16));
7887        assert!(!ast_has_kind_id(&parser, Tsx::QMARK2 as u16));
7888    }
7889
7890    #[cfg(feature = "typescript")]
7891    #[test]
7892    fn tsx_abstract_class_abc() {
7893        check_metrics::<TsxParser>(
7894            "abstract class C {
7895                abstract a(): void;
7896                m(x: number): number {
7897                    if (x > 0) return 1;
7898                    return 0;
7899                }
7900            }",
7901            "foo.tsx",
7902            |metric| {
7903                assert_eq!(metric.abc.conditions_sum(), 1);
7904                assert_eq!(metric.abc.branches_sum(), 0);
7905                insta::assert_json_snapshot!(metric.abc);
7906            },
7907        );
7908    }
7909
7910    #[cfg(feature = "typescript")]
7911    #[test]
7912    fn tsx_interface_abc_zero() {
7913        check_metrics::<TsxParser>(
7914            "interface I { a(): void; p: string; }",
7915            "foo.tsx",
7916            |metric| {
7917                assert_eq!(metric.abc.assignments_sum(), 0);
7918                assert_eq!(metric.abc.branches_sum(), 0);
7919                assert_eq!(metric.abc.conditions_sum(), 0);
7920                insta::assert_json_snapshot!(metric.abc);
7921            },
7922        );
7923    }
7924
7925    #[cfg(feature = "typescript")]
7926    #[test]
7927    fn tsx_arrow_field_contributes_abc() {
7928        check_metrics::<TsxParser>(
7929            "class C {
7930                arrow = (x: number) => {
7931                    if (x > 0) return foo();
7932                    return 0;
7933                };
7934            }",
7935            "foo.tsx",
7936            |metric| {
7937                assert_eq!(metric.abc.conditions_sum(), 1);
7938                assert_eq!(metric.abc.branches_sum(), 1);
7939                insta::assert_json_snapshot!(metric.abc);
7940            },
7941        );
7942    }
7943
7944    #[cfg(feature = "typescript")]
7945    #[test]
7946    fn tsx_asi_const_does_not_suppress_later_assignments() {
7947        // TSX half of the #1277 cluster; see
7948        // `typescript_asi_const_does_not_suppress_later_assignments`.
7949        // TSX has its own `Const` / `VariableDeclarator` /
7950        // `LexicalDeclaration` kind ids, so the predicate is generated
7951        // separately and needs its own fixture.
7952        check_metrics::<TsxParser>(
7953            "function f() {
7954                const a = 1
7955                x = 2
7956                y = 3
7957            }",
7958            "foo.tsx",
7959            |metric| {
7960                assert_eq!(metric.abc.assignments_sum(), 2);
7961            },
7962        );
7963    }
7964
7965    #[cfg(feature = "typescript")]
7966    #[test]
7967    fn tsx_const_declarator_shapes_stay_suppressed() {
7968        // TSX half of `typescript_const_declarator_shapes_stay_suppressed`.
7969        check_metrics::<TsxParser>(
7970            "function f(o: any, xs: number[]) {
7971                const a = 1, b = 2
7972                const {c = 5, d: {e = 6} = {}} = o
7973                const [g = 7, ...[h = 8]] = xs
7974                let i = 3
7975                var j = 4
7976                for (const x of xs) { k(x) }
7977            }",
7978            "foo.tsx",
7979            |metric| {
7980                assert_eq!(metric.abc.assignments_sum(), 2);
7981            },
7982        );
7983    }
7984
7985    #[cfg(feature = "typescript")]
7986    #[test]
7987    fn tsx_parameter_property_init_not_assignment() {
7988        // Parameter properties contribute no `=`; the body's `let z = 0`
7989        // and the field initializer do.
7990        check_metrics::<TsxParser>(
7991            "class C {
7992                f: number = 0;
7993                constructor(public x: number) { let z = 0; }
7994            }",
7995            "foo.tsx",
7996            |metric| {
7997                assert_eq!(metric.abc.assignments_sum(), 2);
7998                insta::assert_json_snapshot!(metric.abc);
7999            },
8000        );
8001    }
8002
8003    // --- Ruby ABC tests ---------------------------------------------------
8004    //
8005    // Each Ruby `assignment` / `operator_assignment` is one assignment
8006    // regardless of whether the LHS is a local, instance, or class
8007    // variable. Every `call` / `super` / `yield` is one branch. Every
8008    // comparison-operator token inside a `binary` node plus each
8009    // `else` / `elsif` / `when` / `then` / `?` / `rescue` clause is
8010    // one condition.
8011
8012    #[cfg(feature = "ruby")]
8013    #[test]
8014    fn ruby_zero_abc() {
8015        check_metrics::<RubyParser>("\n", "foo.rb", |metric| {
8016            assert_eq!(metric.abc.assignments_sum(), 0);
8017            assert_eq!(metric.abc.branches_sum(), 0);
8018            assert_eq!(metric.abc.conditions_sum(), 0);
8019            insta::assert_json_snapshot!(metric.abc);
8020        });
8021    }
8022
8023    #[cfg(feature = "ruby")]
8024    #[test]
8025    fn ruby_simple_assignment() {
8026        check_metrics::<RubyParser>("def f\n  a = 1\n  b = 2\nend\n", "foo.rb", |metric| {
8027            assert_eq!(metric.abc.assignments_sum(), 2);
8028            assert_eq!(metric.abc.branches_sum(), 0);
8029            assert_eq!(metric.abc.conditions_sum(), 0);
8030            insta::assert_json_snapshot!(metric.abc);
8031        });
8032    }
8033
8034    #[cfg(feature = "ruby")]
8035    #[test]
8036    fn ruby_augmented_assignment() {
8037        // `+=`, `-=`, `*=` are `operator_assignment` nodes — each is
8038        // one assignment. Plain `=` to set the initial value adds one
8039        // more.
8040        check_metrics::<RubyParser>(
8041            "def f(x)\n  a = 0\n  a += x\n  a -= 1\n  a *= 2\nend\n",
8042            "foo.rb",
8043            |metric| {
8044                assert_eq!(metric.abc.assignments_sum(), 4);
8045                insta::assert_json_snapshot!(metric.abc);
8046            },
8047        );
8048    }
8049
8050    #[cfg(feature = "ruby")]
8051    #[test]
8052    fn ruby_logical_augmented_assignment() {
8053        // `||=` and `&&=` are also `operator_assignment` nodes.
8054        check_metrics::<RubyParser>("def f\n  @x ||= 0\n  @x &&= 1\nend\n", "foo.rb", |metric| {
8055            assert_eq!(metric.abc.assignments_sum(), 2);
8056            insta::assert_json_snapshot!(metric.abc);
8057        });
8058    }
8059
8060    #[cfg(feature = "ruby")]
8061    #[test]
8062    fn ruby_method_call_branch() {
8063        // Each method invocation is one branch.
8064        check_metrics::<RubyParser>(
8065            "def f(obj)\n  foo()\n  obj.bar(1)\nend\n",
8066            "foo.rb",
8067            |metric| {
8068                assert_eq!(metric.abc.branches_sum(), 2);
8069                insta::assert_json_snapshot!(metric.abc);
8070            },
8071        );
8072    }
8073
8074    #[cfg(feature = "ruby")]
8075    #[test]
8076    fn ruby_super_and_yield_branches() {
8077        // `super` and `yield` both count as branches (control-pass).
8078        check_metrics::<RubyParser>("def f\n  super\n  yield\nend\n", "foo.rb", |metric| {
8079            assert_eq!(metric.abc.branches_sum(), 2);
8080            assert_eq!(metric.abc.assignments_sum(), 0);
8081            insta::assert_json_snapshot!(metric.abc);
8082        });
8083    }
8084
8085    #[cfg(feature = "ruby")]
8086    #[test]
8087    fn ruby_attr_macro_is_branch() {
8088        // `attr_accessor` is a `Call3` node and registers as a branch
8089        // like any method invocation.
8090        check_metrics::<RubyParser>("class A\n  attr_accessor :x\nend\n", "foo.rb", |metric| {
8091            assert_eq!(metric.abc.branches_sum(), 1);
8092            insta::assert_json_snapshot!(metric.abc);
8093        });
8094    }
8095
8096    #[cfg(feature = "ruby")]
8097    #[test]
8098    fn ruby_comparison_conditions() {
8099        // Each comparison operator is one condition.
8100        check_metrics::<RubyParser>(
8101            "def f(a, b)\n  a == b\n  a != b\n  a < b\n  a > b\n  a <= b\n  a >= b\nend\n",
8102            "foo.rb",
8103            |metric| {
8104                assert_eq!(metric.abc.conditions_sum(), 6);
8105                insta::assert_json_snapshot!(metric.abc);
8106            },
8107        );
8108    }
8109
8110    #[cfg(feature = "ruby")]
8111    #[test]
8112    fn ruby_superclass_clause_is_not_a_condition() {
8113        // Regression for #1280: a superclass clause spells its `<` with the
8114        // same `LT` token as a comparison, but parents it under
8115        // `superclass` rather than `binary`, so the parent gate excludes
8116        // it. Before the gate every subclass declaration scored a phantom
8117        // condition.
8118        // expected: 0 conditions — the file contains no conditional at all;
8119        // the single assignment is `x = 1`.
8120        check_metrics::<RubyParser>(
8121            "class Foo < Bar\n  def plain\n    x = 1\n  end\nend\n",
8122            "foo.rb",
8123            |metric| {
8124                assert_eq!(metric.abc.conditions_sum(), 0);
8125                assert_eq!(metric.abc.assignments_sum(), 1);
8126            },
8127        );
8128    }
8129
8130    #[cfg(feature = "ruby")]
8131    #[test]
8132    fn ruby_operator_method_name_is_not_a_condition() {
8133        // The `<` naming an operator method parents under `operator`, which
8134        // the `binary` gate likewise excludes (#1280). The body carries a
8135        // real comparison so the expected value is not the all-zero default:
8136        // 1 discriminates "only the name token is excluded" from both "the
8137        // gate excludes everything" (0) and "nothing is gated" (2).
8138        // expected: 1 condition — the `@v < other` comparison, not the `def <`.
8139        check_metrics::<RubyParser>("def <(other)\n  @v < other\nend\n", "foo.rb", |metric| {
8140            assert_eq!(metric.abc.conditions_sum(), 1);
8141        });
8142    }
8143
8144    #[cfg(feature = "ruby")]
8145    #[test]
8146    fn ruby_every_comparison_operator_method_name_is_not_a_condition() {
8147        // The sibling half of #1280. `<` is not special: every comparison
8148        // and equality token Ruby lets you `def` parents under `operator`
8149        // in that position, so gating only `LT` / `GT` left `def ==`,
8150        // `def <=`, `def >=`, `def <=>`, `def !=` and `def =~` each
8151        // scoring a phantom condition — measured at 1 apiece for a body
8152        // containing no conditional at all.
8153        // expected: 0 conditions per definition; only the name token is on
8154        // the line, so a single non-zero total localises the regression.
8155        check_metrics::<RubyParser>(
8156            "def ==(o)\n  1\nend\n\
8157             def !=(o)\n  1\nend\n\
8158             def <=(o)\n  1\nend\n\
8159             def >=(o)\n  1\nend\n\
8160             def <=>(o)\n  1\nend\n\
8161             def =~(o)\n  1\nend\n\
8162             def <(o)\n  1\nend\n\
8163             def >(o)\n  1\nend\n",
8164            "foo.rb",
8165            |metric| {
8166                assert_eq!(metric.abc.conditions_sum(), 0);
8167            },
8168        );
8169        // The positive control: the same tokens inside a `binary` are real
8170        // comparisons and still count, so the gate is not blanket
8171        // suppression.
8172        check_metrics::<RubyParser>(
8173            "def cmp(a, b)\n  a == b || a <= b || a <=> b\nend\n",
8174            "foo.rb",
8175            |metric| {
8176                assert_eq!(metric.abc.conditions_sum(), 3);
8177            },
8178        );
8179    }
8180
8181    #[cfg(feature = "ruby")]
8182    #[test]
8183    fn ruby_case_match_in_arms_are_conditions() {
8184        // Regression for #977: each non-wildcard `case … in` arm is one
8185        // ABC condition, matching Python's `case_clause` handling. Using
8186        // literal patterns (no comparison operators) isolates the
8187        // `in_clause` contribution from any operand tokens.
8188        // expected: 2 conditions — one per `in 1` / `in 2` arm.
8189        check_metrics::<RubyParser>(
8190            "def f(x)\n  case x\n  in 1 then :one\n  in 2 then :two\n  end\nend\n",
8191            "foo.rb",
8192            |metric| {
8193                assert_eq!(metric.abc.conditions_sum(), 2);
8194            },
8195        );
8196    }
8197
8198    #[cfg(feature = "ruby")]
8199    #[test]
8200    fn ruby_case_match_guarded_wildcard_is_a_condition() {
8201        // Regression for #977: a guarded wildcard arm `in _ if x` is not a
8202        // bare default and counts as one ABC condition, while the trailing
8203        // bare `in _` adds none.
8204        // expected: 2 conditions — the guarded `in _ if x` arm, plus its
8205        // guard slot. The guard predicate is a bare identifier, so before
8206        // #1454 it contributed nothing and the total was 1; the slot is
8207        // what makes `if x`, `if x.even?` and `if x > 0` agree.
8208        check_metrics::<RubyParser>(
8209            "def f(x)\n  case x\n  in _ if x then :y\n  in _ then :default\n  end\nend\n",
8210            "foo.rb",
8211            |metric| {
8212                assert_eq!(metric.abc.conditions_sum(), 2);
8213            },
8214        );
8215    }
8216
8217    #[cfg(feature = "ruby")]
8218    #[test]
8219    fn ruby_case_match_bare_wildcard_is_not_a_condition() {
8220        // Regression for #977: a `case … in` whose only arm is the bare
8221        // wildcard `in _` (no guard) is the default arm and contributes no
8222        // ABC condition, keeping ABC and cyclomatic in lockstep on the
8223        // same construct.
8224        // expected: 0 conditions.
8225        check_metrics::<RubyParser>(
8226            "def f(x)\n  case x\n  in _ then :default\n  end\nend\n",
8227            "foo.rb",
8228            |metric| {
8229                assert_eq!(metric.abc.conditions_sum(), 0);
8230            },
8231        );
8232    }
8233
8234    #[cfg(feature = "ruby")]
8235    #[test]
8236    fn ruby_bare_predicate_control_flow_counts_one_condition() {
8237        // Regression for #696: idiomatic Ruby bare predicates
8238        // (`if flag` / `while flag` / `unless flag` / `until flag`) each
8239        // count one unary condition, matching Rust / C# / PHP / Python. The
8240        // condition field is read for both block and modifier forms.
8241        //
8242        // expected: 8 conditions — four block forms (`if`/`unless`/`while`/
8243        // `until`) plus the same four as modifiers, one each.
8244        check_metrics::<RubyParser>(
8245            "def f(flag)\n  if flag\n    a\n  end\n  unless flag\n    b\n  end\n  while flag\n    c\n  end\n  until flag\n    d\n  end\n  a if flag\n  b unless flag\n  c while flag\n  d until flag\nend\n",
8246            "foo.rb",
8247            |metric| {
8248                assert_eq!(metric.abc.conditions_sum(), 8);
8249            },
8250        );
8251    }
8252
8253    #[cfg(feature = "ruby")]
8254    #[test]
8255    fn ruby_bare_predicate_does_not_double_count_comparison_or_chain() {
8256        // `if a == b` counts only the `==` comparison (the condition field
8257        // is a `binary` node, adding nothing). `if a && b` counts the two
8258        // chain operands via the `&&` walker, again with the condition-field
8259        // arm adding nothing — so neither shape is double-counted (#696).
8260        //
8261        // expected: 3 — `==` (1) + the `a`,`b` operands of `&&` (2).
8262        check_metrics::<RubyParser>(
8263            "def f(a, b)\n  if a == b\n    x\n  end\n  if a && b\n    y\n  end\nend\n",
8264            "foo.rb",
8265            |metric| {
8266                assert_eq!(metric.abc.conditions_sum(), 3);
8267            },
8268        );
8269    }
8270
8271    #[cfg(feature = "ruby")]
8272    #[test]
8273    fn ruby_spaceship_and_case_equality() {
8274        // `<=>` and `===` are comparison operators (conditions).
8275        check_metrics::<RubyParser>(
8276            "def f(a, b)\n  a <=> b\n  a === b\nend\n",
8277            "foo.rb",
8278            |metric| {
8279                assert_eq!(metric.abc.conditions_sum(), 2);
8280                insta::assert_json_snapshot!(metric.abc);
8281            },
8282        );
8283    }
8284
8285    #[cfg(feature = "ruby")]
8286    #[test]
8287    fn ruby_ternary_condition() {
8288        // The `?` ternary marker is one condition; the inner `==` is
8289        // another.
8290        check_metrics::<RubyParser>("def f(x)\n  x == 0 ? :z : :nz\nend\n", "foo.rb", |metric| {
8291            assert_eq!(metric.abc.conditions_sum(), 2);
8292            insta::assert_json_snapshot!(metric.abc);
8293        });
8294    }
8295
8296    // Issue #1161. Ruby's ternary carried only the `?` token arm, so
8297    // `a ? !b : !c` scored 1 against the 4 that Java, C#, Groovy, the C
8298    // family, the JS family, PHP and Perl all report for the same
8299    // expression (#1102) — and `ruby_inspect_container`'s `Conditional`
8300    // boolean-context seed was unreachable for the same reason.
8301    //
8302    // Every expectation below is the value its C++ sibling
8303    // (`cpp_ternary_operand_slots_count_as_unary_conditions`) already
8304    // asserts for the same expression, so the two read as one table.
8305    #[cfg(feature = "ruby")]
8306    #[test]
8307    fn ruby_ternary_operand_slots_count_as_unary_conditions() {
8308        // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4.
8309        check_metrics::<RubyParser>("def f\n  x = a ? !b : !c\nend\n", "foo.rb", |metric| {
8310            assert_eq!(metric.abc.conditions_sum(), 4);
8311        });
8312        // No-double-count pin, and the assertion that catches the trap
8313        // this grammar sets: `-b` and `!b` are the SAME node kind
8314        // (`unary:284`), separated only by child(0). Routing the branch
8315        // slots through `ruby_inspect_container` — which tests for the
8316        // `!` token, not for the kind — is what keeps this at 2. An
8317        // implementation keying on `Unary` reads 3 here.
8318        // `?` (1) + `>` (1) = 2, unchanged by the fix: the parenthesised
8319        // condition unwraps to a `binary`, which is not a boolean
8320        // terminal, and neither branch is negated.
8321        check_metrics::<RubyParser>("def f\n  x = (a > 0) ? b : -b\nend\n", "foo.rb", |metric| {
8322            assert_eq!(metric.abc.conditions_sum(), 2);
8323        });
8324        // Nested — Ruby needs the inner ternary parenthesised. Outer `?`
8325        // (1) + outer condition `a` (1) + inner `?` (1) + inner
8326        // condition `b` (1) = 4. The outer consequence unwraps to the
8327        // inner `conditional`, which is neither a boolean terminal nor a
8328        // further paren / `!` layer, so it adds nothing on its own; the
8329        // inner ternary is reached by the walk, not by descent.
8330        check_metrics::<RubyParser>(
8331            "def f\n  x = a ? (b ? c : d) : e\nend\n",
8332            "foo.rb",
8333            |metric| {
8334                assert_eq!(metric.abc.conditions_sum(), 4);
8335            },
8336        );
8337        // A parenthesised condition, pinning the `is_parens` unwrap on
8338        // the condition slot: `(a)` is `parenthesized_statements`, not a
8339        // boolean terminal, so it reaches the walker's `else` fallback
8340        // and only `ruby_inspect_container` can resolve it.
8341        // `?` (1) + `(a)` (1) + `!b` (1) + `!c` (1) = 4; drop the
8342        // fallback and this reads 3 while every other case here holds.
8343        check_metrics::<RubyParser>("def f\n  x = (a) ? !b : !c\nend\n", "foo.rb", |metric| {
8344            assert_eq!(metric.abc.conditions_sum(), 4);
8345        });
8346        // A negated condition takes the same fallback through the `!`
8347        // unwrap rather than the paren one. `?` (1) + `!a` (1) = 2.
8348        check_metrics::<RubyParser>("def f\n  x = !a ? b : c\nend\n", "foo.rb", |metric| {
8349            assert_eq!(metric.abc.conditions_sum(), 2);
8350        });
8351    }
8352
8353    // The boolean-context seed must discriminate between the condition
8354    // slot and the two branch slots — not merely exist. None of the
8355    // fixtures above can tell the difference: their branches are either
8356    // `!`-unaries (which set the flag inside the unwrap loop regardless
8357    // of the seed) or kinds the loop breaks on before any terminal test.
8358    // A seed that returned `true` for every slot of a `Conditional`
8359    // leaves all five at their asserted values and fails nothing.
8360    //
8361    // A parenthesised *branch* is the input that separates them: the
8362    // unwrap reaches a bare terminal, so only the seed decides whether
8363    // it counts. `?` (1) + condition `a` (1) = 2 in both directions.
8364    #[cfg(feature = "ruby")]
8365    #[test]
8366    fn ruby_ternary_branch_operands_are_not_double_counted() {
8367        check_metrics::<RubyParser>("def f\n  x = a ? (b) : c\nend\n", "foo.rb", |metric| {
8368            assert_eq!(metric.abc.conditions_sum(), 2);
8369        });
8370        check_metrics::<RubyParser>("def f\n  x = a ? b : (c)\nend\n", "foo.rb", |metric| {
8371            assert_eq!(metric.abc.conditions_sum(), 2);
8372        });
8373        // The same pair with a comment before the operand. Comments are
8374        // tree-sitter `extras`, so they become the branch's previous
8375        // sibling — which is why the seed asks the grammar which child
8376        // is the `condition` field rather than testing that sibling for
8377        // `?` / `:` as the C family does. Under the token form both of
8378        // these read 3.
8379        check_metrics::<RubyParser>(
8380            "def f\n  x = a ?\n    # note\n    (b) : c\nend\n",
8381            "foo.rb",
8382            |metric| {
8383                assert_eq!(metric.abc.conditions_sum(), 2);
8384            },
8385        );
8386        check_metrics::<RubyParser>(
8387            "def f\n  x = a ? b :\n    # note\n    (c)\nend\n",
8388            "foo.rb",
8389            |metric| {
8390                assert_eq!(metric.abc.conditions_sum(), 2);
8391            },
8392        );
8393    }
8394
8395    #[cfg(feature = "ruby")]
8396    #[test]
8397    fn ruby_case_when_arms() {
8398        // Each `when` named clause and the `else` clause count as one
8399        // condition each; the `case` head and the implicit `then`
8400        // wrappers do not.
8401        check_metrics::<RubyParser>(
8402            "def f(x)\n  case x\n  when 1 then 'one'\n  when 2 then 'two'\n  else 'other'\n  end\nend\n",
8403            "foo.rb",
8404            |metric| {
8405                // 2 `when` + 1 `else` = 3 conditions.
8406                assert_eq!(metric.abc.conditions_sum(), 3);
8407                insta::assert_json_snapshot!(metric.abc);
8408            },
8409        );
8410    }
8411
8412    #[cfg(feature = "ruby")]
8413    #[test]
8414    fn ruby_elsif_and_else() {
8415        // `elsif` and `else` named clauses are conditions; their inner
8416        // `then` wrappers are not.
8417        check_metrics::<RubyParser>(
8418            "def f(x)\n  if x > 0\n    1\n  elsif x < 0\n    -1\n  else\n    0\n  end\nend\n",
8419            "foo.rb",
8420            |metric| {
8421                // `>`(1) + `elsif`(1) + `<`(1) + `else`(1) = 4.
8422                assert_eq!(metric.abc.conditions_sum(), 4);
8423                insta::assert_json_snapshot!(metric.abc);
8424            },
8425        );
8426    }
8427
8428    #[cfg(feature = "ruby")]
8429    #[test]
8430    fn ruby_rescue_clause_condition() {
8431        // The `rescue` named clause is one condition; the `rescue`
8432        // keyword token (`Rescue2`) is not counted on its own.
8433        // `do_it` without parens is an `identifier`, not a `call`, so
8434        // it contributes no branch. `handle(e)` is a `call` (1 branch).
8435        check_metrics::<RubyParser>(
8436            "def f\n  begin\n    do_it\n  rescue StandardError => e\n    handle(e)\n  end\nend\n",
8437            "foo.rb",
8438            |metric| {
8439                assert_eq!(metric.abc.conditions_sum(), 1);
8440                assert_eq!(metric.abc.branches_sum(), 1);
8441                insta::assert_json_snapshot!(metric.abc);
8442            },
8443        );
8444    }
8445
8446    #[cfg(feature = "ruby")]
8447    #[test]
8448    fn ruby_class_complex_function() {
8449        // Mixed: assignment(=), branch(call), conditions(`>` and `==`).
8450        check_metrics::<RubyParser>(
8451            "class A\n  def f(a, b)\n    sum = a + b\n    if sum > 0 && b == 0\n      foo(sum)\n    end\n  end\nend\n",
8452            "foo.rb",
8453            |metric| {
8454                assert_eq!(metric.abc.assignments_sum(), 1);
8455                assert_eq!(metric.abc.branches_sum(), 1);
8456                // `>`(1) + `==`(1) = 2 conditions. `if` is not a
8457                // token; `&&` is `AMPAMP` and is not counted (see
8458                // the module-level `Stats` doc-comment for the
8459                // cross-language policy; #395, walker tracked in
8460                // #403).
8461                assert_eq!(metric.abc.conditions_sum(), 2);
8462                insta::assert_json_snapshot!(metric.abc);
8463            },
8464        );
8465    }
8466
8467    #[cfg(feature = "ruby")]
8468    #[test]
8469    fn ruby_unary_conditions_in_chain() {
8470        // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
8471        // `&&` / `||` chain is one condition. `a && b || c` → a, b, c each
8472        // contribute one. Ruby's `if` keyword is not a condition token.
8473        // expected: 3 unary conditions (matches the Java byte-equivalent).
8474        check_metrics::<RubyParser>(
8475            "def f(a, b, c)\n  if a && b || c\n    puts \"x\"\n  end\nend\n",
8476            "foo.rb",
8477            |metric| {
8478                assert_eq!(metric.abc.conditions_sum(), 3);
8479            },
8480        );
8481    }
8482
8483    #[cfg(feature = "ruby")]
8484    #[test]
8485    fn ruby_keyword_and_or_chain_counts_operands() {
8486        // The keyword forms `and` / `or` get the same Rule 9 treatment as
8487        // `&&` / `||`. expected: 3 unary conditions (a, b, c).
8488        check_metrics::<RubyParser>(
8489            "def f(a, b, c)\n  if a and b or c\n    puts \"x\"\n  end\nend\n",
8490            "foo.rb",
8491            |metric| {
8492                assert_eq!(metric.abc.conditions_sum(), 3);
8493            },
8494        );
8495    }
8496
8497    #[cfg(feature = "ruby")]
8498    #[test]
8499    fn ruby_negated_operand_is_unary_condition() {
8500        // A `!`-negated operand unwraps the `unary` node to the inner
8501        // identifier. expected: 2 (`a` and the `!b` operand).
8502        check_metrics::<RubyParser>(
8503            "def f(a, b)\n  if a && !b\n    puts \"x\"\n  end\nend\n",
8504            "foo.rb",
8505            |metric| {
8506                assert_eq!(metric.abc.conditions_sum(), 2);
8507            },
8508        );
8509    }
8510
8511    #[cfg(feature = "ruby")]
8512    #[test]
8513    fn ruby_comparison_operands_add_nothing() {
8514        // Isolation for Rule 9 (issue #557): when the `&&` operands are
8515        // themselves comparisons, the unary-condition walker must add
8516        // nothing on top of the two `>` comparisons already counted as
8517        // conditions — distinguishing the gap (bare boolean operands)
8518        // from ordinary relational conditions. Mirrors the Kotlin and
8519        // Elixir isolation tests. expected: 2 (the two `>` comparisons).
8520        check_metrics::<RubyParser>(
8521            "def f(x, y)\n  if x > 0 && y > 0\n    puts \"x\"\n  end\nend\n",
8522            "foo.rb",
8523            |metric| {
8524                assert_eq!(metric.abc.conditions_sum(), 2);
8525            },
8526        );
8527    }
8528
8529    // ---------------------------------------------------------------
8530    // Default-impl placeholder smoke tests (audited in #188).
8531    //
8532    // These tests assert that the *current* default-impl languages
8533    // return ABC = 0/0/0 for source that DOES contain branches,
8534    // conditions, and assignments. When the real impl lands for any
8535    // of these languages, the corresponding assertion below will fire
8536    // — the implementer must update the expected values, which is the
8537    // gate. Tag the follow-up issue in each test.
8538    // ---------------------------------------------------------------
8539
8540    // --- Python ABC ---------------------------------------------------
8541
8542    #[cfg(feature = "python")]
8543    #[test]
8544    fn python_empty_module_zero() {
8545        check_metrics::<PythonParser>("", "empty.py", |metric| {
8546            assert_eq!(metric.abc.assignments_sum(), 0);
8547            assert_eq!(metric.abc.branches_sum(), 0);
8548            assert_eq!(metric.abc.conditions_sum(), 0);
8549            insta::assert_json_snapshot!(metric.abc);
8550        });
8551    }
8552
8553    #[cfg(feature = "python")]
8554    #[test]
8555    fn python_plain_assignments_count() {
8556        // Three plain `=` assignments → A=3. No branches, no conditions.
8557        check_metrics::<PythonParser>("x = 1\ny = 2\nz = x\n", "foo.py", |metric| {
8558            assert_eq!(metric.abc.assignments_sum(), 3);
8559            assert_eq!(metric.abc.branches_sum(), 0);
8560            assert_eq!(metric.abc.conditions_sum(), 0);
8561            insta::assert_json_snapshot!(metric.abc);
8562        });
8563    }
8564
8565    #[cfg(feature = "python")]
8566    #[test]
8567    fn python_typed_assignment_counts_bare_annotation_does_not() {
8568        // `x: int = 1` carries an `=`, so it counts.
8569        // `y: int` is a bare annotation (no `=`) — declares a type but
8570        // binds nothing; it must NOT inflate the assignment count.
8571        check_metrics::<PythonParser>("x: int = 1\ny: int\n", "foo.py", |metric| {
8572            assert_eq!(metric.abc.assignments_sum(), 1);
8573            insta::assert_json_snapshot!(metric.abc);
8574        });
8575    }
8576
8577    #[cfg(feature = "python")]
8578    #[test]
8579    fn python_augmented_assignments_count() {
8580        // Each augmented op counts once.
8581        check_metrics::<PythonParser>("x = 0\nx += 1\nx -= 1\nx *= 2\n", "foo.py", |metric| {
8582            // 1 plain `=` + 3 augmented = 4 assignments.
8583            assert_eq!(metric.abc.assignments_sum(), 4);
8584            insta::assert_json_snapshot!(metric.abc);
8585        });
8586    }
8587
8588    #[cfg(feature = "python")]
8589    #[test]
8590    fn python_walrus_counts_as_assignment() {
8591        // `x := 10` is a `NamedExpression` (PEP 572). It binds a value
8592        // → one assignment under Fitzpatrick's rule.
8593        check_metrics::<PythonParser>("if (n := 10) > 5:\n    pass\n", "foo.py", |metric| {
8594            // 1 assignment (walrus) + 1 condition (`> 5` is a
8595            // ComparisonOperator).
8596            assert_eq!(metric.abc.assignments_sum(), 1);
8597            assert_eq!(metric.abc.conditions_sum(), 1);
8598            insta::assert_json_snapshot!(metric.abc);
8599        });
8600    }
8601
8602    #[cfg(feature = "python")]
8603    #[test]
8604    fn python_calls_are_branches() {
8605        // `foo()`, `bar()`, `Baz()` (constructor) all parse as `Call`
8606        // → three branches.
8607        check_metrics::<PythonParser>(
8608            "def foo():\n    pass\ndef bar():\n    pass\nclass Baz:\n    pass\nfoo()\nbar()\nBaz()\n",
8609            "foo.py",
8610            |metric| {
8611                assert_eq!(metric.abc.branches_sum(), 3);
8612                assert_eq!(metric.abc.assignments_sum(), 0);
8613                insta::assert_json_snapshot!(metric.abc);
8614            },
8615        );
8616    }
8617
8618    #[cfg(feature = "python")]
8619    #[test]
8620    fn python_comparisons_count_conditions() {
8621        // `x > 0`, `x == y`, `x is None` are each a single
8622        // `ComparisonOperator` node — three conditions.
8623        check_metrics::<PythonParser>(
8624            "def f(x, y):\n    a = x > 0\n    b = x == y\n    c = x is None\n",
8625            "foo.py",
8626            |metric| {
8627                assert_eq!(metric.abc.conditions_sum(), 3);
8628                // 3 plain assignments; the comparisons are operands.
8629                assert_eq!(metric.abc.assignments_sum(), 3);
8630                insta::assert_json_snapshot!(metric.abc);
8631            },
8632        );
8633    }
8634
8635    #[cfg(feature = "python")]
8636    #[test]
8637    fn python_chained_comparison_counts_once() {
8638        // tree-sitter-python collapses `0 < x < 10` into a single
8639        // `ComparisonOperator` — one condition, not two.
8640        check_metrics::<PythonParser>("def f(x):\n    return 0 < x < 10\n", "foo.py", |metric| {
8641            assert_eq!(metric.abc.conditions_sum(), 1);
8642            insta::assert_json_snapshot!(metric.abc);
8643        });
8644    }
8645
8646    #[cfg(feature = "python")]
8647    #[test]
8648    fn python_number_truthy_condition_counts() {
8649        // Regression for #772: Python treats every non-zero number as
8650        // truthy, so `if 5:` and `x and 5` should each count their
8651        // numeric literal as a Fitzpatrick unary condition. Pre-fix
8652        // `python_bool_terminal_kinds!()` listed `True` / `False` but
8653        // omitted `Integer` / `Float`, so the walker dropped every
8654        // numeric-truthy operand (mirrors the Lua `Number` fix).
8655        check_metrics::<PythonParser>(
8656            "def f(a):\n    if 5:\n        pass\n    return a and 2\n",
8657            "foo.py",
8658            |metric| {
8659                // `if 5:` → walker counts the Integer literal (+1).
8660                // `a and 2` → `and` walker counts both operands:
8661                //   identifier `a` (+1), Integer `2` (+1).
8662                // Total: 3.
8663                assert_eq!(metric.abc.conditions_sum(), 3);
8664                insta::assert_json_snapshot!(metric.abc);
8665            },
8666        );
8667    }
8668
8669    #[cfg(feature = "python")]
8670    #[test]
8671    fn python_boolean_operators_not_counted_directly() {
8672        // Python's `and` / `or` are not counted as conditions on
8673        // their own (Fitzpatrick Rule 5; #395). Each operand is
8674        // instead counted as a unary conditional by the walker
8675        // (Rule 9; #403). `if a and b or c:` parses left-to-right
8676        // with `or` lower precedence: `(a and b) or c`. Walker
8677        // tallies: inner `and` counts `a`, `b` (+2); outer `or`
8678        // counts only the new outer operand `c` (+1; the inner
8679        // `(a and b)` BooleanOperator is not a terminal). Total
8680        // C = 3.
8681        check_metrics::<PythonParser>(
8682            "def f(a, b, c):\n    if a and b or c:\n        pass\n",
8683            "foo.py",
8684            |metric| {
8685                assert_eq!(metric.abc.conditions_sum(), 3);
8686                insta::assert_json_snapshot!(metric.abc);
8687            },
8688        );
8689    }
8690
8691    /// Python's unary `not` operator parses as `NotOperator` and now
8692    /// counts as one condition, matching Java's `!x` rule. Closes
8693    /// the parity gap noted in #214: without this, `if not flag:`
8694    /// reported 0 conditions while the Java equivalent reports 1.
8695    #[cfg(feature = "python")]
8696    #[test]
8697    fn python_unary_not_counts_as_condition() {
8698        check_metrics::<PythonParser>(
8699            "def f(flag):\n    if not flag:\n        return 1\n    return 0\n",
8700            "foo.py",
8701            |metric| {
8702                // One `NotOperator` -> 1 condition. The `if` itself
8703                // is structural and doesn't add an Abc condition.
8704                assert_eq!(metric.abc.conditions_sum(), 1);
8705                insta::assert_json_snapshot!(metric.abc);
8706            },
8707        );
8708    }
8709
8710    /// `return not flag` — the unary `not` is the entire return
8711    /// expression. Without `NotOperator` counted, this reports zero
8712    /// conditions; with it, one. Java's `return !flag;` is one.
8713    #[cfg(feature = "python")]
8714    #[test]
8715    fn python_return_unary_not_counts() {
8716        check_metrics::<PythonParser>("def f(flag):\n    return not flag\n", "foo.py", |metric| {
8717            assert_eq!(metric.abc.conditions_sum(), 1);
8718            insta::assert_json_snapshot!(metric.abc);
8719        });
8720    }
8721
8722    /// `foo(not ready, value)` — the unary `not` inside an argument
8723    /// list still contributes. Mirrors Java's
8724    /// `java_count_unary_conditions` walk over argument lists.
8725    #[cfg(feature = "python")]
8726    #[test]
8727    fn python_unary_not_in_argument_list_counts() {
8728        check_metrics::<PythonParser>(
8729            "def f(ready, value):\n    log(not ready, value)\n",
8730            "foo.py",
8731            |metric| {
8732                // 1 Call (log) -> 1 branch.
8733                // 1 NotOperator (not ready) -> 1 condition.
8734                assert_eq!(metric.abc.branches_sum(), 1);
8735                assert_eq!(metric.abc.conditions_sum(), 1);
8736                insta::assert_json_snapshot!(metric.abc);
8737            },
8738        );
8739    }
8740
8741    /// Nested `not` + comparison counts each unique node once.
8742    /// `not (x > 0)` parses as `NotOperator(ParenthesizedExpression(
8743    /// ComparisonOperator))`; both the unary and the comparison
8744    /// contribute one condition (mirrors Java's `!(x > 0)` = 2
8745    /// conditions).
8746    #[cfg(feature = "python")]
8747    #[test]
8748    fn python_unary_not_with_comparison_counts_each_once() {
8749        check_metrics::<PythonParser>(
8750            "def f(x):\n    if not (x > 0):\n        return 1\n    return 0\n",
8751            "foo.py",
8752            |metric| {
8753                // NotOperator (1) + ComparisonOperator (1) = 2.
8754                assert_eq!(metric.abc.conditions_sum(), 2);
8755                insta::assert_json_snapshot!(metric.abc);
8756            },
8757        );
8758    }
8759
8760    /// `not x and y` parses as `BooleanOperator(NotOperator(x), and,
8761    /// y)`. The `and` itself is NOT counted (Fitzpatrick Rule 5
8762    /// lists only comparison operators); the `NotOperator` is
8763    /// counted at the top level (Rule 7); and the `y` operand is
8764    /// counted by the Rule 9 walker (issue #403). Total: 2.
8765    /// `NotOperator` is intentionally not walked-into a second
8766    /// time — the walker skips it to avoid double-counting.
8767    #[cfg(feature = "python")]
8768    #[test]
8769    fn python_unary_not_with_boolean_combinator_counts_each() {
8770        check_metrics::<PythonParser>(
8771            "def f(x, y):\n    if not x and y:\n        return 1\n    return 0\n",
8772            "foo.py",
8773            |metric| {
8774                // NotOperator (1) + walker on `and` finds `y` (1) = 2.
8775                assert_eq!(metric.abc.conditions_sum(), 2);
8776                insta::assert_json_snapshot!(metric.abc);
8777            },
8778        );
8779    }
8780
8781    #[cfg(feature = "python")]
8782    #[test]
8783    fn python_control_flow_arms_count_conditions() {
8784        // `elif`, `else`, `except`, `finally`, `case` each contribute
8785        // one condition. The comparisons in the `if`/`elif`/`while`
8786        // headers contribute their own ComparisonOperator counts.
8787        check_metrics::<PythonParser>(
8788            "def f(x):\n    if x > 0:\n        a = 1\n    elif x > -1:\n        a = 2\n    else:\n        a = 3\n",
8789            "foo.py",
8790            |metric| {
8791                // 2 ComparisonOperator (`x > 0`, `x > -1`) + 1
8792                // ElifClause + 1 ElseClause = 4 conditions.
8793                assert_eq!(metric.abc.conditions_sum(), 4);
8794                insta::assert_json_snapshot!(metric.abc);
8795            },
8796        );
8797    }
8798
8799    #[cfg(feature = "python")]
8800    #[test]
8801    fn python_ternary_counts_as_condition() {
8802        // `a if c else b` is `ConditionalExpression` → 1 condition.
8803        // `c > 0` adds 1 more (ComparisonOperator).
8804        check_metrics::<PythonParser>(
8805            "def f(c):\n    return 1 if c > 0 else 0\n",
8806            "foo.py",
8807            |metric| {
8808                assert_eq!(metric.abc.conditions_sum(), 2);
8809                insta::assert_json_snapshot!(metric.abc);
8810            },
8811        );
8812    }
8813
8814    // Issue #1161. Python counted the `conditional_expression` node but
8815    // never its condition slot, so `a if c() else b` reported 1 where
8816    // the equivalent `c() ? a : b` reports 2 everywhere else — and
8817    // `python_inspect_container`'s `ConditionalExpression` boolean-
8818    // context seed was unreachable, no call site having passed that
8819    // parent.
8820    #[cfg(feature = "python")]
8821    #[test]
8822    fn python_ternary_condition_slot_counts_as_a_unary_condition() {
8823        // ternary (1) + condition `c()` (1) = 2. `c()` is a `Call`, a
8824        // boolean terminal; it also adds one *branch*, not a condition.
8825        check_metrics::<PythonParser>(
8826            "def f(a, b, c):\n    return a if c() else b\n",
8827            "foo.py",
8828            |metric| {
8829                assert_eq!(metric.abc.conditions_sum(), 2);
8830            },
8831        );
8832        // A parenthesised condition, pinning the seed line this fix made
8833        // reachable: `(c)` is a `parenthesized_expression`, so only
8834        // `python_inspect_container` can resolve it, and it counts the
8835        // unwrapped terminal only when the parent seeds boolean context.
8836        // ternary (1) + `(c)` (1) = 2.
8837        check_metrics::<PythonParser>(
8838            "def f(a, b, c):\n    return a if (c) else b\n",
8839            "foo.py",
8840            |metric| {
8841                assert_eq!(metric.abc.conditions_sum(), 2);
8842            },
8843        );
8844        // A negated condition is *not* counted by the new slot: it is a
8845        // `NotOperator`, which already has its own top-level dispatcher
8846        // arm. ternary (1) + `not c` (1) = 2, not 3.
8847        check_metrics::<PythonParser>(
8848            "def f(a, b, c):\n    return a if not c else b\n",
8849            "foo.py",
8850            |metric| {
8851                assert_eq!(metric.abc.conditions_sum(), 2);
8852            },
8853        );
8854        // The cross-language reference case. `(not b) if a else (not c)`
8855        // is the exact semantic equivalent of `a ? !b : !c`, which every
8856        // other language reports as 4: ternary (1) + condition `a` (1) +
8857        // two `NotOperator`s (2). The negated operands come from
8858        // Python's own arm, the condition from the slot added here.
8859        //
8860        // #1161's resolution plan predicted this would stay 3, having
8861        // measured the condition slot's contribution against the pre-fix
8862        // total. 3 would have left Python disagreeing with every other
8863        // language on the reference expression — the gap the issue was
8864        // filed about.
8865        check_metrics::<PythonParser>(
8866            "def f(a, b, c):\n    return (not b) if a else (not c)\n",
8867            "foo.py",
8868            |metric| {
8869                assert_eq!(metric.abc.conditions_sum(), 4);
8870            },
8871        );
8872    }
8873
8874    // The double-count pin, and the reason Python gets a condition-slot
8875    // helper rather than a copy of `cpp_walk_ternary`: Python's branch
8876    // operands are counted by the top-level `NotOperator` /
8877    // `ComparisonOperator` arms, a different mechanism from every other
8878    // language's walker. Routing the branch slots through
8879    // `python_inspect_container` as the C family does would count a
8880    // parenthesised operand that the identical unparenthesised
8881    // expression scores at zero.
8882    //
8883    // Both fixtures below are 2 today and 4 under such a copy, so a
8884    // later "make Python consistent with the others" change cannot land
8885    // silently.
8886    #[cfg(feature = "python")]
8887    #[test]
8888    fn python_ternary_branch_operands_are_not_double_counted() {
8889        // ternary (1) + condition `a` (1) = 2. The two parenthesised
8890        // operands add nothing — an unnegated branch is type-free.
8891        check_metrics::<PythonParser>(
8892            "def f(a, b, c):\n    return (b) if a else (c)\n",
8893            "foo.py",
8894            |metric| {
8895                assert_eq!(metric.abc.conditions_sum(), 2);
8896            },
8897        );
8898        // The unparenthesised form must agree: nothing about `(b)`
8899        // versus `b` is a condition.
8900        check_metrics::<PythonParser>(
8901            "def f(a, b, c):\n    return b if a else c\n",
8902            "foo.py",
8903            |metric| {
8904                assert_eq!(metric.abc.conditions_sum(), 2);
8905            },
8906        );
8907    }
8908
8909    // Comments are tree-sitter `extras`, so they arrive as direct
8910    // children of `conditional_expression` and shift every positional
8911    // index after them. `python_count_ternary_condition` therefore
8912    // anchors on the `if` keyword and skips comments after it; both
8913    // halves are needed and each fixture below fails without one.
8914    #[cfg(feature = "python")]
8915    #[test]
8916    fn python_ternary_condition_survives_an_interposed_comment() {
8917        // Comment before the keyword: `child(2)` is the `if` token here,
8918        // so a positional lookup reads 1. ternary (1) + `f()` (1) = 2.
8919        check_metrics::<PythonParser>(
8920            "def f(b, c):\n    return (b\n            # why\n            if f() else c)\n",
8921            "foo.py",
8922            |metric| {
8923                assert_eq!(metric.abc.conditions_sum(), 2);
8924            },
8925        );
8926        // Comment after the keyword: the child immediately following
8927        // `if` is the comment, so taking the first rather than the first
8928        // non-comment reads 1. ternary (1) + `f()` (1) = 2.
8929        check_metrics::<PythonParser>(
8930            "def f(b, c):\n    return (b if\n            # why\n            f() else c)\n",
8931            "foo.py",
8932            |metric| {
8933                assert_eq!(metric.abc.conditions_sum(), 2);
8934            },
8935        );
8936    }
8937
8938    #[cfg(feature = "python")]
8939    #[test]
8940    fn python_try_except_finally_count_conditions() {
8941        // ExceptClause + FinallyClause → 2 conditions.
8942        check_metrics::<PythonParser>(
8943            "def f():\n    try:\n        pass\n    except ValueError:\n        pass\n    finally:\n        pass\n",
8944            "foo.py",
8945            |metric| {
8946                assert_eq!(metric.abc.conditions_sum(), 2);
8947                insta::assert_json_snapshot!(metric.abc);
8948            },
8949        );
8950    }
8951
8952    #[cfg(feature = "python")]
8953    #[test]
8954    fn python_match_case_counts_conditions() {
8955        // Each non-wildcard `CaseClause` → 1 condition. The bare
8956        // `case _:` arm is the language-neutral `default:` equivalent
8957        // and is excluded (matches Rust's bare-`_` MatchArm filter and
8958        // Java/C#'s `default:` rule). Source has `case 1:` (counts) +
8959        // `case _:` (excluded) → C = 1.
8960        check_metrics::<PythonParser>(
8961            "def f(x):\n    match x:\n        case 1:\n            pass\n        case _:\n            pass\n",
8962            "foo.py",
8963            |metric| {
8964                assert_eq!(metric.abc.conditions_sum(), 1);
8965                insta::assert_json_snapshot!(metric.abc);
8966            },
8967        );
8968    }
8969
8970    #[cfg(feature = "python")]
8971    #[test]
8972    fn python_match_case_guarded_wildcard_counts() {
8973        // `case _ if g:` is NOT a bare wildcard — the guard
8974        // contributes real branching, so the arm counts as a
8975        // condition. Mirrors Rust's `_ if g => ...` behavior.
8976        // Source: `case 1:` (counts) + `case _ if x > 0:` (guarded
8977        // wildcard, counts) + `case _:` (bare wildcard, excluded) →
8978        // C from CaseClause = 2; the guard's `x > 0` adds one
8979        // ComparisonOperator → total C = 3.
8980        check_metrics::<PythonParser>(
8981            "def f(x):\n    match x:\n        case 1:\n            pass\n        case _ if x > 0:\n            pass\n        case _:\n            pass\n",
8982            "foo.py",
8983            |metric| {
8984                assert_eq!(metric.abc.conditions_sum(), 3);
8985                insta::assert_json_snapshot!(metric.abc);
8986            },
8987        );
8988    }
8989
8990    #[cfg(feature = "python")]
8991    #[test]
8992    fn python_complex_function_abc() {
8993        // Mixed-shape regression: assignments, calls, conditions all in
8994        // a single function.
8995        check_metrics::<PythonParser>(
8996            "def f(items, threshold):\n\
8997             \x20   result = []\n\
8998             \x20   for item in items:\n\
8999             \x20       if item > threshold:\n\
9000             \x20           result.append(item)\n\
9001             \x20   return result\n",
9002            "foo.py",
9003            |metric| {
9004                // assignments: `result = []` → 1
9005                // branches: `result.append(item)` is one call → 1
9006                // conditions: `item > threshold` is one
9007                // ComparisonOperator → 1
9008                assert_eq!(metric.abc.assignments_sum(), 1);
9009                assert_eq!(metric.abc.branches_sum(), 1);
9010                assert_eq!(metric.abc.conditions_sum(), 1);
9011                insta::assert_json_snapshot!(metric.abc);
9012            },
9013        );
9014    }
9015
9016    #[cfg(feature = "python")]
9017    #[test]
9018    fn python_if_multiple_conditions() {
9019        // Fitzpatrick Rule 9 walker on `and` / `or` (issue #403).
9020        //   - `if a or b or c or d:` → 4 (each operand counted once)
9021        //   - `if a and b and c:`    → 3
9022        //   - `if not a and not b:`  → 2 (two `NotOperator`s counted
9023        //     by the top-level dispatcher arm; the walker SKIPS
9024        //     `NotOperator` children to avoid double-counting)
9025        // Total: 4 + 3 + 2 = 9.
9026        check_metrics::<PythonParser>(
9027            "def f(a, b, c, d):\n\
9028             \x20   if a or b or c or d:           # +4c\n\
9029             \x20       pass\n\
9030             \x20   if a and b and c:              # +3c\n\
9031             \x20       pass\n\
9032             \x20   if not a and not b:            # +2c (NotOperator x2)\n\
9033             \x20       pass\n",
9034            "foo.py",
9035            |metric| {
9036                assert_eq!(metric.abc.conditions_sum(), 9);
9037                insta::assert_json_snapshot!(metric.abc);
9038            },
9039        );
9040    }
9041
9042    #[cfg(feature = "python")]
9043    #[test]
9044    fn python_while_conditions() {
9045        // Python has no `do { ... } while(cond);` construct, so this
9046        // mirrors only the `while` half of the Java suite. The
9047        // walker fires on each `and` / `or` token inside the loop
9048        // header.
9049        check_metrics::<PythonParser>(
9050            "def f(a, b):\n\
9051             \x20   while a or b:                  # +2c\n\
9052             \x20       break\n\
9053             \x20   while a and not b:             # +2c (a + NotOperator)\n\
9054             \x20       break\n",
9055            "foo.py",
9056            |metric| {
9057                assert_eq!(metric.abc.conditions_sum(), 4);
9058                insta::assert_json_snapshot!(metric.abc);
9059            },
9060        );
9061    }
9062
9063    #[cfg(feature = "python")]
9064    #[test]
9065    fn python_short_circuit_with_boolean_literal_operand() {
9066        // `a and True` reports 2 conditions: one identifier, one
9067        // True literal. Confirms `True` / `False` are in the walker
9068        // terminal set.
9069        check_metrics::<PythonParser>("def f(a):\n    return a and True\n", "foo.py", |metric| {
9070            assert_eq!(metric.abc.conditions_sum(), 2);
9071            insta::assert_json_snapshot!(metric.abc);
9072        });
9073    }
9074
9075    #[cfg(feature = "python")]
9076    #[test]
9077    fn python_await_expression_condition_counts() {
9078        // Regression for findings.md round-2 #2 (Python):
9079        // `if await ready(): pass` parses with `await` as the
9080        // condition node. Adding `Python::Await` to the
9081        // terminal-bool set mirrors the C# reference (lesson 19).
9082        check_metrics::<PythonParser>(
9083            "async def ready(): return True\n\
9084             async def f():\n    if await ready(): pass\n",
9085            "foo.py",
9086            |metric| {
9087                // ready() is a call (1 branch); await is the
9088                // condition (1).
9089                assert_eq!(metric.abc.branches_sum(), 1);
9090                assert_eq!(metric.abc.conditions_sum(), 1);
9091                insta::assert_json_snapshot!(metric.abc);
9092            },
9093        );
9094    }
9095
9096    #[cfg(feature = "python")]
9097    #[test]
9098    fn python_if_call_terminal_condition_counts_once() {
9099        // Pins the Phase-2B behaviour for Python's `Call` terminal-bool
9100        // kind: `if foo():` is a Fitzpatrick Rule 6 unary conditional
9101        // (a bare boolean-evaluating call as the if-condition). The
9102        // walker's terminal-at-top check fires once per call-condition;
9103        // the call itself separately contributes 1 branch. Surfaced
9104        // (and verified intentional) by the code-review pass on
9105        // Phase 2B.
9106        check_metrics::<PythonParser>("def f():\n    if foo(): pass\n", "foo.py", |metric| {
9107            assert_eq!(metric.abc.branches_sum(), 1);
9108            assert_eq!(metric.abc.conditions_sum(), 1);
9109            insta::assert_json_snapshot!(metric.abc);
9110        });
9111    }
9112
9113    #[cfg(feature = "python")]
9114    #[test]
9115    fn python_if_boolean_literal_condition() {
9116        // Phase 2B (issue #403): bare-boolean conditions count once.
9117        // Python has no paren wrap around if-conditions, so the
9118        // condition node is checked directly. The existing
9119        // NotOperator / ComparisonOperator arms continue to fire
9120        // for those shapes; only the bare-terminal cases (Identifier,
9121        // True, False, etc.) are added by the new arm.
9122        check_metrics::<PythonParser>(
9123            "def f(a):\n\
9124             \x20   if True: pass        # +1c\n\
9125             \x20   if False: pass       # +1c\n\
9126             \x20   while True: break    # +1c\n\
9127             \x20   if a: pass           # +1c (Rule 6 — bare identifier as condition)\n",
9128            "foo.py",
9129            |metric| {
9130                assert_eq!(metric.abc.conditions_sum(), 4);
9131                insta::assert_json_snapshot!(metric.abc);
9132            },
9133        );
9134    }
9135
9136    #[cfg(feature = "python")]
9137    #[test]
9138    fn python_methods_arguments_with_conditions() {
9139        // `m(not a, not b)` reports 2 conditions — both `NotOperator`
9140        // nodes are counted by Python's pre-existing top-level
9141        // NotOperator dispatcher arm. The argument-list walker does
9142        // not need a separate Python arm.
9143        check_metrics::<PythonParser>(
9144            "def f(a, b):\n\
9145             \x20   m(a, b)             # +1b\n\
9146             \x20   m(not a, not b)     # +1b +2c\n",
9147            "foo.py",
9148            |metric| {
9149                assert_eq!(metric.abc.branches_sum(), 2);
9150                assert_eq!(metric.abc.conditions_sum(), 2);
9151                insta::assert_json_snapshot!(metric.abc);
9152            },
9153        );
9154    }
9155
9156    #[cfg(feature = "python")]
9157    #[test]
9158    fn python_return_with_conditions() {
9159        // Phase 2B (issue #403). Python uses the pre-existing top-
9160        // level NotOperator / ComparisonOperator arms for return
9161        // expressions; no dedicated ReturnStatement walker arm is
9162        // needed.
9163        check_metrics::<PythonParser>(
9164            "def m1(z): return not (z >= 0)\n\
9165             def m2(x): return (((not x)))\n\
9166             def m3(x, y): return x and y\n",
9167            "foo.py",
9168            |metric| {
9169                // m1: NotOperator (1) + ComparisonOperator (1) = 2.
9170                // m2: NotOperator (1).
9171                // m3: walker on `and` counts both operands = 2.
9172                // Sum: 5.
9173                assert_eq!(metric.abc.conditions_sum(), 5);
9174                insta::assert_json_snapshot!(metric.abc);
9175            },
9176        );
9177    }
9178
9179    #[cfg(feature = "rust")]
9180    #[test]
9181    fn rust_empty_unit_zero() {
9182        // No code at all → A=B=C=0. Establishes the trait is wired up
9183        // and the per-language compute is reachable.
9184        check_metrics::<RustParser>("", "empty.rs", |metric| {
9185            assert_eq!(metric.abc.assignments_sum(), 0);
9186            assert_eq!(metric.abc.branches_sum(), 0);
9187            assert_eq!(metric.abc.conditions_sum(), 0);
9188            insta::assert_json_snapshot!(metric.abc);
9189        });
9190    }
9191
9192    #[cfg(feature = "rust")]
9193    #[test]
9194    fn rust_assignments_let_init_plain_and_compound() {
9195        // `let mut x = 0` is a `let_declaration` carrying an `=`
9196        // initializer → counts as 1 (matches Fitzpatrick's literal
9197        // "every `=` is an assignment" rule and the JS impl's
9198        // treatment of `let x = 5`). `x = 5` and `x = 7` are plain
9199        // `=` assignments → 2. `x += 2` is a compound assignment → 1.
9200        // Total A = 4.
9201        check_metrics::<RustParser>(
9202            "fn f() { let mut x = 0; x = 5; x += 2; x = 7; }",
9203            "foo.rs",
9204            |metric| {
9205                assert_eq!(metric.abc.assignments_sum(), 4);
9206                assert_eq!(metric.abc.branches_sum(), 0);
9207                assert_eq!(metric.abc.conditions_sum(), 0);
9208                insta::assert_json_snapshot!(metric.abc);
9209            },
9210        );
9211    }
9212
9213    #[cfg(feature = "rust")]
9214    #[test]
9215    fn rust_let_without_initializer_does_not_count() {
9216        // `let a;` is a `let_declaration` with NO `=` and no `value`
9217        // field — the binding is uninitialised. The arm only fires
9218        // when `value` is present, so this contributes zero to A.
9219        // `let _b;` is the same shape (the `_` pattern is still a
9220        // pattern, not a wildcard suppression of the binding).
9221        // Regression test for issue #393: only `=` counts, not the
9222        // bare declaration.
9223        check_metrics::<RustParser>(
9224            "fn f() { let a: i32; let _b: i32; a = 5; }",
9225            "foo.rs",
9226            |metric| {
9227                // Only `a = 5` (assignment_expression) → A = 1.
9228                assert_eq!(metric.abc.assignments_sum(), 1);
9229                insta::assert_json_snapshot!(metric.abc);
9230            },
9231        );
9232    }
9233
9234    #[cfg(feature = "rust")]
9235    #[test]
9236    fn rust_let_initializers_immutable_and_mutable_count() {
9237        // Issue #393: `let a = 1;`, `let b = 2;`, `let c = a + b;`,
9238        // `let mut d = 0;` are all `let_declaration` nodes carrying
9239        // an `=` initializer — each counts as 1 (Option B in the
9240        // issue body: literal Fitzpatrick, both `let` and `let mut`
9241        // count). `d = 5;` is one plain assignment_expression, `d
9242        // += 1;` is one compound. Total A = 4 + 1 + 1 = 6.
9243        check_metrics::<RustParser>(
9244            "fn f() { let a=1; let b=2; let c=a+b; let mut d=0; d=5; d+=1; }",
9245            "foo.rs",
9246            |metric| {
9247                assert_eq!(metric.abc.assignments_sum(), 6);
9248                insta::assert_json_snapshot!(metric.abc);
9249            },
9250        );
9251    }
9252
9253    #[cfg(feature = "rust")]
9254    #[test]
9255    fn rust_calls_are_branches() {
9256        // Free function call + method call (parses as call_expression
9257        // with a field_expression callee) + associated-fn call. All
9258        // three are `call_expression` → B = 3. Macro invocations like
9259        // `println!` parse as `macro_invocation`, NOT `call_expression`,
9260        // so they are not branches.
9261        check_metrics::<RustParser>(
9262            "fn f() { g(); 1.to_string(); String::new(); }\nfn g() {}\n",
9263            "foo.rs",
9264            |metric| {
9265                assert_eq!(metric.abc.branches_sum(), 3);
9266                assert_eq!(metric.abc.assignments_sum(), 0);
9267                assert_eq!(metric.abc.conditions_sum(), 0);
9268                insta::assert_json_snapshot!(metric.abc);
9269            },
9270        );
9271    }
9272
9273    #[cfg(feature = "rust")]
9274    #[test]
9275    fn rust_try_operator_is_branch() {
9276        // `?` parses as `try_expression` and counts as one branch
9277        // (short-circuit return on Err / None). The `Err(())` call
9278        // contributes one branch in addition (call_expression).
9279        check_metrics::<RustParser>(
9280            "fn f() -> Result<i32, ()> { let r: Result<i32, ()> = Err(()); Ok(r?) }",
9281            "foo.rs",
9282            |metric| {
9283                // Err(()) + Ok(...) + r? → 2 calls + 1 try = 3 branches.
9284                assert_eq!(metric.abc.branches_sum(), 3);
9285                insta::assert_json_snapshot!(metric.abc);
9286            },
9287        );
9288    }
9289
9290    #[cfg(feature = "rust")]
9291    #[test]
9292    fn rust_comparisons_count_conditions() {
9293        // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six
9294        // comparisons → C = 6.
9295        check_metrics::<RustParser>(
9296            "fn f(a: i32, b: i32) -> bool { a < b || a > b || a <= b || a >= b || a == b || a != b }",
9297            "foo.rs",
9298            |metric| {
9299                assert_eq!(metric.abc.conditions_sum(), 6);
9300                insta::assert_json_snapshot!(metric.abc);
9301            },
9302        );
9303    }
9304
9305    #[cfg(feature = "rust")]
9306    #[test]
9307    fn rust_generic_brackets_not_conditions() {
9308        // `<` / `>` in `Vec<i32>` are TypeArguments delimiters, not
9309        // comparison operators. The parent-check in the LT/GT arms
9310        // must filter them out. Expected C = 0.
9311        check_metrics::<RustParser>(
9312            "fn f() -> Vec<i32> { Vec::<i32>::new() }",
9313            "foo.rs",
9314            |metric| {
9315                assert_eq!(metric.abc.conditions_sum(), 0);
9316                insta::assert_json_snapshot!(metric.abc);
9317            },
9318        );
9319    }
9320
9321    #[cfg(feature = "rust")]
9322    #[test]
9323    fn rust_if_let_counts_as_condition() {
9324        // `if let Some(v) = opt { ... }` introduces a `let_condition`
9325        // → 1 condition. The `if` keyword itself does not add another
9326        // count — Fitzpatrick counts conditions, not branch keywords.
9327        check_metrics::<RustParser>(
9328            "fn f(opt: Option<i32>) { if let Some(_v) = opt { } }",
9329            "foo.rs",
9330            |metric| {
9331                assert_eq!(metric.abc.conditions_sum(), 1);
9332                insta::assert_json_snapshot!(metric.abc);
9333            },
9334        );
9335    }
9336
9337    #[cfg(feature = "rust")]
9338    #[test]
9339    fn rust_while_let_counts_as_condition() {
9340        // `while let Some(y) = it.next() { ... }` is also a
9341        // `let_condition` (the `while` form). One condition; the
9342        // `it.next()` call adds one branch.
9343        check_metrics::<RustParser>(
9344            "fn f(mut it: std::vec::IntoIter<i32>) { while let Some(_y) = it.next() { } }",
9345            "foo.rs",
9346            |metric| {
9347                assert_eq!(metric.abc.conditions_sum(), 1);
9348                assert_eq!(metric.abc.branches_sum(), 1);
9349                insta::assert_json_snapshot!(metric.abc);
9350            },
9351        );
9352    }
9353
9354    #[cfg(feature = "rust")]
9355    #[test]
9356    fn rust_match_arms_count_conditions_wildcard_excluded() {
9357        // Three arms: `0 => 1`, `n if n > 0 => n`, `_ => -1`. The
9358        // bare wildcard is the `default:` equivalent and is skipped.
9359        // The guarded arm has a `n if n > 0` pattern (more than one
9360        // child in the match_pattern) and still counts. Two non-wildcard
9361        // arms → C = 2 from MatchArm. Plus the comparison `n > 0`
9362        // adds one more → C = 3.
9363        check_metrics::<RustParser>(
9364            "fn f(x: i32) -> i32 { match x { 0 => 1, n if n > 0 => n, _ => -1, } }",
9365            "foo.rs",
9366            |metric| {
9367                assert_eq!(metric.abc.conditions_sum(), 3);
9368                insta::assert_json_snapshot!(metric.abc);
9369            },
9370        );
9371    }
9372
9373    #[cfg(feature = "rust")]
9374    #[test]
9375    fn rust_else_counts_as_condition() {
9376        // `if a > b { ... } else { ... }` → `a > b` is one condition,
9377        // `else` is one condition → C = 2.
9378        check_metrics::<RustParser>(
9379            "fn f(a: i32, b: i32) -> i32 { if a > b { a } else { b } }",
9380            "foo.rs",
9381            |metric| {
9382                assert_eq!(metric.abc.conditions_sum(), 2);
9383                insta::assert_json_snapshot!(metric.abc);
9384            },
9385        );
9386    }
9387
9388    #[cfg(feature = "rust")]
9389    #[test]
9390    fn rust_let_chain2_hidden_rule_drift_marker() {
9391        // Drift marker (findings.md round-2 #3): `Rust::LetChain2`
9392        // maps to the hidden grammar rule `_let_chain`. At the
9393        // pinned tree-sitter-rust version it is never emitted as a
9394        // concrete node — the visible `LetChain` (= 352) carries
9395        // every let-chain. We list `LetChain2` defensively in
9396        // `rust_inspect_container` and `rust_count_unary_conditions`
9397        // (lesson 34); if a future grammar bump promotes
9398        // `_let_chain` to a visible rule, this assertion fails
9399        // loudly so the maintainer knows to verify the walker still
9400        // counts correctly for the new shape.
9401        let src = "fn f(a: bool, b: Option<i32>) {\n\
9402                   \x20   if a && let Some(_) = b { }\n\
9403                   }\n";
9404        let parser = RustParser::new(
9405            src.as_bytes().to_vec(),
9406            &std::path::PathBuf::from("foo.rs"),
9407            None,
9408        );
9409        assert!(!ast_has_kind_id(&parser, Rust::LetChain2 as u16));
9410    }
9411
9412    #[cfg(feature = "rust")]
9413    #[test]
9414    fn rust_scoped_identifier_condition_counts() {
9415        // Regression for findings.md round-2 #1 (Rust):
9416        // `if crate::FLAG {}` parses with `scoped_identifier` as the
9417        // condition node. Pre-fix, `rust_bool_terminal_kinds!()`
9418        // listed only `Identifier` so the walker reached the
9419        // `scoped_identifier` child, found it non-terminal /
9420        // non-paren / non-unary, and broke without counting.
9421        // Mirrors the C# fix in #372 (lesson 19) for
9422        // `MemberAccessExpression`.
9423        check_metrics::<RustParser>("fn f() { if crate::FLAG { } }\n", "foo.rs", |metric| {
9424            assert_eq!(metric.abc.conditions_sum(), 1);
9425            insta::assert_json_snapshot!(metric.abc);
9426        });
9427    }
9428
9429    #[cfg(feature = "rust")]
9430    #[test]
9431    fn rust_await_expression_condition_counts() {
9432        // Regression for findings.md round-2 #2 (Rust):
9433        // `if ready().await {}` parses with `await_expression` as
9434        // the condition node. Adding `Rust::AwaitExpression` to the
9435        // terminal-bool set closes the parity gap with the C#
9436        // reference (`csharp_bool_terminal_kinds!()`).
9437        check_metrics::<RustParser>(
9438            "async fn ready() -> bool { true }\n\
9439             async fn f() { if ready().await { } }\n",
9440            "foo.rs",
9441            |metric| {
9442                // ready() is a call (1 branch); `ready().await` is
9443                // the unary boolean condition (1).
9444                assert_eq!(metric.abc.branches_sum(), 1);
9445                assert_eq!(metric.abc.conditions_sum(), 1);
9446                insta::assert_json_snapshot!(metric.abc);
9447            },
9448        );
9449    }
9450
9451    #[cfg(feature = "rust")]
9452    #[test]
9453    fn rust_complex_function_abc() {
9454        // Mixed-shape regression: assignments, calls, conditions, `?`,
9455        // `if let`, `match` in one body. Verified by hand:
9456        // - assignments: `let mut x = 0` (let init), `x = 5`, `x += 2`,
9457        //   `let _ = ...` (let init), `let r: ... = Err(())` (let init),
9458        //   `let _v = r?` (let init) → A = 6 (post-#393: every `=`
9459        //   initializer in a `let_declaration` is one assignment, in
9460        //   line with the literal Fitzpatrick reading).
9461        // - branches: `xs.iter()`, `.next()`, `Err(())`, `r?` → B = 4
9462        //   (3 calls + 1 try).
9463        // - conditions: `if let Some(v) = opt` → 1, `match x` arms
9464        //   `0`, `n if n>0` (wildcard excluded) → 2, `n > 0` → 1.
9465        //   Total C = 4.
9466        check_metrics::<RustParser>(
9467            "fn f(opt: Option<i32>, xs: Vec<i32>) -> Result<i32, ()> {\n\
9468             \x20   let mut x = 0;\n\
9469             \x20   x = 5;\n\
9470             \x20   x += 2;\n\
9471             \x20   if let Some(_v) = opt { }\n\
9472             \x20   let _ = xs.iter().next();\n\
9473             \x20   let r: Result<i32, ()> = Err(());\n\
9474             \x20   let _v = r?;\n\
9475             \x20   Ok(match x {\n\
9476             \x20       0 => 1,\n\
9477             \x20       n if n > 0 => n,\n\
9478             \x20       _ => -1,\n\
9479             \x20   })\n\
9480             }\n",
9481            "foo.rs",
9482            |metric| {
9483                assert_eq!(metric.abc.assignments_sum(), 6);
9484                // calls: xs.iter(), .next(), Err(()), Ok(...) → 4 calls
9485                // plus 1 try (`r?`) → 5 branches.
9486                assert_eq!(metric.abc.branches_sum(), 5);
9487                // 1 let_condition + 2 non-wildcard match_arms + 1
9488                // comparison (`n > 0`) → 4.
9489                assert_eq!(metric.abc.conditions_sum(), 4);
9490                insta::assert_json_snapshot!(metric.abc);
9491            },
9492        );
9493    }
9494
9495    #[cfg(feature = "rust")]
9496    #[test]
9497    fn rust_let_chain_bare_identifier_operand_counts() {
9498        // Regression: pre-fix, `if a && let Some(_z) = y { }` reported
9499        // 1 condition (only the LetCondition). The bare-identifier
9500        // `a` operand was lost because Rust 2024 wraps let-chain
9501        // `&&` operands in a `LetChain` node (not `BinaryExpression`)
9502        // and `rust_count_unary_conditions` only counted terminals
9503        // under a `BinaryExpression` parent. Allowing `LetChain` /
9504        // `LetChain2` as known-bool list parents fixes the loss.
9505        // Expected: LetCondition (1) + walker on `a` (1) = 2.
9506        check_metrics::<RustParser>(
9507            "fn f(a: bool, y: Option<i32>) {\n\
9508             \x20   if a && let Some(_z) = y { }\n\
9509             }\n",
9510            "foo.rs",
9511            |metric| {
9512                assert_eq!(metric.abc.conditions_sum(), 2);
9513                insta::assert_json_snapshot!(metric.abc);
9514            },
9515        );
9516    }
9517
9518    #[cfg(feature = "rust")]
9519    #[test]
9520    fn rust_if_multiple_conditions() {
9521        // Fitzpatrick Rule 7 / Listing 2 (issue #403): every operand of
9522        // a `&&` / `||` chain is one condition. Mirrors
9523        // `java_if_multiple_conditions`. Rust's `if` head has no
9524        // parentheses, but the walker fires on each `&&` / `||` token
9525        // and walks the parent `binary_expression` regardless.
9526        check_metrics::<RustParser>(
9527            "fn f(a: bool, b: bool, c: bool, d: bool) -> i32 {\n\
9528             \x20   if a || b || c || d { return 1; }    // +4c\n\
9529             \x20   if a && b && c { return 2; }         // +3c\n\
9530             \x20   if !a && !b { return 3; }            // +2c\n\
9531             \x20   0\n\
9532             }\n",
9533            "foo.rs",
9534            |metric| {
9535                // 4 + 3 + 2 = 9
9536                assert_eq!(metric.abc.conditions_sum(), 9);
9537                insta::assert_json_snapshot!(metric.abc);
9538            },
9539        );
9540    }
9541
9542    #[cfg(feature = "rust")]
9543    #[test]
9544    fn rust_while_conditions() {
9545        // Rust has no `do { ... } while(cond);` construct, so this
9546        // mirrors only the `while` half of `java_while_and_do_while_conditions`.
9547        // Each operand of the `&&` / `||` chain in the loop condition
9548        // counts as one Fitzpatrick condition (Rule 7).
9549        check_metrics::<RustParser>(
9550            "fn f(a: bool, b: bool) {\n\
9551             \x20   while a || b { break; }       // +2c\n\
9552             \x20   while a && !b { break; }      // +2c\n\
9553             }\n",
9554            "foo.rs",
9555            |metric| {
9556                assert_eq!(metric.abc.conditions_sum(), 4);
9557                insta::assert_json_snapshot!(metric.abc);
9558            },
9559        );
9560    }
9561
9562    #[cfg(feature = "rust")]
9563    #[test]
9564    fn rust_if_boolean_literal_condition() {
9565        // Phase 2B (issue #403): a condition whose entire body is a
9566        // boolean literal counts as one Fitzpatrick condition.
9567        // `if true {}` → 1, `if !false {}` → 1 (unary unwrap), and
9568        // `while true { break }` → 1.
9569        check_metrics::<RustParser>(
9570            "fn f() {\n\
9571             \x20   if true { }                  // +1c\n\
9572             \x20   if !false { }                // +1c\n\
9573             \x20   while true { break; }        // +1c\n\
9574             }\n",
9575            "foo.rs",
9576            |metric| {
9577                assert_eq!(metric.abc.conditions_sum(), 3);
9578                insta::assert_json_snapshot!(metric.abc);
9579            },
9580        );
9581    }
9582
9583    #[cfg(feature = "rust")]
9584    #[test]
9585    fn rust_methods_arguments_with_conditions() {
9586        // Phase 2B (issue #403): unary-conditional arguments to a
9587        // call each count once. `m(!a, !b)` → 2 conditions + 1
9588        // branch (the call itself). Bare identifier arguments do
9589        // NOT count (they reach the count_unary_conditions list with
9590        // list_kind = Arguments, not BinaryExpression).
9591        check_metrics::<RustParser>(
9592            "fn f(a: bool, b: bool) {\n\
9593             \x20   m(a, b);                     // +1b\n\
9594             \x20   m(!a, !b);                   // +1b +2c\n\
9595             \x20   m(!a, b, !a);                // +1b +2c\n\
9596             }\n",
9597            "foo.rs",
9598            |metric| {
9599                assert_eq!(metric.abc.branches_sum(), 3);
9600                assert_eq!(metric.abc.conditions_sum(), 4);
9601                insta::assert_json_snapshot!(metric.abc);
9602            },
9603        );
9604    }
9605
9606    #[cfg(feature = "rust")]
9607    #[test]
9608    fn rust_return_with_conditions() {
9609        // Phase 2B (issue #403). Mirrors `java_return_with_conditions`
9610        // — `return !a` / `return x && y` count their unary
9611        // conditional operands. Per Fitzpatrick Rule 7, a `!`-wrapped
9612        // relational expression contributes ONE condition (the
9613        // relational op itself) — the `!` does not add a second
9614        // count when its operand is already a comparison.
9615        check_metrics::<RustParser>(
9616            "fn m1(z: i32) -> bool { return !(z >= 0); }\n\
9617             fn m2(x: bool) -> bool { return (((!x))); }\n\
9618             fn m3(x: bool, y: bool) -> bool { return x && y; }\n\
9619             fn m4(y: bool, z: i32) -> bool { return y || (z < 0); }\n",
9620            "foo.rs",
9621            |metric| {
9622                // m1: !(z >= 0) → the `>=` contributes 1; the unary
9623                //     `!` wraps a paren'd BinaryExpression, which
9624                //     inspect_container does not unwrap further →
9625                //     no walker count. Total: 1.
9626                // m2: (((!x))) → ReturnExpression arm walks (((!x))).
9627                //     inspect_container unwraps three parens and one
9628                //     unary, reaches Identifier `x`, has_boolean_content
9629                //     was seeded true by the unary-not flip. +1.
9630                // m3: x && y → `&&` walker counts both terminals → 2.
9631                // m4: y || (z < 0) → `||` walker counts `y` (terminal,
9632                //     +1); the `<` contributes 1 via its own arm; the
9633                //     paren'd BinaryExpression `(z < 0)` is not
9634                //     terminal under the walker → no extra count.
9635                //     Total: 2.
9636                // Sum: 1 + 1 + 2 + 2 = 6.
9637                assert_eq!(metric.abc.conditions_sum(), 6);
9638                insta::assert_json_snapshot!(metric.abc);
9639            },
9640        );
9641    }
9642
9643    #[cfg(feature = "rust")]
9644    #[test]
9645    fn rust_short_circuit_with_boolean_literal_operand() {
9646        // `if a && true` reports 2 conditions: one for the identifier
9647        // operand, one for the boolean-literal operand. Confirms the
9648        // walker terminal set includes `BooleanLiteral`.
9649        check_metrics::<RustParser>(
9650            "fn f(a: bool) -> bool { a && true }\n",
9651            "foo.rs",
9652            |metric| {
9653                assert_eq!(metric.abc.conditions_sum(), 2);
9654                insta::assert_json_snapshot!(metric.abc);
9655            },
9656        );
9657    }
9658
9659    // ----- Go -----
9660
9661    #[cfg(feature = "go")]
9662    #[test]
9663    fn go_empty_unit_zero() {
9664        // Package declaration only — no Fitzpatrick events. Confirms the
9665        // GoCode Abc trait is wired up and emits zero counts.
9666        check_metrics::<GoParser>("package main\n", "empty.go", |metric| {
9667            assert_eq!(metric.abc.assignments_sum(), 0);
9668            assert_eq!(metric.abc.branches_sum(), 0);
9669            assert_eq!(metric.abc.conditions_sum(), 0);
9670            insta::assert_json_snapshot!(metric.abc);
9671        });
9672    }
9673
9674    #[cfg(feature = "go")]
9675    #[test]
9676    fn go_assignments_count_plain_compound_short_var_and_incdec() {
9677        // `x := 0` (short var decl), `x = 5` and `x = 7` (plain `=`),
9678        // `x += 2` (compound), `x++` (inc), and the initialized
9679        // declaration `var y = 1` — which is counted, matching the Rust
9680        // and Java rules for `let y = 1` / `int y = 1` (both measured at
9681        // one assignment each). The comment here previously claimed the
9682        // opposite and pinned Go at 6 (#1278).
9683        check_metrics::<GoParser>(
9684            "package main\nfunc f() { var y = 1; _ = y; x := 0; x = 5; x += 2; x = 7; x++ }\n",
9685            "foo.go",
9686            |metric| {
9687                // `_ = y` is itself an assignment_statement → +1.
9688                // var y=1 + _=y + x:= + x=5 + x+=2 + x=7 + x++ → 7
9689                assert_eq!(metric.abc.assignments_sum(), 7);
9690                assert_eq!(metric.abc.branches_sum(), 0);
9691                assert_eq!(metric.abc.conditions_sum(), 0);
9692                insta::assert_json_snapshot!(metric.abc);
9693            },
9694        );
9695    }
9696
9697    #[cfg(feature = "go")]
9698    #[test]
9699    fn go_var_declarations_count_only_when_initialized() {
9700        // Regression for #1278: a `var` declaration with an initializer is
9701        // a `var_spec` carrying a `value` field, not an
9702        // `assignment_statement` or `short_var_declaration`, so it scored
9703        // zero — `var x = 5` and `x := 5` are the same binding spelled two
9704        // ways. Both typed and untyped initializers count; an
9705        // uninitialized `var z int` and a `const` do not.
9706        // expected: 3 assignments — `var x = 5`, `var y int = 6`, `z := 7`;
9707        // `var w int` and `const c = 1` contribute nothing.
9708        check_metrics::<GoParser>(
9709            "package main\nfunc f() int {\n\tvar x = 5\n\tvar y int = 6\n\tvar w int\n\tconst c = 1\n\tz := 7\n\treturn x + y + w + c + z\n}\n",
9710            "foo.go",
9711            |metric| {
9712                assert_eq!(metric.abc.assignments_sum(), 3);
9713                assert_eq!(metric.abc.branches_sum(), 0);
9714            },
9715        );
9716    }
9717
9718    #[cfg(feature = "go")]
9719    #[test]
9720    fn go_grouped_var_block_counts_each_initialized_spec() {
9721        // A grouped `var ( … )` block is one `var_declaration` holding one
9722        // `var_spec` per line, so matching the spec counts each initialized
9723        // line on its own. A multi-name spec is still one binding
9724        // statement, matching `p, q := 1, 2` (#1278).
9725        // expected: 3 assignments — `a = 1`, `p, q = 1, 2`, and `r := 0`;
9726        // the uninitialized `b int` contributes nothing.
9727        check_metrics::<GoParser>(
9728            "package main\nfunc f() {\n\tvar (\n\t\ta = 1\n\t\tb int\n\t)\n\tvar p, q = 1, 2\n\tr := 0\n\t_ = a + b + p + q + r\n}\n",
9729            "foo.go",
9730            |metric| {
9731                // The trailing `_ = …` is itself an assignment_statement.
9732                assert_eq!(metric.abc.assignments_sum(), 4);
9733            },
9734        );
9735    }
9736
9737    #[cfg(feature = "go")]
9738    #[test]
9739    fn go_calls_are_branches() {
9740        // Three calls: free function `g()`, method call `r.Inc()`, and
9741        // builtin call `len(s)`. All parse as `call_expression` → B = 3.
9742        // Composite literal `Foo{}` is NOT a call.
9743        check_metrics::<GoParser>(
9744            "package main\n\
9745             type R struct{}\n\
9746             func (r R) Inc() {}\n\
9747             func g() {}\n\
9748             func f(s string) { g(); var r R = R{}; r.Inc(); _ = len(s) }\n",
9749            "foo.go",
9750            |metric| {
9751                assert_eq!(metric.abc.branches_sum(), 3);
9752                insta::assert_json_snapshot!(metric.abc);
9753            },
9754        );
9755    }
9756
9757    #[cfg(feature = "go")]
9758    #[test]
9759    fn go_comparisons_count_conditions() {
9760        // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six
9761        // comparisons → C = 6.
9762        check_metrics::<GoParser>(
9763            "package main\nfunc f(a, b int) bool { return a < b || a > b || a <= b || a >= b || a == b || a != b }\n",
9764            "foo.go",
9765            |metric| {
9766                assert_eq!(metric.abc.conditions_sum(), 6);
9767                insta::assert_json_snapshot!(metric.abc);
9768            },
9769        );
9770    }
9771
9772    #[cfg(feature = "go")]
9773    #[test]
9774    fn go_generic_brackets_not_conditions() {
9775        // Generic instantiation `Min[int](a, b)` puts `int` inside
9776        // `TypeArguments`, not `BinaryExpression`. The parent guard on
9777        // `<` / `>` must not count these. Expected C = 0; B = 1 (one call).
9778        check_metrics::<GoParser>(
9779            "package main\nfunc Min[T int | float64](a, b T) T { return a }\nfunc f() { _ = Min[int](1, 2) }\n",
9780            "foo.go",
9781            |metric| {
9782                assert_eq!(metric.abc.conditions_sum(), 0);
9783                assert_eq!(metric.abc.branches_sum(), 1);
9784                insta::assert_json_snapshot!(metric.abc);
9785            },
9786        );
9787    }
9788
9789    #[cfg(feature = "go")]
9790    #[test]
9791    fn go_switch_arms_count_conditions_default_excluded() {
9792        // Four arms: `case 1:`, `case 2:`, `case 3:`, `default:`. The
9793        // bare `default` is the C/Java `default:` equivalent and is
9794        // excluded — 3 conditions from ExpressionCase. The switch
9795        // expression `x` is bare (no comparison), so no extra
9796        // condition from `==`-style operators.
9797        check_metrics::<GoParser>(
9798            "package main\nfunc f(x int) int { switch x { case 1: return 1; case 2: return 2; case 3: return 3; default: return 0 } }\n",
9799            "foo.go",
9800            |metric| {
9801                assert_eq!(metric.abc.conditions_sum(), 3);
9802                insta::assert_json_snapshot!(metric.abc);
9803            },
9804        );
9805    }
9806
9807    #[cfg(feature = "go")]
9808    #[test]
9809    fn go_type_switch_arms_count_conditions() {
9810        // Type switch: `case int:`, `case string:`, `default:`. Two
9811        // non-default type-case arms → C = 2.
9812        check_metrics::<GoParser>(
9813            "package main\nfunc f(v interface{}) { switch v.(type) { case int: return; case string: return; default: return } }\n",
9814            "foo.go",
9815            |metric| {
9816                assert_eq!(metric.abc.conditions_sum(), 2);
9817                insta::assert_json_snapshot!(metric.abc);
9818            },
9819        );
9820    }
9821
9822    #[cfg(feature = "go")]
9823    #[test]
9824    fn go_select_arms_count_conditions() {
9825        // `select { case <-ch: ...; case ch <- 1: ...; default: ... }`.
9826        // Two non-default communication cases → C = 2.
9827        check_metrics::<GoParser>(
9828            "package main\nfunc f(ch chan int) { select { case <-ch: return; case ch <- 1: return; default: return } }\n",
9829            "foo.go",
9830            |metric| {
9831                assert_eq!(metric.abc.conditions_sum(), 2);
9832                insta::assert_json_snapshot!(metric.abc);
9833            },
9834        );
9835    }
9836
9837    #[cfg(feature = "go")]
9838    #[test]
9839    fn go_else_counts_as_condition() {
9840        // `if a > b { ... } else { ... }` → `a > b` is one condition,
9841        // `else` is one condition → C = 2.
9842        check_metrics::<GoParser>(
9843            "package main\nfunc f(a, b int) int { if a > b { return a } else { return b } }\n",
9844            "foo.go",
9845            |metric| {
9846                assert_eq!(metric.abc.conditions_sum(), 2);
9847                insta::assert_json_snapshot!(metric.abc);
9848            },
9849        );
9850    }
9851
9852    #[cfg(feature = "go")]
9853    #[test]
9854    fn go_complex_function_abc() {
9855        // Mixed shape, verified by hand:
9856        // - Assignments: `var x = 10` (an initialized declaration, #1278),
9857        //   `_ = x`, `n := 0`, `n = n + 1`, `n += 2`, `n++`,
9858        //   `_ = len(s)` → A = 7. Every `_ = ...` IS counted as an
9859        //   assignment_statement.
9860        // - Branches: `len(s)` → B = 1.
9861        // - Conditions: `n < 10` → 1, `else` → 1, switch arms `case 0:`
9862        //   and `case 1:` (default excluded) → 2 → total C = 4.
9863        check_metrics::<GoParser>(
9864            "package main\nfunc f(s string) int {\n\
9865             \x20   var x = 10\n\
9866             \x20   _ = x\n\
9867             \x20   n := 0\n\
9868             \x20   if n < 10 { n = n + 1 } else { n += 2 }\n\
9869             \x20   n++\n\
9870             \x20   _ = len(s)\n\
9871             \x20   switch n {\n\
9872             \x20   case 0: return 0\n\
9873             \x20   case 1: return 1\n\
9874             \x20   default: return n\n\
9875             \x20   }\n\
9876             }\n",
9877            "foo.go",
9878            |metric| {
9879                assert_eq!(metric.abc.assignments_sum(), 7);
9880                assert_eq!(metric.abc.branches_sum(), 1);
9881                assert_eq!(metric.abc.conditions_sum(), 4);
9882                insta::assert_json_snapshot!(metric.abc);
9883            },
9884        );
9885    }
9886
9887    #[cfg(feature = "go")]
9888    #[test]
9889    fn go_if_multiple_conditions() {
9890        // Fitzpatrick Rule 7 walker fan-out (issue #403). Mirrors
9891        // `rust_if_multiple_conditions`.
9892        check_metrics::<GoParser>(
9893            "package p\n\
9894             func F(a, b, c, d bool) int {\n\
9895             \x20   if a || b || c || d { return 1 }    // +4c\n\
9896             \x20   if a && b && c { return 2 }         // +3c\n\
9897             \x20   if !a && !b { return 3 }            // +2c\n\
9898             \x20   return 0\n\
9899             }\n",
9900            "foo.go",
9901            |metric| {
9902                assert_eq!(metric.abc.conditions_sum(), 9);
9903                insta::assert_json_snapshot!(metric.abc);
9904            },
9905        );
9906    }
9907
9908    #[cfg(feature = "go")]
9909    #[test]
9910    fn go_for_with_conditions() {
9911        // Go has no `while` or `do { … } while(…);` — the `for` loop
9912        // header is the sole condition slot. Each operand of the
9913        // `&&` / `||` chain in the for-condition counts as one
9914        // Fitzpatrick condition.
9915        check_metrics::<GoParser>(
9916            "package p\n\
9917             func F(a, b bool) {\n\
9918             \x20   for a || b { break }       // +2c\n\
9919             \x20   for a && !b { break }      // +2c\n\
9920             }\n",
9921            "foo.go",
9922            |metric| {
9923                assert_eq!(metric.abc.conditions_sum(), 4);
9924                insta::assert_json_snapshot!(metric.abc);
9925            },
9926        );
9927    }
9928
9929    #[cfg(feature = "go")]
9930    #[test]
9931    fn go_for_bare_condition_counts() {
9932        // Regression for findings.md #1: `for true {}` / `for !ready {}`
9933        // are Go's only loop-condition slot. Pre-fix, the Phase-2B
9934        // dispatcher had no `G::ForStatement` arm, so bare-boolean
9935        // and `!`-wrapped `for` conditions silently reported zero.
9936        // `go_count_condition`'s terminal-bool / paren / unary filter
9937        // makes the walker safe across all three for-statement shapes:
9938        // bare condition, `for_clause` (init; cond; post) — whose own
9939        // `condition` field #1276 taught the walker to read — and
9940        // `range_clause`, which has no such field and contributes
9941        // nothing.
9942        check_metrics::<GoParser>(
9943            "package p\n\
9944             func F(ready bool) {\n\
9945             \x20   for true { break }      // +1c\n\
9946             \x20   for !ready { break }    // +1c\n\
9947             \x20   for i := 0; i < 3; i++ { _ = i }    // +1c (the `<`)\n\
9948             }\n",
9949            "foo.go",
9950            |metric| {
9951                // `for true`: walker counts True (+1).
9952                // `for !ready`: walker on unary unwraps to `ready`
9953                //   (+1).
9954                // `for_clause`'s condition is `i < 3`, a
9955                //   `binary_expression` that `go_count_condition`
9956                //   filters out; the `<` itself contributes 1 via the
9957                //   pre-existing LT/GT arm.
9958                // Total: 3.
9959                assert_eq!(metric.abc.conditions_sum(), 3);
9960                insta::assert_json_snapshot!(metric.abc);
9961            },
9962        );
9963    }
9964
9965    // Issue #1276, Go's share. `for_statement`'s child(1) is the
9966    // condition only in the `for cond {}` spelling; the three-clause
9967    // form puts it one level down, in the `for_clause`'s `condition`
9968    // field. Letting the `for_clause` fall through — which the arm's
9969    // own comment used to call harmless — scored a bare three-clause
9970    // condition zero while `for a {}` scored one.
9971    #[cfg(feature = "go")]
9972    #[test]
9973    fn go_three_clause_for_condition_counts() {
9974        // Bare identifier in the three-clause header: no comparison
9975        // token, so only the `for_clause` lookup can count it.
9976        check_metrics::<GoParser>(
9977            "package p\n\
9978             func F(a bool) {\n\
9979             \x20   for i := 0; a; i++ { _ = i }\n\
9980             }\n",
9981            "foo.go",
9982            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
9983        );
9984        // Negation, through `go_inspect_container`'s `!` unwrap.
9985        check_metrics::<GoParser>(
9986            "package p\n\
9987             func F(a bool) {\n\
9988             \x20   for i := 0; !a; i++ { _ = i }\n\
9989             }\n",
9990            "foo.go",
9991            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
9992        );
9993        // Empty condition: the `for_clause` exposes no `condition`
9994        // field, so zero — the same answer as Go's bare `for {}` and
9995        // as every other language since #1276.
9996        check_metrics::<GoParser>(
9997            "package p\n\
9998             func F() {\n\
9999             \x20   for i := 0; ; i++ { break }\n\
10000             }\n",
10001            "foo.go",
10002            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
10003        );
10004        // A `range_clause` carries no condition either, and the walker
10005        // must not mistake the clause itself for one.
10006        check_metrics::<GoParser>(
10007            "package p\n\
10008             func F(xs []int) {\n\
10009             \x20   for _, v := range xs { _ = v }\n\
10010             }\n",
10011            "foo.go",
10012            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
10013        );
10014    }
10015
10016    // Go's `for_statement` is the one grammar here that exposes no
10017    // `condition` field, so its header slot is located structurally and
10018    // has to skip what the field-addressed siblings get for free. Two
10019    // things it must skip, each of which `node.child(1)` got wrong:
10020    // a leading comment (tree-sitter counts comments among a node's
10021    // children — the #1181 failure), and the body of a bare `for {}`,
10022    // which IS child(1) and would otherwise be offered to
10023    // `go_count_condition` as though it were a condition.
10024    #[cfg(feature = "go")]
10025    #[test]
10026    fn go_for_header_slot_skips_comments_and_the_body() {
10027        // Each pair is (source, expected conditions). The commented
10028        // spelling must agree with its bare twin.
10029        let cases = [
10030            ("for a { break }", 1),
10031            ("for /* n */ a { break }", 1),
10032            ("for i := 0; a; i++ { _ = i }", 1),
10033            ("for /* n */ i := 0; a; i++ { _ = i }", 1),
10034            ("for i := 0; /* n */ a; i++ { _ = i }", 1),
10035            // Bare infinite loop: the body is child(1) and must not be
10036            // read as the condition.
10037            ("for { break }", 0),
10038            ("for /* n */ { break }", 0),
10039            ("for _, v := range xs { _ = v }", 0),
10040            ("for /* n */ _, v := range xs { _ = v }", 0),
10041        ];
10042        let mut ran = 0;
10043        for (body, expected) in cases {
10044            let src = format!("package p\nfunc F(a bool, xs []int) {{\n\t{body}\n}}\n");
10045            assert_eq!(abc_conditions(LANG::Go, &src), expected, "`{body}`");
10046            ran += 1;
10047        }
10048        // Non-vacuity, both halves: the loop must actually have run
10049        // every row, and the rows must carry both answers — a walker
10050        // stuck at 0 or at 1 would otherwise pass half the table
10051        // silently.
10052        assert_eq!(ran, cases.len());
10053        assert!(cases.iter().any(|&(_, n)| n == 1));
10054        assert!(cases.iter().any(|&(_, n)| n == 0));
10055    }
10056
10057    #[cfg(feature = "go")]
10058    #[test]
10059    fn go_if_init_statement_condition_counts() {
10060        // Regression for the code-review finding: Go's
10061        // `if x := f(); x { ... }` init-statement form puts the
10062        // short-var declaration at child(1) and the condition at
10063        // child(2). Pre-fix, the dispatcher used child(1) and
10064        // counted zero conditions for this idiomatic Go shape.
10065        // The fix uses `child_by_field_name("condition")` which
10066        // returns the condition regardless of init presence.
10067        check_metrics::<GoParser>(
10068            "package p\nfunc F() { if x := g(); x { } }\n",
10069            "foo.go",
10070            |metric| {
10071                // `x` bare-identifier condition contributes 1
10072                // (Rule 6 — bare boolean identifier in if-condition).
10073                // `g()` call contributes 1 branch but no condition.
10074                assert_eq!(metric.abc.branches_sum(), 1);
10075                assert_eq!(metric.abc.conditions_sum(), 1);
10076                insta::assert_json_snapshot!(metric.abc);
10077            },
10078        );
10079    }
10080
10081    #[cfg(feature = "go")]
10082    #[test]
10083    fn go_if_boolean_literal_condition() {
10084        check_metrics::<GoParser>(
10085            "package p\n\
10086             func F() {\n\
10087             \x20   if true {}                  // +1c\n\
10088             \x20   if !false {}                // +1c\n\
10089             }\n",
10090            "foo.go",
10091            |metric| {
10092                assert_eq!(metric.abc.conditions_sum(), 2);
10093                insta::assert_json_snapshot!(metric.abc);
10094            },
10095        );
10096    }
10097
10098    #[cfg(feature = "go")]
10099    #[test]
10100    fn go_methods_arguments_with_conditions() {
10101        check_metrics::<GoParser>(
10102            "package p\n\
10103             func F(a, b bool) {\n\
10104             \x20   m(a, b)                     // +1b\n\
10105             \x20   m(!a, !b)                   // +1b +2c\n\
10106             }\n",
10107            "foo.go",
10108            |metric| {
10109                assert_eq!(metric.abc.branches_sum(), 2);
10110                assert_eq!(metric.abc.conditions_sum(), 2);
10111                insta::assert_json_snapshot!(metric.abc);
10112            },
10113        );
10114    }
10115
10116    #[cfg(feature = "go")]
10117    #[test]
10118    fn go_return_with_conditions() {
10119        check_metrics::<GoParser>(
10120            "package p\n\
10121             func M1(z int) bool { return !(z >= 0) }\n\
10122             func M2(x bool) bool { return !x }\n\
10123             func M3(x, y bool) bool { return x && y }\n",
10124            "foo.go",
10125            |metric| {
10126                // M1: `>=` (1). `!(z >= 0)` walker on the unary
10127                //     doesn't reach a terminal — stops at the
10128                //     BinaryExpression z>=0 inside the parens. +1.
10129                // M2: walker on `!x` → 1.
10130                // M3: `&&` walker counts both → 2.
10131                // Sum: 1 + 1 + 2 = 4.
10132                assert_eq!(metric.abc.conditions_sum(), 4);
10133                insta::assert_json_snapshot!(metric.abc);
10134            },
10135        );
10136    }
10137
10138    #[cfg(feature = "go")]
10139    #[test]
10140    fn go_short_circuit_with_boolean_literal_operand() {
10141        // `a && true` reports 2 conditions: one identifier, one
10142        // boolean literal. Confirms the terminal set includes
10143        // `True` / `False`.
10144        check_metrics::<GoParser>(
10145            "package p\nfunc F(a bool) bool { return a && true }\n",
10146            "foo.go",
10147            |metric| {
10148                assert_eq!(metric.abc.conditions_sum(), 2);
10149                insta::assert_json_snapshot!(metric.abc);
10150            },
10151        );
10152    }
10153
10154    // ----- Elixir -----
10155
10156    // No top-level Calls and no operators → all three vectors are
10157    // zero. Uses a bare expression rather than a `defmodule` wrapper
10158    // (which would itself be a Call → 1 branch). Confirms the
10159    // ElixirCode Abc trait is wired up and the metric emits.
10160    #[cfg(feature = "elixir")]
10161    #[test]
10162    fn elixir_empty_unit_zero() {
10163        check_metrics::<ElixirParser>(":ok\n", "foo.ex", |metric| {
10164            assert_eq!(metric.abc.assignments_sum(), 0);
10165            assert_eq!(metric.abc.branches_sum(), 0);
10166            assert_eq!(metric.abc.conditions_sum(), 0);
10167            insta::assert_json_snapshot!(metric.abc);
10168        });
10169    }
10170
10171    // An empty `defmodule Foo do ... end` is itself ONE `Call` →
10172    // Documents that module-/function-defining macros (`defmodule`,
10173    // `def`, `defp`, `defmacro`, `defmacrop`) and declarative
10174    // directives (`alias`, `import`, `require`, `use`) are NOT
10175    // runtime dispatch and therefore do NOT inflate `branches`,
10176    // matching Cognitive's treatment.
10177    #[cfg(feature = "elixir")]
10178    #[test]
10179    fn elixir_defmodule_is_zero_branches() {
10180        check_metrics::<ElixirParser>("defmodule Foo do\nend\n", "foo.ex", |metric| {
10181            assert_eq!(metric.abc.branches_sum(), 0);
10182            assert_eq!(metric.abc.assignments_sum(), 0);
10183            assert_eq!(metric.abc.conditions_sum(), 0);
10184            insta::assert_json_snapshot!(metric.abc);
10185        });
10186    }
10187
10188    // Pattern-match `=` counts as an assignment. Two bindings → A = 2.
10189    // `defmodule` and `def` are declarative-Call wrappers and are
10190    // filtered out of branches; the assertion focuses on assignments
10191    // so we only pin that vector.
10192    #[cfg(feature = "elixir")]
10193    #[test]
10194    fn elixir_pattern_match_is_assignment() {
10195        check_metrics::<ElixirParser>(
10196            "defmodule Foo do\n  def f do\n    x = 1\n    y = x + 1\n    y\n  end\nend\n",
10197            "foo.ex",
10198            |metric| {
10199                assert_eq!(metric.abc.assignments_sum(), 2);
10200                insta::assert_json_snapshot!(metric.abc);
10201            },
10202        );
10203    }
10204
10205    // `|>` pipeline operator: each `|>` token contributes one branch.
10206    // Two `|>` ops → +2 from the pipe operator itself. Each pipeline
10207    // step also dispatches a Call (`String.upcase(...)`,
10208    // `String.trim(...)`) — these are wrapped inside the outer
10209    // pipeline Call tree, contributing additional Call branches.
10210    // The headline assertion confirms (a) `|>` is detected and (b)
10211    // pipeline steps are not silently dropped.
10212    #[cfg(feature = "elixir")]
10213    #[test]
10214    fn elixir_pipeline_each_step_is_branch() {
10215        check_metrics::<ElixirParser>(
10216            "defmodule Foo do\n  def normalize(s) do\n    s |> String.trim() |> String.upcase()\n  end\nend\n",
10217            "foo.ex",
10218            |metric| {
10219                // Pipeline yields 2 `|>` branches plus Calls for
10220                // String.trim, String.upcase, and the outer pipeline
10221                // (which surfaces as a Call wrapping the binary
10222                // operator). `def` and `defmodule` are declarative
10223                // and excluded. Empirical total: B = 5.
10224                assert_eq!(metric.abc.branches_sum(), 5);
10225                assert_eq!(metric.abc.assignments_sum(), 0);
10226                insta::assert_json_snapshot!(metric.abc);
10227            },
10228        );
10229    }
10230
10231    // Comparison operators all count as conditions. Six comparisons
10232    // (`==`, `!=`, `<`, `>`, `<=`, `>=`) → C = 6.
10233    #[cfg(feature = "elixir")]
10234    #[test]
10235    fn elixir_comparisons_are_conditions() {
10236        check_metrics::<ElixirParser>(
10237            "defmodule Foo do\n  def f(a, b) do\n    a == b or a != b or a < b or a > b or a <= b or a >= b\n  end\nend\n",
10238            "foo.ex",
10239            |metric| {
10240                assert_eq!(metric.abc.conditions_sum(), 6);
10241                insta::assert_json_snapshot!(metric.abc);
10242            },
10243        );
10244    }
10245
10246    // Strict-equality operators `===` / `!==` count as conditions too.
10247    #[cfg(feature = "elixir")]
10248    #[test]
10249    fn elixir_strict_equality_is_condition() {
10250        check_metrics::<ElixirParser>(
10251            "defmodule Foo do\n  def f(a, b) do\n    a === b or a !== b\n  end\nend\n",
10252            "foo.ex",
10253            |metric| {
10254                assert_eq!(metric.abc.conditions_sum(), 2);
10255                insta::assert_json_snapshot!(metric.abc);
10256            },
10257        );
10258    }
10259
10260    // #1461 moved every relational construct a grammar spells as its own
10261    // production onto an unconditional arm — scored by use, Fitzpatrick
10262    // Rule 5 — in C#, Java, Groovy, Kotlin and Ruby. Elixir's membership
10263    // and type tests were the same class and were not in that sweep, so
10264    // `a in b` scored zero where `a == b` scored one. The gap stayed
10265    // invisible while the `when` arm paid a flat one for every guard,
10266    // and surfaced the moment the guard became a slot: an
10267    // operator-spelled guard is owned by its operator's arm, and `in`
10268    // had none.
10269    //
10270    // `bare` / `negbare` are the by-use claim; `slot` is the §5 check
10271    // that a boolean slot does not now score the same operator twice;
10272    // `cmp` is the control the two membership rows are levelled against.
10273    // `in` rides the `<` / `>` `binary_operator` gate rather than
10274    // standing alone — it has the same three grammar positions — and
10275    // `elixir_operator_identifier_is_not_a_condition` pins that gate.
10276    #[cfg(feature = "elixir")]
10277    #[test]
10278    fn elixir_membership_is_a_condition_by_use() {
10279        check_func_space::<ElixirParser, _>(
10280            "defmodule Foo do\n\
10281               def bare(a, b), do: a in b\n\
10282               def negbare(a, b), do: a not in b\n\
10283               def slot(a, b) do\n\
10284                 if a in b do\n\
10285                   IO.puts(\"x\")\n\
10286                 end\n\
10287               end\n\
10288               def cmp(a, b), do: a == b\n\
10289             end\n",
10290            "foo.ex",
10291            |space| {
10292                assert_members_score(
10293                    &space.spaces[0],
10294                    &[
10295                        // Each was 0 before the arm.
10296                        ("bare", 1, 1),
10297                        ("negbare", 1, 1),
10298                        // `if` (1) + `in` (1), exactly as `if a == b`
10299                        // scores.
10300                        ("slot", 2, 2),
10301                        ("cmp", 1, 1),
10302                    ],
10303                );
10304            },
10305        );
10306    }
10307
10308    // Guard `when` clause counts as a condition — exactly one, and here
10309    // it is the `>` that supplies it. The guard is a condition *slot*
10310    // (#1454), so an operator-spelled guard scores through the operator
10311    // arm that owns it and the slot itself adds nothing; see
10312    // `elixir_guard_is_a_decision_however_spelled` for the four-spelling
10313    // agreement this is one row of.
10314    //
10315    // Was 2, on a flat `+1` for the `when` token laid on top of the `>`:
10316    // the §5 double count a whole-branch review of the #1454 batch
10317    // found.
10318    #[cfg(feature = "elixir")]
10319    #[test]
10320    fn elixir_guard_when_is_condition() {
10321        check_metrics::<ElixirParser>(
10322            "defmodule Foo do\n  def f(x) when x > 0 do\n    :pos\n  end\nend\n",
10323            "foo.ex",
10324            |metric| {
10325                // the guard slot (+0, the guard is an operator
10326                // application) + `>` (+1) = 1
10327                assert_eq!(metric.abc.conditions_sum(), 1);
10328                insta::assert_json_snapshot!(metric.abc);
10329            },
10330        );
10331    }
10332
10333    // Keyword-shaped Calls (`case`, `cond`, `if`, `with`) each count
10334    // as one condition AND one branch. `case` here adds 1 condition
10335    // (the keyword Call) + 1 branch (the Call itself).
10336    #[cfg(feature = "elixir")]
10337    #[test]
10338    fn elixir_case_is_condition_and_branch() {
10339        check_metrics::<ElixirParser>(
10340            "defmodule Foo do\n  def f(x) do\n    case x do\n      1 -> :one\n      _ -> :other\n    end\n  end\nend\n",
10341            "foo.ex",
10342            |metric| {
10343                // conditions: case → 1
10344                assert_eq!(metric.abc.conditions_sum(), 1);
10345                insta::assert_json_snapshot!(metric.abc);
10346            },
10347        );
10348    }
10349
10350    // `cond` is structurally identical to `case` for Abc.
10351    #[cfg(feature = "elixir")]
10352    #[test]
10353    fn elixir_cond_is_condition() {
10354        check_metrics::<ElixirParser>(
10355            "defmodule Foo do\n  def f(x) do\n    cond do\n      x > 0 -> :pos\n      true -> :other\n    end\n  end\nend\n",
10356            "foo.ex",
10357            |metric| {
10358                // conditions: cond (+1) + > (+1) = 2
10359                assert_eq!(metric.abc.conditions_sum(), 2);
10360                insta::assert_json_snapshot!(metric.abc);
10361            },
10362        );
10363    }
10364
10365    // `for` is a comprehension/loop, NOT in the issue's condition
10366    // list. It is still a Call so it contributes one branch, but no
10367    // condition.
10368    #[cfg(feature = "elixir")]
10369    #[test]
10370    fn elixir_for_is_branch_not_condition() {
10371        check_metrics::<ElixirParser>(
10372            "defmodule Foo do\n  def f(xs) do\n    for x <- xs, do: x * 2\n  end\nend\n",
10373            "foo.ex",
10374            |metric| {
10375                assert_eq!(metric.abc.conditions_sum(), 0);
10376                insta::assert_json_snapshot!(metric.abc);
10377            },
10378        );
10379    }
10380
10381    // Mixed shape, verified by hand: defmodule Call + def Call + if Call
10382    // + Call to side_effect/0 + assignment `x = 1` + comparison `x > 0`.
10383    // - Assignments: `x = 1` → A = 1.
10384    // - Branches: `defmodule` and `def` are declarative and excluded;
10385    //   `if` Call + `side_effect()` Call → 2 Calls, plus 0 `|>` → B = 2.
10386    // - Conditions: `if` keyword → 1, `x > 0` → 1 → C = 2.
10387    #[cfg(feature = "elixir")]
10388    #[test]
10389    fn elixir_mixed_abc() {
10390        check_metrics::<ElixirParser>(
10391            "defmodule Foo do\n  def f do\n    x = 1\n    if x > 0 do\n      side_effect()\n    end\n  end\nend\n",
10392            "foo.ex",
10393            |metric| {
10394                assert_eq!(metric.abc.assignments_sum(), 1);
10395                assert_eq!(metric.abc.branches_sum(), 2);
10396                assert_eq!(metric.abc.conditions_sum(), 2);
10397                insta::assert_json_snapshot!(metric.abc);
10398            },
10399        );
10400    }
10401
10402    #[cfg(feature = "elixir")]
10403    #[test]
10404    fn elixir_unary_conditions_in_chain() {
10405        // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
10406        // `&&` / `||` chain is one condition. For `if a && b || c`: the
10407        // `if` keyword Call contributes 1 condition, and the walker adds
10408        // a, b, c → 3. expected: 4 conditions, consistent with the
10409        // function's cyclomatic complexity of 4 (base 1 + if + && + ||).
10410        check_metrics::<ElixirParser>(
10411            "defmodule Foo do\n  def f(a, b, c) do\n    if a && b || c do\n      IO.puts(\"x\")\n    end\n  end\nend\n",
10412            "foo.ex",
10413            |metric| {
10414                assert_eq!(metric.abc.conditions_sum(), 4);
10415            },
10416        );
10417    }
10418
10419    // Elixir spells negation two ways and `elixir_inspect_container`
10420    // recognised only `!`, so `a && not b` scored 3 where `a && !b`
10421    // scored 4 — the `not` operand reached no terminal and vanished.
10422    // Both forms now read the same. `not` is the stricter of the two (it
10423    // raises on a non-boolean operand where `!` accepts any truthy
10424    // value), so it is at least as good a proof that what it wraps is
10425    // boolean, which is all the walker's flag claims.
10426    //
10427    // Both members in one fixture because the claim is that they agree:
10428    // asserting either alone says nothing about the pair (§11), and a
10429    // shared `check_metrics` total could not tell 3 + 4 from 4 + 3.
10430    //
10431    // `assert_members_score` rather than the parity-asserting sibling:
10432    // a `&&` operand is an ABC condition with no cyclomatic decision
10433    // behind it, so both members legitimately sit one above their
10434    // decision count (`base 1 + if + &&` = 3).
10435    #[cfg(feature = "elixir")]
10436    #[test]
10437    fn elixir_keyword_not_negates_like_bang() {
10438        check_func_space::<ElixirParser, _>(
10439            "defmodule Foo do\n  def kw(a, b) do\n    if a && not b do\n      IO.puts(\"x\")\n    end\n  end\n  def bang(a, b) do\n    if a && !b do\n      IO.puts(\"x\")\n    end\n  end\nend\n",
10440            "foo.ex",
10441            |space| {
10442                assert_members_score(&space.spaces[0], &[("kw", 3, 3), ("bang", 3, 3)]);
10443            },
10444        );
10445    }
10446
10447    #[cfg(feature = "elixir")]
10448    #[test]
10449    fn elixir_comparison_operands_add_nothing() {
10450        // Isolation check: comparison operands of a `&&` chain are nested
10451        // `binary_operator` nodes, not bare boolean leaves, so the walker
10452        // adds nothing. expected: 3 = `if` (1) + `>` (1) + `>` (1); the
10453        // `&&` walker contributes 0.
10454        check_metrics::<ElixirParser>(
10455            "defmodule Foo do\n  def f(x, y) do\n    if x > 0 && y > 0 do\n      IO.puts(\"x\")\n    end\n  end\nend\n",
10456            "foo.ex",
10457            |metric| {
10458                assert_eq!(metric.abc.conditions_sum(), 3);
10459            },
10460        );
10461    }
10462
10463    #[cfg(feature = "elixir")]
10464    #[test]
10465    fn elixir_keyword_and_or_chain_counts_operands() {
10466        // The keyword forms `and` / `or` get the same Rule 9 treatment as
10467        // `&&` / `||`. expected: 4 = `if` (1) + operands a, b, c (3).
10468        check_metrics::<ElixirParser>(
10469            "defmodule Foo do\n  def f(a, b, c) do\n    if a and b or c do\n      IO.puts(\"x\")\n    end\n  end\nend\n",
10470            "foo.ex",
10471            |metric| {
10472                assert_eq!(metric.abc.conditions_sum(), 4);
10473            },
10474        );
10475    }
10476
10477    // Sigil delimiter choice must not move ABC: `~s<hi>` and `~s(hi)`
10478    // are the same value spelled differently, but the `<` / `>`
10479    // delimiter tokens carry the comparison kind ids, so the unguarded
10480    // condition arm scored `~s<hi>` as 2 conditions and `~s(hi)` as 0.
10481    // The parent-is-`Sigil` guard (mirroring the Halstead getter's,
10482    // #1256) suppresses the delimiter case. expected, for each
10483    // spelling: A = 1 (the `x =` pattern match), B = 0 (a bare sigil
10484    // is not a `Call` node — verified by AST dump: `binary_operator`
10485    // wrapping `identifier`, `=`, `sigil`), C = 0 (no comparison, no
10486    // guard, no keyword Call).
10487    #[cfg(feature = "elixir")]
10488    #[test]
10489    fn elixir_sigil_delimiter_choice_is_abc_invariant() {
10490        for src in ["x = ~s<hi>\n", "x = ~s(hi)\n"] {
10491            check_metrics::<ElixirParser>(src, "foo.ex", |metric| {
10492                assert_eq!(metric.abc.assignments_sum(), 1);
10493                assert_eq!(metric.abc.branches_sum(), 0);
10494                assert_eq!(metric.abc.conditions_sum(), 0);
10495            });
10496        }
10497    }
10498
10499    // Control for the guard above: `<` *outside* a sigil is a genuine
10500    // comparison and must keep counting even with a `<`-delimited
10501    // sigil in the same unit. expected: A = 2 (`x =`, `y =`), C = 1
10502    // (only `a < b`; the sigil's `<` / `>` delimiters are guarded).
10503    #[cfg(feature = "elixir")]
10504    #[test]
10505    fn elixir_lt_comparison_still_counts_beside_sigil() {
10506        check_metrics::<ElixirParser>("x = ~s<hi>\ny = a < b\n", "foo.ex", |metric| {
10507            assert_eq!(metric.abc.assignments_sum(), 2);
10508            assert_eq!(metric.abc.conditions_sum(), 1);
10509        });
10510    }
10511
10512    // #1297's sweep converted six grammars from a denylist of
10513    // non-comparison `<` / `>` parents to an allowlist and recorded
10514    // Elixir as already covered by its sigil guard. It was not: an
10515    // operator *named* rather than applied — the capture `&</2` and the
10516    // qualified call `Kernel.<(a, b)` — puts the bare token under an
10517    // `operator_identifier`, which no denylist entry mentioned, so each
10518    // scored a condition against zero decisions. Same shape as the C#
10519    // `operator <` declaration the sweep did fix.
10520    //
10521    // Each function is its own space and its own row, so a fix that
10522    // reached one spelling and not the other fails here. `max/2` is the
10523    // control: a qualified call whose name is not an operator was
10524    // always 0, so a guard that merely stopped counting `<` everywhere
10525    // would pass this row and fail the two below it.
10526    #[cfg(feature = "elixir")]
10527    #[test]
10528    fn elixir_operator_identifier_is_not_a_condition() {
10529        check_func_space::<ElixirParser, _>(
10530            "defmodule M do\n\
10531               def asc(l), do: Enum.sort(l, &</2)\n\
10532               def lt(a, b), do: Kernel.<(a, b)\n\
10533               def big(a, b), do: Kernel.max(a, b)\n\
10534               def cmp(a, b), do: a < b\n\
10535             end\n",
10536            "foo.ex",
10537            |space| {
10538                let module = &space.spaces[0];
10539                let scored: Vec<(String, u64)> = module
10540                    .spaces
10541                    .iter()
10542                    .map(|f| {
10543                        (
10544                            f.name.clone().unwrap_or_default(),
10545                            f.metrics.abc.conditions(),
10546                        )
10547                    })
10548                    .collect();
10549                assert_eq!(
10550                    scored,
10551                    vec![
10552                        ("asc".to_owned(), 0),
10553                        ("lt".to_owned(), 0),
10554                        ("big".to_owned(), 0),
10555                        ("cmp".to_owned(), 1),
10556                    ],
10557                );
10558            },
10559        );
10560    }
10561
10562    // ----- C++ -----
10563
10564    #[cfg(feature = "cpp")]
10565    #[test]
10566    fn cpp_empty_unit_zero() {
10567        // No code → A=B=C=0. Wires up the trait and exercises the
10568        // per-language compute reachability.
10569        check_metrics::<CppParser>("", "empty.cpp", |metric| {
10570            assert_eq!(metric.abc.assignments_sum(), 0);
10571            assert_eq!(metric.abc.branches_sum(), 0);
10572            assert_eq!(metric.abc.conditions_sum(), 0);
10573            insta::assert_json_snapshot!(metric.abc);
10574        });
10575    }
10576
10577    #[cfg(feature = "cpp")]
10578    #[test]
10579    fn cpp_plain_and_compound_assignments_count() {
10580        // `int x = 0` is an `init_declarator` carrying an `=` token
10581        // and counts as 1 (post-#393: the literal Fitzpatrick rule
10582        // counts every `=` operator, matching the JS impl's
10583        // `let x = 5` treatment). `x = 5`, `x += 2`, `x = 7` all
10584        // parse as `assignment_expression` → 3. Total A = 4.
10585        check_metrics::<CppParser>(
10586            "void f() { int x = 0; x = 5; x += 2; x = 7; }",
10587            "foo.cpp",
10588            |metric| {
10589                assert_eq!(metric.abc.assignments_sum(), 4);
10590                assert_eq!(metric.abc.branches_sum(), 0);
10591                assert_eq!(metric.abc.conditions_sum(), 0);
10592                insta::assert_json_snapshot!(metric.abc);
10593            },
10594        );
10595    }
10596
10597    #[cfg(feature = "cpp")]
10598    #[test]
10599    fn cpp_increment_and_decrement_count_as_assignment() {
10600        // `x++` / `--x` / prefix and postfix forms each parse as
10601        // `update_expression` and count as 1 assignment per
10602        // Fitzpatrick — 4. `int x = 0` (init_declarator with `=`)
10603        // adds 1 (post-#393). Total A = 5.
10604        check_metrics::<CppParser>(
10605            "void f() { int x = 0; x++; --x; ++x; x--; }",
10606            "foo.cpp",
10607            |metric| {
10608                assert_eq!(metric.abc.assignments_sum(), 5);
10609                insta::assert_json_snapshot!(metric.abc);
10610            },
10611        );
10612    }
10613
10614    #[cfg(feature = "cpp")]
10615    #[test]
10616    fn cpp_init_declarators_count_as_assignments() {
10617        // Issue #393 regression: `int a=1;`, `int b=2;`, `int c=a+b;`,
10618        // `int d=0;` are all `init_declarator` nodes with `=` → 4
10619        // assignments. `d=5;` is one plain `assignment_expression`,
10620        // `d+=1;` is one compound. Total A = 4 + 1 + 1 = 6.
10621        check_metrics::<CppParser>(
10622            "void f() { int a=1; int b=2; int c=a+b; int d=0; d=5; d+=1; }",
10623            "foo.cpp",
10624            |metric| {
10625                assert_eq!(metric.abc.assignments_sum(), 6);
10626                insta::assert_json_snapshot!(metric.abc);
10627            },
10628        );
10629    }
10630
10631    #[cfg(feature = "cpp")]
10632    #[test]
10633    fn cpp_declaration_without_initializer_does_not_count() {
10634        // `int a;` parses as a plain declarator inside `declaration`,
10635        // NOT an `init_declarator` (the latter only appears when an
10636        // initializer is present). Regression test for issue #393:
10637        // un-initialised declarations contribute zero to A.
10638        check_metrics::<CppParser>("void f() { int a; a = 5; }", "foo.cpp", |metric| {
10639            // Only `a = 5` (assignment_expression) → A = 1.
10640            assert_eq!(metric.abc.assignments_sum(), 1);
10641            insta::assert_json_snapshot!(metric.abc);
10642        });
10643    }
10644
10645    #[cfg(feature = "cpp")]
10646    #[test]
10647    fn cpp_init_declarator_brace_paren_init_does_not_count() {
10648        // `init_declarator` has two grammar forms: `declarator = value`
10649        // (the `=` form) and `declarator argument_list_or_initializer_list`
10650        // (the `int x(5);` / `int x{5};` direct-init forms). Only the
10651        // first form contains an `=` token, so only it should count.
10652        // Regression test pinning that distinction so that
10653        // refactorings of the init_declarator arm don't accidentally
10654        // start counting direct-init too.
10655        check_metrics::<CppParser>(
10656            "void f() { int x(5); int y{7}; x = 1; }",
10657            "foo.cpp",
10658            |metric| {
10659                // Only `x = 1` (assignment_expression) → A = 1.
10660                assert_eq!(metric.abc.assignments_sum(), 1);
10661                insta::assert_json_snapshot!(metric.abc);
10662            },
10663        );
10664    }
10665
10666    #[cfg(feature = "cpp")]
10667    #[test]
10668    fn cpp_calls_are_branches() {
10669        // Free call + member-fn call (parses as `call_expression` with
10670        // a `field_expression` callee) + `new` allocation. All three
10671        // are branches → B = 3. `auto* p = new int(5)` is also an
10672        // `init_declarator` with `=` so it contributes one assignment
10673        // (post-#393); the snapshot pins that magnitude.
10674        check_metrics::<CppParser>(
10675            "struct S { void m(); }; void g(); void f() { g(); S s; s.m(); auto* p = new int(5); }",
10676            "foo.cpp",
10677            |metric| {
10678                assert_eq!(metric.abc.branches_sum(), 3);
10679                assert_eq!(metric.abc.assignments_sum(), 1);
10680                insta::assert_json_snapshot!(metric.abc);
10681            },
10682        );
10683    }
10684
10685    #[cfg(feature = "cpp")]
10686    #[test]
10687    fn cpp_comparisons_count_conditions() {
10688        // `<`, `>`, `<=`, `>=`, `==`, `!=`, and the C++20 spaceship
10689        // `<=>` each contribute one condition. The `||` short-
10690        // circuits add 0 (Fitzpatrick Rule 5, issue #395). Six
10691        // comparisons in the `||` chain plus `<=>` (1) plus the
10692        // outer `== 0` (1) → C = 8.
10693        check_metrics::<CppParser>(
10694            "#include <compare>\n\
10695             bool f(int a, int b) {\n\
10696                 return a < b || a > b || a <= b || a >= b || a == b || a != b || (a <=> b) == 0;\n\
10697             }\n",
10698            "foo.cpp",
10699            |metric| {
10700                // `<`, `>`, `<=`, `>=`, `==`, `!=` → 6 comparisons
10701                // from the chained `||` expression. `(a <=> b) == 0`
10702                // adds the spaceship `<=>` (1) + the outer `== 0`
10703                // (1) → 8 total. The six `||` short-circuits add 0
10704                // (Fitzpatrick Rule 5; issue #395).
10705                assert_eq!(metric.abc.conditions_sum(), 8);
10706                insta::assert_json_snapshot!(metric.abc);
10707            },
10708        );
10709    }
10710
10711    #[cfg(feature = "cpp")]
10712    #[test]
10713    fn cpp_short_circuit_ops_not_counted_directly() {
10714        // `&&` and `||` do NOT count on their own (see the
10715        // module-level `Stats` doc-comment; #395). Phase-2 walker
10716        // counts each operand of a logical chain once (#403), but
10717        // when every operand is itself a relational expression
10718        // (`a == b`, `a > 0`, `b < 0`) the walker doesn't add
10719        // anything on top of the existing comparison-token tally
10720        // — relational sub-expressions are not in
10721        // `cpp_bool_terminal_kinds!()` and `cpp_inspect_container`
10722        // does not recurse into them.
10723        check_metrics::<CppParser>(
10724            "bool f(int a, int b) { return a == b && a > 0 || b < 0; }",
10725            "foo.cpp",
10726            |metric| {
10727                // == 1, > 1, < 1; the walker on && and || finds
10728                // BinaryExpression operands (not terminal-bool) and
10729                // adds nothing. Total: 3.
10730                assert_eq!(metric.abc.conditions_sum(), 3);
10731                insta::assert_json_snapshot!(metric.abc);
10732            },
10733        );
10734    }
10735
10736    #[cfg(feature = "cpp")]
10737    #[test]
10738    fn cpp_generic_brackets_not_conditions() {
10739        // `<` / `>` in `std::vector<int>` are `template_argument_list`
10740        // delimiters, NOT comparison operators. The `binary_expression`
10741        // parent check must filter them out → C = 0.
10742        check_metrics::<CppParser>(
10743            "#include <vector>\nstd::vector<int> f() { return std::vector<int>{}; }",
10744            "foo.cpp",
10745            |metric| {
10746                assert_eq!(metric.abc.conditions_sum(), 0);
10747                insta::assert_json_snapshot!(metric.abc);
10748            },
10749        );
10750    }
10751
10752    #[cfg(feature = "cpp")]
10753    #[test]
10754    fn cpp_else_and_ternary_count_conditions() {
10755        // `if (cond) ... else ...` + ternary `cond ? a : b`. The
10756        // `if`-keyword is NOT a condition (its condition is the
10757        // comparison inside, which counts separately). `else` adds 1,
10758        // `?` adds 1. Two comparisons (`a > b`, `b < 0`) → 2. Total = 4.
10759        check_metrics::<CppParser>(
10760            "int f(int a, int b) {\n\
10761                 if (a > b) { return a; } else { return b; }\n\
10762                 return (b < 0) ? -b : b;\n\
10763             }\n",
10764            "foo.cpp",
10765            |metric| {
10766                assert_eq!(metric.abc.conditions_sum(), 4);
10767                insta::assert_json_snapshot!(metric.abc);
10768            },
10769        );
10770    }
10771
10772    // Issue #1102. A ternary's condition and both branch operands are
10773    // Fitzpatrick Rule 9 unary conditions, exactly as `java_walk_ternary`
10774    // has always counted them. Before the fix the C family scored
10775    // `a ? !b : !c` as 1 — the `?` token alone — against Java's 4.
10776    #[cfg(feature = "cpp")]
10777    #[test]
10778    fn cpp_ternary_operand_slots_count_as_unary_conditions() {
10779        // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4.
10780        check_metrics::<CppParser>("void f() { x = a ? !b : !c; }", "foo.cpp", |metric| {
10781            assert_eq!(metric.abc.conditions_sum(), 4);
10782        });
10783        // No-double-count pin: `?` (1) + `>` (1) = 2, unchanged by the
10784        // fix. The parenthesised condition unwraps to a
10785        // `binary_expression`, which is not a boolean terminal, and
10786        // neither branch is negated — the `!` is the type-free proxy for
10787        // "this operand is boolean", so an unnegated branch contributes
10788        // nothing.
10789        check_metrics::<CppParser>("void f() { x = (a > 0) ? b : -b; }", "foo.cpp", |metric| {
10790            assert_eq!(metric.abc.conditions_sum(), 2);
10791        });
10792        // Nested: outer `?` (1) + outer condition `a` (1) + inner `?`
10793        // (1) + inner condition `b` (1) = 4. The outer consequence is
10794        // the inner ternary — neither a boolean terminal nor a
10795        // paren / `!` wrapper — so it adds nothing on its own and the
10796        // inner one is reached by the walk, not by descent.
10797        check_metrics::<CppParser>("void f() { x = a ? b ? c : d : e; }", "foo.cpp", |metric| {
10798            assert_eq!(metric.abc.conditions_sum(), 4);
10799        });
10800        // A negated *condition* is the only input that reaches the
10801        // walker's `else` fallback: `!a` is neither a boolean terminal
10802        // (so the terminal arm skips it) nor an operand slot (so
10803        // `cpp_inspect_container` is never called on it from anywhere
10804        // else). Every other condition fixture in this file wraps a
10805        // comparison, which the fallback resolves to 0 — delete the
10806        // fallback and only this case moves. `?` (1) + `!a` (1) = 2.
10807        check_metrics::<CppParser>("void f() { x = !a ? b : c; }", "foo.cpp", |metric| {
10808            assert_eq!(metric.abc.conditions_sum(), 2);
10809        });
10810    }
10811
10812    // The GNU short-ternary `a ?: b` elides the consequence, so the
10813    // C-family grammar marks that field optional and the alternative
10814    // lands at child(3) rather than child(4). Addressing the operand
10815    // slots by grammar field name — never by index — is what keeps `!b`
10816    // counted here; a fixed `child(4)` reads `None` and scores 2.
10817    #[cfg(feature = "cpp")]
10818    #[test]
10819    fn cpp_elided_ternary_consequence_still_walks_the_alternative() {
10820        // `?` (1) + condition `a` (1) + `!b` (1) = 3.
10821        check_metrics::<CppParser>("void f() { x = a ?: !b; }", "foo.cpp", |metric| {
10822            assert_eq!(metric.abc.conditions_sum(), 3);
10823        });
10824    }
10825
10826    // `cpp_walk_ternary` is shared by the C, ObjC, and Mozcpp ABC impls
10827    // exactly as `cpp_inspect_container` is, so each needs its own
10828    // dispatcher arm. Mozcpp owns no file extension and so gets no
10829    // integration-snapshot coverage at all — this parity assertion is
10830    // its only guard.
10831    //
10832    // The expected value is *derived from the C++ run*, not hardcoded,
10833    // so the four languages cannot silently drift apart if the C++
10834    // expectation ever legitimately moves.
10835    #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
10836    #[test]
10837    fn c_family_ternary_operand_slots_agree_with_cpp() {
10838        #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
10839        const SRC: &str = "void f() { x = a ? !b : !c; }\n";
10840        let conditions = abc_conditions;
10841
10842        let cpp = conditions(LANG::Cpp, SRC);
10843        // Non-degenerate: a zeroed reference would make every
10844        // comparison below vacuous.
10845        assert_eq!(cpp, 4, "C++ reference value for `a ? !b : !c`");
10846
10847        assert_eq!(conditions(LANG::C, SRC), cpp, "C must match C++");
10848        assert_eq!(conditions(LANG::Mozcpp, SRC), cpp, "Mozcpp must match C++");
10849        assert_eq!(
10850            conditions(
10851                LANG::Objc,
10852                "@implementation Foo\n\
10853                 - (void)bar {\n\
10854                     x = a ? !b : !c;\n\
10855                 }\n\
10856                 @end\n",
10857            ),
10858            cpp,
10859            "ObjC must match C++"
10860        );
10861    }
10862
10863    // Issue #1276, C-family half. `cpp_walk_for_statement` is the
10864    // `for` header's counterpart to the `if` / `while` arms: the slot
10865    // is a bare expression rather than a `condition_clause`, so it
10866    // needs the top-level terminal check `cpp_walk_ternary` already
10867    // had. Every fixture is a shape only that walker can classify —
10868    // a comparison-shaped condition proves nothing, the `<` token arm
10869    // counts it either way (grammar-dispatch §11).
10870    #[cfg(feature = "cpp")]
10871    #[test]
10872    fn cpp_for_condition_slot_counts_unary_conditions() {
10873        // Bare identifier: the whole condition, no operator token.
10874        check_metrics::<CppParser>("void f(int a) { for (; a; ) {} }", "foo.cpp", |metric| {
10875            assert_eq!(metric.abc.conditions_sum(), 1);
10876        });
10877        // Negation: reaches the terminal through
10878        // `cpp_inspect_container`'s `!` unwrap.
10879        check_metrics::<CppParser>("void f(int a) { for (; !a; ) {} }", "foo.cpp", |metric| {
10880            assert_eq!(metric.abc.conditions_sum(), 1);
10881        });
10882        // Parentheses: counts only because the `for_statement` parent
10883        // seeds `has_boolean_content` — the seed #1276 found dead.
10884        check_metrics::<CppParser>("void f(int a) { for (; (a); ) {} }", "foo.cpp", |metric| {
10885            assert_eq!(metric.abc.conditions_sum(), 1);
10886        });
10887        // No-double-count pin: the `<` token arm already counted this
10888        // shape before the fix and the walker must not add a second.
10889        // The two assignments confirm the header parsed as the
10890        // three-clause form rather than degenerating.
10891        check_metrics::<CppParser>(
10892            "void f(int n) { for (int i = 0; i < n; i++) {} }",
10893            "foo.cpp",
10894            |metric| {
10895                assert_eq!(metric.abc.conditions_sum(), 1);
10896                assert_eq!(metric.abc.assignments_sum(), 2);
10897            },
10898        );
10899        // Empty condition: no `condition` field, no decision, zero.
10900        check_metrics::<CppParser>("void f() { for (;;) { break; } }", "foo.cpp", |metric| {
10901            assert_eq!(metric.abc.conditions_sum(), 0);
10902        });
10903    }
10904
10905    // `cpp_walk_for_statement` is shared by the C, ObjC and Mozcpp ABC
10906    // impls the way `cpp_walk_ternary` is, so each needs its own
10907    // dispatcher arm — and Mozcpp, which owns no file extension, has no
10908    // integration-snapshot coverage at all, making this its only guard.
10909    // The expected value is derived from the C++ run rather than
10910    // hardcoded, so the four cannot silently drift apart.
10911    #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
10912    #[test]
10913    fn c_family_for_condition_slot_agrees_with_cpp() {
10914        #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
10915        const SRC: &str = "void f(int a) { for (; !a; ) {} }\n";
10916        let conditions = abc_conditions;
10917
10918        let cpp = conditions(LANG::Cpp, SRC);
10919        // Non-degenerate: a zeroed reference makes every comparison
10920        // below vacuous.
10921        assert_eq!(cpp, 1, "C++ reference value for `for (; !a; )`");
10922
10923        assert_eq!(conditions(LANG::C, SRC), cpp, "C must match C++");
10924        assert_eq!(conditions(LANG::Mozcpp, SRC), cpp, "Mozcpp must match C++");
10925        assert_eq!(
10926            conditions(
10927                LANG::Objc,
10928                "@implementation Foo\n\
10929                 - (void)bar {\n\
10930                     for (; !a; ) {}\n\
10931                 }\n\
10932                 @end\n",
10933            ),
10934            cpp,
10935            "ObjC must match C++"
10936        );
10937    }
10938
10939    #[cfg(feature = "cpp")]
10940    #[test]
10941    fn cpp_switch_cases_count_default_excluded() {
10942        // `case 1`, `case 2` → 2 conditions. `default` is intentionally
10943        // excluded (the unconditional fallthrough, mirroring cyclomatic's
10944        // `Case`-only count). Since #469 every C-family language —
10945        // Java, C#, Groovy, JS, TS — agrees on this; C++ already did.
10946        // C = 2.
10947        check_metrics::<CppParser>(
10948            "void f(int x) {\n\
10949                 switch (x) {\n\
10950                     case 1: break;\n\
10951                     case 2: break;\n\
10952                     default: break;\n\
10953                 }\n\
10954             }\n",
10955            "foo.cpp",
10956            |metric| {
10957                assert_eq!(metric.abc.conditions_sum(), 2);
10958                insta::assert_json_snapshot!(metric.abc);
10959            },
10960        );
10961    }
10962
10963    #[cfg(feature = "cpp")]
10964    #[test]
10965    fn cpp_try_catch_count_conditions() {
10966        // `try` and `catch` each add one condition (Fitzpatrick's rule;
10967        // Java's impl above counts them too).
10968        check_metrics::<CppParser>(
10969            "void f() { try { } catch (int) { } catch (...) { } }",
10970            "foo.cpp",
10971            |metric| {
10972                // 1 `try` + 2 `catch` arms = 3.
10973                assert_eq!(metric.abc.conditions_sum(), 3);
10974                insta::assert_json_snapshot!(metric.abc);
10975            },
10976        );
10977    }
10978
10979    #[cfg(feature = "cpp")]
10980    #[test]
10981    fn cpp_complex_function_abc() {
10982        // Mixed-shape regression: assignments, calls, conditions,
10983        // ternary, switch, new. Verified by hand:
10984        // - assignments: `int x = 0` (init_declarator with `=`),
10985        //   `x = 5`, `x += 2`, `x++`, `x = (a > b) ? a : b`, `x = b`,
10986        //   `auto* p = new int(5)` (init_declarator with `=`) → A = 7
10987        //   (post-#393: every `=` in an init_declarator counts).
10988        // - branches: `f(a, b)` self-call + `new int(5)` → B = 2.
10989        // - conditions: `a == b` (1) + `a > 0` (1) inside the if;
10990        //   `&&` itself is NOT a condition (Fitzpatrick Rule 5,
10991        //   issue #395). `a > b` (1) + `?` (1) in the ternary.
10992        //   `else` (1, from the `else if` keyword) + `a < b` (1)
10993        //   in the else-if. `!x` contributes 1 via the unary-
10994        //   conditional walker (Fitzpatrick Rule 9, issue #403):
10995        //   the `||` walker treats `!x` as a unary boolean operand
10996        //   and counts the wrapped Identifier once. `case 1`,
10997        //   `case 2` → 2. `default` excluded. Total C = 9.
10998        check_metrics::<CppParser>(
10999            "int f(int a, int b) {\n\
11000                 int x = 0;\n\
11001                 x = 5;\n\
11002                 x += 2;\n\
11003                 x++;\n\
11004                 if (a == b && a > 0) {\n\
11005                     x = (a > b) ? a : b;\n\
11006                 } else if (a < b || !x) {\n\
11007                     x = b;\n\
11008                 }\n\
11009                 switch (x) {\n\
11010                     case 1: break;\n\
11011                     case 2: break;\n\
11012                     default: break;\n\
11013                 }\n\
11014                 auto* p = new int(5);\n\
11015                 return f(a, b);\n\
11016             }\n",
11017            "foo.cpp",
11018            |metric| {
11019                assert_eq!(metric.abc.assignments_sum(), 7);
11020                assert_eq!(metric.abc.branches_sum(), 2);
11021                assert_eq!(metric.abc.conditions_sum(), 9);
11022                insta::assert_json_snapshot!(metric.abc);
11023            },
11024        );
11025    }
11026
11027    #[cfg(feature = "cpp")]
11028    #[test]
11029    fn cpp_if_multiple_conditions() {
11030        // Fitzpatrick Rule 9 walker (issue #403): each operand of a
11031        // `&&` / `||` chain is one condition.
11032        check_metrics::<CppParser>(
11033            "void f(bool a, bool b, bool c, bool d) {\n\
11034             \x20   if (a || b || c || d) {}        // +4c\n\
11035             \x20   if (a && b && c) {}             // +3c\n\
11036             \x20   if (!a && !b) {}                // +2c\n\
11037             }\n",
11038            "foo.cpp",
11039            |metric| {
11040                assert_eq!(metric.abc.conditions_sum(), 9);
11041                insta::assert_json_snapshot!(metric.abc);
11042            },
11043        );
11044    }
11045
11046    #[cfg(feature = "cpp")]
11047    #[test]
11048    fn cpp_while_and_do_while_conditions() {
11049        // Exercise both the WhileStatement and DoStatement arms via
11050        // the walker on the `&&` / `||` tokens inside their parens.
11051        check_metrics::<CppParser>(
11052            "void f(bool a, bool b) {\n\
11053             \x20   while (a || b) {}              // +2c\n\
11054             \x20   do {} while (a && !b);         // +2c\n\
11055             }\n",
11056            "foo.cpp",
11057            |metric| {
11058                assert_eq!(metric.abc.conditions_sum(), 4);
11059                insta::assert_json_snapshot!(metric.abc);
11060            },
11061        );
11062    }
11063
11064    #[cfg(feature = "cpp")]
11065    #[test]
11066    fn cpp_if_constexpr_condition_counts() {
11067        // Regression for the code-review finding: C++ `if constexpr
11068        // (cond)` puts the `constexpr` keyword at child(1) and the
11069        // condition_clause at child(2). Pre-fix, the dispatcher used
11070        // child(1) and counted zero conditions for the `constexpr`
11071        // form. The fix uses `child_by_field_name("condition")`
11072        // which returns the condition_clause regardless of the
11073        // optional `constexpr` keyword.
11074        check_metrics::<CppParser>(
11075            "template <int N> void f() {\n\
11076             \x20   if constexpr (true) { }      // +1c\n\
11077             \x20   if (false) { }               // +1c\n\
11078             }\n",
11079            "foo.cpp",
11080            |metric| {
11081                assert_eq!(metric.abc.conditions_sum(), 2);
11082                insta::assert_json_snapshot!(metric.abc);
11083            },
11084        );
11085    }
11086
11087    #[cfg(feature = "cpp")]
11088    #[test]
11089    fn cpp_cast_expression_in_logical_chain_counts() {
11090        // Regression for findings.md round-2 #1 (C++):
11091        // `if ((bool)ptr && ready) {}` had the `||` walker missing
11092        // the `(bool)ptr` operand because `CastExpression` was not
11093        // in `cpp_bool_terminal_kinds!()`. Mirrors C#'s
11094        // `csharp_bool_terminal_kinds!()` which lists
11095        // `CastExpression` (lesson 19, #372).
11096        check_metrics::<CppParser>(
11097            "void f(void* ptr, bool ready) { if ((bool)ptr && ready) { } }\n",
11098            "foo.cpp",
11099            |metric| {
11100                // `&&` walker counts both operands: `(bool)ptr` (1)
11101                // and `ready` (1). Total: 2.
11102                assert_eq!(metric.abc.conditions_sum(), 2);
11103                insta::assert_json_snapshot!(metric.abc);
11104            },
11105        );
11106    }
11107
11108    #[cfg(feature = "cpp")]
11109    #[test]
11110    fn cpp_qualified_identifier_condition_counts() {
11111        // Regression for findings.md #3 (C++): tree-sitter-cpp emits
11112        // `qualified_identifier` under four kind_ids (573..576) per
11113        // the production-rule path; runtime kind for `ns::flag` is
11114        // 574 (`QualifiedIdentifier2`). Pre-fix the
11115        // `cpp_bool_terminal_kinds!()` macro listed neither the
11116        // primary nor any alias, so `if (n::flag) {}` reported zero
11117        // conditions. The macro now includes all four variants
11118        // (lesson #2).
11119        check_metrics::<CppParser>(
11120            "namespace n { extern bool flag; }\n\
11121             void f() { if (n::flag) { } }\n",
11122            "foo.cpp",
11123            |metric| {
11124                assert_eq!(metric.abc.conditions_sum(), 1);
11125                insta::assert_json_snapshot!(metric.abc);
11126            },
11127        );
11128    }
11129
11130    #[cfg(feature = "cpp")]
11131    #[test]
11132    fn cpp_if_boolean_literal_condition() {
11133        check_metrics::<CppParser>(
11134            "void f() {\n\
11135             \x20   if (true) {}                 // +1c\n\
11136             \x20   if (!false) {}               // +1c\n\
11137             \x20   while (true) {}              // +1c\n\
11138             \x20   do {} while (false);         // +1c\n\
11139             }\n",
11140            "foo.cpp",
11141            |metric| {
11142                assert_eq!(metric.abc.conditions_sum(), 4);
11143                insta::assert_json_snapshot!(metric.abc);
11144            },
11145        );
11146    }
11147
11148    #[cfg(feature = "cpp")]
11149    #[test]
11150    fn cpp_methods_arguments_with_conditions() {
11151        check_metrics::<CppParser>(
11152            "void f(bool a, bool b) {\n\
11153             \x20   m(a, b);                     // +1b\n\
11154             \x20   m(!a, !b);                   // +1b +2c\n\
11155             }\n",
11156            "foo.cpp",
11157            |metric| {
11158                assert_eq!(metric.abc.branches_sum(), 2);
11159                assert_eq!(metric.abc.conditions_sum(), 2);
11160                insta::assert_json_snapshot!(metric.abc);
11161            },
11162        );
11163    }
11164
11165    #[cfg(feature = "cpp")]
11166    #[test]
11167    fn cpp_return_with_conditions() {
11168        check_metrics::<CppParser>(
11169            "bool m1(int z) { return !(z >= 0); }\n\
11170             bool m2(bool x) { return (((!x))); }\n\
11171             bool m3(bool x, bool y) { return x && y; }\n",
11172            "foo.cpp",
11173            |metric| {
11174                // m1: !(z >= 0) → `>=` (1). `!` wraps a paren'd
11175                //     BinaryExpression — inspect_container reaches
11176                //     the inner BinaryExpression and stops, no
11177                //     walker count. +1.
11178                // m2: (((!x))) → ReturnStatement → inspect_container
11179                //     unwraps three parens + one unary → reaches `x`
11180                //     in has_boolean_content=true (seeded by the
11181                //     unary `!`). +1.
11182                // m3: x && y → `&&` walker counts both → +2.
11183                // Sum: 1 + 1 + 2 = 4.
11184                assert_eq!(metric.abc.conditions_sum(), 4);
11185                insta::assert_json_snapshot!(metric.abc);
11186            },
11187        );
11188    }
11189
11190    #[cfg(feature = "cpp")]
11191    #[test]
11192    fn cpp_short_circuit_with_boolean_literal_operand() {
11193        // `a && true` reports 2 conditions: one for the identifier
11194        // operand, one for the `True` literal operand.
11195        check_metrics::<CppParser>(
11196            "bool f(bool a) { return a && true; }\n",
11197            "foo.cpp",
11198            |metric| {
11199                assert_eq!(metric.abc.conditions_sum(), 2);
11200                insta::assert_json_snapshot!(metric.abc);
11201            },
11202        );
11203    }
11204
11205    #[cfg(feature = "javascript")]
11206    #[test]
11207    fn javascript_empty_unit_zero() {
11208        // No code → A=B=C=0. Wires up the trait and exercises the
11209        // per-language compute reachability.
11210        check_metrics::<JavascriptParser>("", "empty.js", |metric| {
11211            assert_eq!(metric.abc.assignments_sum(), 0);
11212            assert_eq!(metric.abc.branches_sum(), 0);
11213            assert_eq!(metric.abc.conditions_sum(), 0);
11214            insta::assert_json_snapshot!(metric.abc);
11215        });
11216    }
11217
11218    #[cfg(feature = "javascript")]
11219    #[test]
11220    fn javascript_plain_and_compound_assignments_count() {
11221        // `let` / `var` declarations behave like TypeScript: only a
11222        // `const` initializer is suppressed. So `let x = 0` does count as
11223        // A=+1; only `const PI = 3.14` would be elided. Plain `x = 5`,
11224        // `x += 2`, `x = 7` all count → A = 4 total here.
11225        check_metrics::<JavascriptParser>(
11226            "function f() { let x = 0; x = 5; x += 2; x = 7; }",
11227            "foo.js",
11228            |metric| {
11229                assert_eq!(metric.abc.assignments_sum(), 4);
11230                assert_eq!(metric.abc.branches_sum(), 0);
11231                assert_eq!(metric.abc.conditions_sum(), 0);
11232                insta::assert_json_snapshot!(metric.abc);
11233            },
11234        );
11235    }
11236
11237    #[cfg(feature = "javascript")]
11238    #[test]
11239    fn javascript_const_initializer_not_assignment() {
11240        // `const PI = 3.14` must NOT count as an assignment — its `=`
11241        // initialises a `const` binding. `let x = 1` and `var y = 2`
11242        // still count (matches the TS impl: only `const` suppresses).
11243        check_metrics::<JavascriptParser>(
11244            "function f() { const PI = 3.14; let x = 1; var y = 2; x = 9; }",
11245            "foo.js",
11246            |metric| {
11247                // `const PI` suppressed; `let x = 1`, `var y = 2`,
11248                // `x = 9` all count → A = 3.
11249                assert_eq!(metric.abc.assignments_sum(), 3);
11250                insta::assert_json_snapshot!(metric.abc);
11251            },
11252        );
11253    }
11254
11255    #[cfg(feature = "javascript")]
11256    #[test]
11257    fn javascript_asi_const_does_not_suppress_later_assignments() {
11258        // The issue #1277 reproducer verbatim. JavaScript half of the
11259        // cluster documented at
11260        // `typescript_asi_const_does_not_suppress_later_assignments`.
11261        check_metrics::<JavascriptParser>(
11262            "function f() {
11263                const a = 1
11264                x = 2
11265                return x
11266            }",
11267            "foo.js",
11268            |metric| {
11269                // Pre-#1277 this reported 0; the semicolon-terminated
11270                // spelling reported 1.
11271                assert_eq!(metric.abc.assignments_sum(), 1);
11272            },
11273        );
11274    }
11275
11276    #[cfg(feature = "javascript")]
11277    #[test]
11278    fn javascript_nested_arrow_const_does_not_leak() {
11279        // The ASI leak beside a nested space: the arrow body opens its
11280        // own space, so `y = 1` was always counted there, but
11281        // `const f = () => …` has no `;`, so `h`'s sentinel stayed live
11282        // and suppressed `z = 2`. The stack was per-space state that
11283        // `Stats::merge` never carried, so this is the terminator defect
11284        // and not a second route.
11285        check_metrics::<JavascriptParser>(
11286            "function h() {
11287                const f = () => { y = 1 }
11288                z = 2
11289            }",
11290            "foo.js",
11291            |metric| {
11292                // `y = 1` and `z = 2`; the `const` initializer is
11293                // suppressed. Pre-#1277: 1.
11294                assert_eq!(metric.abc.assignments_sum(), 2);
11295            },
11296        );
11297    }
11298
11299    #[cfg(feature = "javascript")]
11300    #[test]
11301    fn javascript_non_declarator_equals_still_count() {
11302        // An `=` that does not belong to a `const` declarator is always
11303        // an assignment: a class `field_definition` initializer, a
11304        // default-parameter `assignment_pattern`, and a destructured
11305        // parameter's defaults, whose climb crosses the same pattern
11306        // layers as a `const` pattern's but ends at `formal_parameters`.
11307        // A predicate that accepted any pattern ancestor as a declarator
11308        // would zero the last three.
11309        check_metrics::<JavascriptParser>(
11310            "class K { f = 1; g(p = 2, {q = 3} = {}) { let d = 4 } }",
11311            "foo.js",
11312            |metric| {
11313                // `f = 1`, `p = 2`, `q = 3`, `= {}`, `let d = 4`.
11314                assert_eq!(metric.abc.assignments_sum(), 5);
11315            },
11316        );
11317    }
11318
11319    #[cfg(feature = "javascript")]
11320    #[test]
11321    fn javascript_const_initializer_value_assignments_still_count() {
11322        // An `=` inside a `const` initializer's *value* is an
11323        // `assignment_expression`: its climb reaches no pattern layer and
11324        // no declarator, so it counts. The pre-#1277 sentinel
11325        // blanket-suppressed every `=` between `const` and `;`, which is
11326        // the shape that moved the pdf.js corpus snapshots
11327        // (`const bbox = (this.data.rect = …)`).
11328        check_metrics::<JavascriptParser>(
11329            "function m(o, a, b) { const x = (o.p = 1); const y = a || (b = 2); }",
11330            "foo.js",
11331            |metric| {
11332                // `o.p = 1` and `b = 2`; the two `const` initializers are
11333                // suppressed. Pre-#1277: 0.
11334                assert_eq!(metric.abc.assignments_sum(), 2);
11335            },
11336        );
11337    }
11338
11339    #[cfg(feature = "javascript")]
11340    #[test]
11341    fn javascript_const_declarator_shapes_stay_suppressed() {
11342        // JavaScript half of
11343        // `typescript_const_declarator_shapes_stay_suppressed`.
11344        check_metrics::<JavascriptParser>(
11345            "function f(o, xs) {
11346                const a = 1, b = 2
11347                const {c = 5, d: {e = 6} = {}} = o
11348                const [g = 7, ...[h = 8]] = xs
11349                let i = 3
11350                var j = 4
11351                for (const x of xs) { k(x) }
11352            }",
11353            "foo.js",
11354            |metric| {
11355                assert_eq!(metric.abc.assignments_sum(), 2);
11356            },
11357        );
11358    }
11359
11360    #[cfg(feature = "javascript")]
11361    #[test]
11362    fn javascript_increment_and_decrement_count_as_assignment() {
11363        // `x++` (post) and `--x` (pre) both update an lvalue and so
11364        // count as assignments. Combined with the `let x = 0`
11365        // initializer (which counts under the JS/TS rule — only `const`
11366        // suppresses), A = 3.
11367        check_metrics::<JavascriptParser>(
11368            "function f() { let x = 0; x++; --x; }",
11369            "foo.js",
11370            |metric| {
11371                assert_eq!(metric.abc.assignments_sum(), 3);
11372                insta::assert_json_snapshot!(metric.abc);
11373            },
11374        );
11375    }
11376
11377    #[cfg(feature = "javascript")]
11378    #[test]
11379    fn javascript_calls_are_branches() {
11380        // `g(1)` is a `call_expression` → B = 1. `new Foo(2)` is a
11381        // `new_expression` → B = 1. Total B = 2.
11382        check_metrics::<JavascriptParser>(
11383            "function f() { g(1); new Foo(2); }",
11384            "foo.js",
11385            |metric| {
11386                assert_eq!(metric.abc.branches_sum(), 2);
11387                assert_eq!(metric.abc.conditions_sum(), 0);
11388                insta::assert_json_snapshot!(metric.abc);
11389            },
11390        );
11391    }
11392
11393    #[cfg(feature = "javascript")]
11394    #[test]
11395    fn javascript_comparisons_count_conditions() {
11396        // `==`, `===`, `!=`, `!==`, `<`, `>`, `<=`, `>=` each count
11397        // once. The `&&` / `||` short-circuit operators are NOT
11398        // counted as conditions in this impl (matches the TS
11399        // precedent — short-circuit ops are folded into the
11400        // surrounding `if` / control-flow arm, not separately).
11401        // Total C = 8.
11402        check_metrics::<JavascriptParser>(
11403            "function f(a, b) { return a == b && a === b && a != b && a !== b && a < b && a > b && a <= b && a >= b; }",
11404            "foo.js",
11405            |metric| {
11406                assert_eq!(metric.abc.conditions_sum(), 8);
11407                insta::assert_json_snapshot!(metric.abc);
11408            },
11409        );
11410    }
11411
11412    // #1297 in the `js_abc_compute!` expansion, where `LT` / `GT` had
11413    // no gate at all: plain JavaScript has no generics, but
11414    // tree-sitter-javascript parses JSX unconditionally, so the same
11415    // seven tag brackets that
11416    // `tsx_jsx_elements_are_not_conditions` covers scored seven
11417    // conditions in a `.js` file too. Same fixture minus the type
11418    // annotations, same discriminating numbers: 8 pre-fix, 1 after.
11419    #[cfg(feature = "javascript")]
11420    #[test]
11421    fn javascript_jsx_elements_are_not_conditions() {
11422        check_metrics::<JavascriptParser>(
11423            "function f(a, b) {
11424                if (a < b) { return <div className=\"x\"><span>hi</span></div>; }
11425                return <br />;
11426            }",
11427            "foo.js",
11428            |metric| {
11429                assert_eq!(metric.abc.conditions_sum(), 1);
11430                // Fixture-presence anchor: the `className="x"` attribute
11431                // `=` is this fixture's only assignment, so the test
11432                // fails if a later edit drops the JSX. Without it the
11433                // surviving `a < b` alone also scores 1, and the
11434                // conditions assertion would pass while proving nothing.
11435                assert_eq!(metric.abc.assignments_sum(), 1);
11436            },
11437        );
11438    }
11439
11440    #[cfg(feature = "javascript")]
11441    #[test]
11442    fn javascript_number_truthy_condition_counts() {
11443        // Regression for #772: JS treats every non-zero number as
11444        // truthy, so `while (5)` and `x && 5` should each count their
11445        // numeric literal as a Fitzpatrick unary condition. Pre-fix
11446        // `javascript_bool_terminal_kinds!()` listed `True` / `False`
11447        // but omitted `Number`, so the walker dropped every numeric-
11448        // truthy operand (mirrors the Lua `Number` fix).
11449        check_metrics::<JavascriptParser>(
11450            "function f(x) { while (5) {} return x && 5; }",
11451            "foo.js",
11452            |metric| {
11453                // `while (5)` → Number literal (+1). `x && 5` → both
11454                // operands count: identifier `x` (+1), Number `5` (+1).
11455                // Total: 3.
11456                assert_eq!(metric.abc.conditions_sum(), 3);
11457                insta::assert_json_snapshot!(metric.abc);
11458            },
11459        );
11460    }
11461
11462    #[cfg(feature = "typescript")]
11463    #[test]
11464    fn typescript_number_truthy_condition_counts() {
11465        // Regression for #772: TS shares the JS truthy semantics. The
11466        // numeric *literal* `5` (kind `Number`) counts; the type-keyword
11467        // `number` (kind `Number2`, the `predefined_type`) must not —
11468        // see `typescript_bool_terminal_kinds!`.
11469        check_metrics::<TypescriptParser>(
11470            "function f(x: number) { while (5) {} return x && 5; }",
11471            "foo.ts",
11472            |metric| {
11473                // `while (5)` → +1; `x && 5` → `x` (+1) + `5` (+1).
11474                // Total: 3. The `: number` annotation contributes 0.
11475                assert_eq!(metric.abc.conditions_sum(), 3);
11476                insta::assert_json_snapshot!(metric.abc);
11477            },
11478        );
11479    }
11480
11481    #[cfg(feature = "javascript")]
11482    #[test]
11483    fn javascript_nullish_coalescing_counts_condition() {
11484        // `a ?? b` is one nullish-coalescing operator → C = 1.
11485        check_metrics::<JavascriptParser>(
11486            "function f(a, b) { return a ?? b; }",
11487            "foo.js",
11488            |metric| {
11489                assert_eq!(metric.abc.conditions_sum(), 1);
11490                insta::assert_json_snapshot!(metric.abc);
11491            },
11492        );
11493    }
11494
11495    #[cfg(feature = "javascript")]
11496    #[test]
11497    fn javascript_else_ternary_case_default_try_catch() {
11498        // `else`, `?` (ternary), `case`, `try`, `catch` all count.
11499        // `default` is the unconditional fallthrough → +0 (#469).
11500        // With the comparisons:
11501        //   - `a > 0` → 1
11502        //   - `else` opens an else_clause → 1
11503        //   - `?` ternary → 1
11504        //   - the ternary's bare-identifier condition `a` → 1 (#1102)
11505        //   - `case 1` → 1
11506        //   - `default` → 0 (fallthrough, #469)
11507        //   - `try` + `catch` → 2
11508        // Total C = 7.
11509        check_metrics::<JavascriptParser>(
11510            "function f(a) { if (a > 0) {} else {} let x = a ? 1 : 2; switch (x) { case 1: break; default: break; } try { } catch (e) { } }",
11511            "foo.js",
11512            |metric| {
11513                assert_eq!(metric.abc.conditions_sum(), 7);
11514                insta::assert_json_snapshot!(metric.abc);
11515            },
11516        );
11517    }
11518
11519    // Issue #1102, JS-family half. See
11520    // `cpp_ternary_operand_slots_count_as_unary_conditions` for the
11521    // rule; the two families were behind Java by the same three units.
11522    #[cfg(feature = "javascript")]
11523    #[test]
11524    fn javascript_ternary_operand_slots_count_as_unary_conditions() {
11525        // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4.
11526        check_metrics::<JavascriptParser>(
11527            "function f() { x = a ? !b : !c; }",
11528            "foo.js",
11529            |metric| assert_eq!(metric.abc.conditions_sum(), 4),
11530        );
11531        // No-double-count pin: `?` (1) + `>` (1) = 2, unchanged by the
11532        // fix — the parenthesised condition unwraps to a
11533        // `binary_expression` (not a boolean terminal) and neither
11534        // branch is negated.
11535        check_metrics::<JavascriptParser>(
11536            "function f() { x = (a > 0) ? b : -b; }",
11537            "foo.js",
11538            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
11539        );
11540        // Nested: two `?` tokens plus the two bare-identifier
11541        // conditions = 4.
11542        check_metrics::<JavascriptParser>(
11543            "function f() { x = a ? b ? c : d : e; }",
11544            "foo.js",
11545            |metric| assert_eq!(metric.abc.conditions_sum(), 4),
11546        );
11547        // A negated condition is the only input reaching the walker's
11548        // `else` fallback — see the C++ sibling for why. `?` (1) +
11549        // `!a` (1) = 2.
11550        check_metrics::<JavascriptParser>("function f() { x = !a ? b : c; }", "foo.js", |metric| {
11551            assert_eq!(metric.abc.conditions_sum(), 2);
11552        });
11553    }
11554
11555    // TypeScript expands the same `ts_abc_compute!` arm from a separate
11556    // macro body than JavaScript's `js_abc_compute!`, so wiring one and
11557    // not the other is a live failure mode; TSX and Mozjs are clones of
11558    // these two.
11559    #[cfg(feature = "typescript")]
11560    #[test]
11561    fn typescript_ternary_operand_slots_count_as_unary_conditions() {
11562        check_metrics::<TypescriptParser>(
11563            "function f() { x = a ? !b : !c; }",
11564            "foo.ts",
11565            |metric| assert_eq!(metric.abc.conditions_sum(), 4),
11566        );
11567        check_metrics::<TypescriptParser>(
11568            "function f() { x = (a > 0) ? b : -b; }",
11569            "foo.ts",
11570            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
11571        );
11572    }
11573
11574    // Issue #1276, JS-family half. See
11575    // `cpp_for_condition_slot_counts_unary_conditions` for the rule.
11576    // The JS grammar marks the `condition` field on both the expression
11577    // and the `;` closing it, so `child_by_field_name` is the only
11578    // addressing that lands on the expression for every header shape.
11579    #[cfg(feature = "javascript")]
11580    #[test]
11581    fn javascript_for_condition_slot_counts_unary_conditions() {
11582        // Bare identifier: no operator token anywhere in the header.
11583        check_metrics::<JavascriptParser>("function f(a) { for (; a; ) {} }", "foo.js", |metric| {
11584            assert_eq!(metric.abc.conditions_sum(), 1);
11585        });
11586        // Negation, through the `!` unwrap.
11587        check_metrics::<JavascriptParser>(
11588            "function f(a) { for (; !a; ) {} }",
11589            "foo.js",
11590            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
11591        );
11592        // Parentheses: counts only via the `ForStatement`
11593        // boolean-context seed #1276 found dead.
11594        check_metrics::<JavascriptParser>(
11595            "function f(a) { for (; (a); ) {} }",
11596            "foo.js",
11597            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
11598        );
11599        // No-double-count pin: the `<` arm already counted this shape.
11600        // `let i = 0` and `i++` are the two assignments, which also
11601        // confirms the header parsed as the three-clause form.
11602        check_metrics::<JavascriptParser>(
11603            "function f(n) { for (let i = 0; i < n; i++) {} }",
11604            "foo.js",
11605            |metric| {
11606                assert_eq!(metric.abc.conditions_sum(), 1);
11607                assert_eq!(metric.abc.assignments_sum(), 2);
11608            },
11609        );
11610        // Empty condition: the slot holds an `empty_statement`, which
11611        // is neither a terminal nor a wrapper, so it counts nothing.
11612        check_metrics::<JavascriptParser>(
11613            "function f() { for (;;) { break; } }",
11614            "foo.js",
11615            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
11616        );
11617    }
11618
11619    // TypeScript expands the `ForStatement` arm from `ts_abc_compute!`,
11620    // a separate macro body from JavaScript's `js_abc_compute!`, so
11621    // wiring one and not the other is a live failure mode; TSX and
11622    // Mozjs are the clones of those two. The expected values are
11623    // derived from the JavaScript run rather than hardcoded.
11624    #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
11625    #[test]
11626    fn js_family_for_condition_slot_agrees_with_javascript() {
11627        #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
11628        const BARE: &str = "function f(a) { for (; a; ) {} }\n";
11629        #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
11630        const EMPTY: &str = "function f() { for (;;) { break; } }\n";
11631        let conditions = abc_conditions;
11632
11633        let bare = conditions(LANG::Javascript, BARE);
11634        // Non-degenerate: a zeroed reference makes the comparisons
11635        // below vacuous.
11636        assert_eq!(bare, 1, "JavaScript reference value for `for (; a; )`");
11637        assert_eq!(
11638            conditions(LANG::Javascript, EMPTY),
11639            0,
11640            "JavaScript `for (;;)`"
11641        );
11642
11643        for lang in [LANG::Mozjs, LANG::Typescript, LANG::Tsx] {
11644            assert_eq!(conditions(lang, BARE), bare, "{lang:?} bare for-condition");
11645            assert_eq!(conditions(lang, EMPTY), 0, "{lang:?} empty for-condition");
11646        }
11647    }
11648
11649    #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
11650    #[test]
11651    fn js_family_ternary_operand_slots_agree_with_javascript() {
11652        // `a ? !b : !c` is the one shape that tells the ternary walker
11653        // from the `for`-header walker: both read the `condition` field
11654        // and share the `(&Node, &mut f64)` signature, so a transposed
11655        // pair in a `ts_abc_compute!` / `js_abc_compute!` invocation
11656        // compiles, passes every condition-slot test, and drops only the
11657        // two branch operands. TypeScript alone pinned those before; the
11658        // other three expansions now do too.
11659        #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
11660        const SRC: &str = "function f(a, b, c) { x = a ? !b : !c; }\n";
11661        let javascript = abc_conditions(LANG::Javascript, SRC);
11662        // expected: 4 — the `?`, the `a` condition slot and both negated
11663        // branch operands; non-degenerate by construction.
11664        assert_eq!(
11665            javascript, 4,
11666            "JavaScript reference value for `a ? !b : !c`"
11667        );
11668        for lang in [LANG::Mozjs, LANG::Typescript, LANG::Tsx] {
11669            assert_eq!(
11670                abc_conditions(lang, SRC),
11671                javascript,
11672                "{lang:?} ternary operand slots"
11673            );
11674        }
11675    }
11676
11677    #[cfg(feature = "javascript")]
11678    #[test]
11679    fn javascript_instanceof_counts_condition() {
11680        // `x instanceof Foo` is a binary expression whose operator is
11681        // the `instanceof` keyword token → C = 1.
11682        check_metrics::<JavascriptParser>(
11683            "function f(x) { return x instanceof Foo; }",
11684            "foo.js",
11685            |metric| {
11686                assert_eq!(metric.abc.conditions_sum(), 1);
11687                insta::assert_json_snapshot!(metric.abc);
11688            },
11689        );
11690    }
11691
11692    #[cfg(feature = "javascript")]
11693    #[test]
11694    fn javascript_complex_function_abc() {
11695        // Mixed-shape regression. Verified by hand:
11696        // - assignments: `let x = 0` (a `let` initializer counts)
11697        //   + `x = 5`, `x += 2`, `x++`, `x = (a>b)?a:b`, `x = b`,
11698        //   `let p = ...` (likewise) → A = 7.
11699        // - branches: `f(a, b)` self-call + `new Bar()` → B = 2.
11700        // - conditions: `a == b`, `a > 0` → 2 inside the if header
11701        //   (`&&` is not counted directly). `else` (1) + `a > b`,
11702        //   `?` → 2 in the ternary. `a < b` → 1 in the else-if.
11703        //   `!x` → 1 from the Fitzpatrick Rule 9 walker on `||`
11704        //   (issue #403): the wrapped Identifier counts once.
11705        //   `case 1` → 1 in the switch; `default` → 0 (fallthrough,
11706        //   #469). Total C = 8.
11707        check_metrics::<JavascriptParser>(
11708            "function f(a, b) {\n\
11709                 let x = 0;\n\
11710                 x = 5;\n\
11711                 x += 2;\n\
11712                 x++;\n\
11713                 if (a == b && a > 0) {\n\
11714                     x = (a > b) ? a : b;\n\
11715                 } else if (a < b || !x) {\n\
11716                     x = b;\n\
11717                 }\n\
11718                 switch (x) {\n\
11719                     case 1: break;\n\
11720                     default: break;\n\
11721                 }\n\
11722                 let p = new Bar();\n\
11723                 return f(a, b);\n\
11724             }\n",
11725            "foo.js",
11726            |metric| {
11727                assert_eq!(metric.abc.assignments_sum(), 7);
11728                assert_eq!(metric.abc.branches_sum(), 2);
11729                assert_eq!(metric.abc.conditions_sum(), 8);
11730                insta::assert_json_snapshot!(metric.abc);
11731            },
11732        );
11733    }
11734
11735    #[cfg(feature = "mozjs")]
11736    #[test]
11737    fn mozjs_asi_const_does_not_suppress_later_assignments() {
11738        // Mozjs half of the #1277 cluster; the fork carries its own
11739        // kind-id numbering, so it needs its own fixture. See
11740        // `typescript_asi_const_does_not_suppress_later_assignments`.
11741        check_metrics::<MozjsParser>(
11742            "function f() {
11743                const a = 1
11744                x = 2
11745                y = 3
11746            }",
11747            "foo.jsm",
11748            |metric| {
11749                assert_eq!(metric.abc.assignments_sum(), 2);
11750            },
11751        );
11752    }
11753
11754    #[cfg(feature = "mozjs")]
11755    #[test]
11756    fn mozjs_const_declarator_shapes_stay_suppressed() {
11757        // Mozjs half of
11758        // `javascript_const_declarator_shapes_stay_suppressed`.
11759        check_metrics::<MozjsParser>(
11760            "function f(o, xs) {
11761                const a = 1, b = 2
11762                const {c = 5, d: {e = 6} = {}} = o
11763                const [g = 7, ...[h = 8]] = xs
11764                let i = 3
11765                var j = 4
11766                for (const x of xs) { k(x) }
11767            }",
11768            "foo.jsm",
11769            |metric| {
11770                assert_eq!(metric.abc.assignments_sum(), 2);
11771            },
11772        );
11773    }
11774
11775    #[cfg(feature = "mozjs")]
11776    #[test]
11777    fn mozjs_complex_function_abc() {
11778        // Mozjs shares JavaScript's expression / statement vocabulary;
11779        // the `js_abc_compute!` macro expands identical token-level
11780        // rules for both. This test pins parity against the JS impl.
11781        check_metrics::<MozjsParser>(
11782            "function f(a, b) {\n\
11783                 let x = 0;\n\
11784                 x = 5;\n\
11785                 x += 2;\n\
11786                 x++;\n\
11787                 if (a == b && a > 0) {\n\
11788                     x = (a > b) ? a : b;\n\
11789                 } else if (a < b || !x) {\n\
11790                     x = b;\n\
11791                 }\n\
11792                 switch (x) {\n\
11793                     case 1: break;\n\
11794                     default: break;\n\
11795                 }\n\
11796                 let p = new Bar();\n\
11797                 return f(a, b);\n\
11798             }\n",
11799            "foo.js",
11800            |metric| {
11801                assert_eq!(metric.abc.assignments_sum(), 7);
11802                assert_eq!(metric.abc.branches_sum(), 2);
11803                assert_eq!(metric.abc.conditions_sum(), 8);
11804                insta::assert_json_snapshot!(metric.abc);
11805            },
11806        );
11807    }
11808
11809    // ----- JS / TS / Tsx / Mozjs Phase-2B condition slots -----
11810
11811    #[cfg(feature = "javascript")]
11812    #[test]
11813    fn javascript_await_expression_condition_counts() {
11814        // Regression for findings.md round-2 #2 (JS):
11815        // `if (await ready()) {}` parses with `await_expression` as
11816        // the condition node inside the `parenthesized_expression`.
11817        // `javascript_inspect_container` unwraps the paren but the
11818        // await child was not in the terminal-bool set, so the
11819        // walker broke without counting. Mirrors C# (lesson 19).
11820        check_metrics::<JavascriptParser>(
11821            "async function ready() { return true; }\n\
11822             async function f() { if (await ready()) { } }\n",
11823            "foo.js",
11824            |metric| {
11825                assert_eq!(metric.abc.branches_sum(), 1);
11826                assert_eq!(metric.abc.conditions_sum(), 1);
11827                insta::assert_json_snapshot!(metric.abc);
11828            },
11829        );
11830    }
11831
11832    #[cfg(feature = "javascript")]
11833    #[test]
11834    fn javascript_member_expression_condition_counts() {
11835        // Regression for findings.md #3 (JS-family): tree-sitter-
11836        // javascript emits `member_expression` under three kind_ids
11837        // (191 primary, 208, 228 — `MemberExpression2/3`) depending
11838        // on the production rule path. The verifier in this audit
11839        // confirmed runtime kind for `o.x` is 208. Pre-fix the
11840        // shared `js_family_bool_terminal_kinds!()` macro listed
11841        // only the primary, so every `if (o.x) {}` / `o.x && o.y`
11842        // condition silently reported zero. The per-language macro
11843        // now includes all three aliases (lesson #2).
11844        check_metrics::<JavascriptParser>(
11845            "function f(o) {\n\
11846             \x20   if (o.x) {}                  // +1c\n\
11847             \x20   return o.x && o.y;           // +2c (walker on &&)\n\
11848             }\n",
11849            "foo.js",
11850            |metric| {
11851                assert_eq!(metric.abc.conditions_sum(), 3);
11852                insta::assert_json_snapshot!(metric.abc);
11853            },
11854        );
11855    }
11856
11857    #[cfg(feature = "javascript")]
11858    #[test]
11859    fn javascript_if_boolean_literal_condition() {
11860        check_metrics::<JavascriptParser>(
11861            "function f() {\n\
11862             \x20   if (true) {}                 // +1c\n\
11863             \x20   if (!false) {}               // +1c\n\
11864             \x20   while (true) {}              // +1c\n\
11865             \x20   do {} while (false);         // +1c\n\
11866             }\n",
11867            "foo.js",
11868            |metric| {
11869                assert_eq!(metric.abc.conditions_sum(), 4);
11870                insta::assert_json_snapshot!(metric.abc);
11871            },
11872        );
11873    }
11874
11875    #[cfg(feature = "javascript")]
11876    #[test]
11877    fn javascript_methods_arguments_with_conditions() {
11878        check_metrics::<JavascriptParser>(
11879            "function f(a, b) {\n\
11880             \x20   m(a, b);                     // +1b\n\
11881             \x20   m(!a, !b);                   // +1b +2c\n\
11882             }\n",
11883            "foo.js",
11884            |metric| {
11885                assert_eq!(metric.abc.branches_sum(), 2);
11886                assert_eq!(metric.abc.conditions_sum(), 2);
11887                insta::assert_json_snapshot!(metric.abc);
11888            },
11889        );
11890    }
11891
11892    #[cfg(feature = "javascript")]
11893    #[test]
11894    fn javascript_return_with_conditions() {
11895        check_metrics::<JavascriptParser>(
11896            "function m1(z) { return !(z >= 0); }\n\
11897             function m2(x) { return (((!x))); }\n\
11898             function m3(x, y) { return x && y; }\n",
11899            "foo.js",
11900            |metric| {
11901                // m1: 1 (`>=`). m2: 1 (walker unwraps to `x`).
11902                // m3: 2 (`&&` walker counts both terminals).
11903                assert_eq!(metric.abc.conditions_sum(), 4);
11904                insta::assert_json_snapshot!(metric.abc);
11905            },
11906        );
11907    }
11908
11909    #[cfg(feature = "typescript")]
11910    #[test]
11911    fn typescript_if_boolean_literal_condition() {
11912        check_metrics::<TypescriptParser>(
11913            "function f() {\n\
11914             \x20   if (true) {}\n\
11915             \x20   if (!false) {}\n\
11916             \x20   while (true) {}\n\
11917             \x20   do {} while (false);\n\
11918             }\n",
11919            "foo.ts",
11920            |metric| {
11921                assert_eq!(metric.abc.conditions_sum(), 4);
11922                insta::assert_json_snapshot!(metric.abc);
11923            },
11924        );
11925    }
11926
11927    #[cfg(feature = "typescript")]
11928    #[test]
11929    fn typescript_methods_arguments_with_conditions() {
11930        check_metrics::<TypescriptParser>(
11931            "function f(a: boolean, b: boolean) {\n\
11932             \x20   m(a, b);\n\
11933             \x20   m(!a, !b);\n\
11934             }\n",
11935            "foo.ts",
11936            |metric| {
11937                assert_eq!(metric.abc.branches_sum(), 2);
11938                assert_eq!(metric.abc.conditions_sum(), 2);
11939                insta::assert_json_snapshot!(metric.abc);
11940            },
11941        );
11942    }
11943
11944    #[cfg(feature = "typescript")]
11945    #[test]
11946    fn typescript_return_with_conditions() {
11947        check_metrics::<TypescriptParser>(
11948            "function m1(z: number): boolean { return !(z >= 0); }\n\
11949             function m2(x: boolean): boolean { return (((!x))); }\n\
11950             function m3(x: boolean, y: boolean): boolean { return x && y; }\n",
11951            "foo.ts",
11952            |metric| {
11953                assert_eq!(metric.abc.conditions_sum(), 4);
11954                insta::assert_json_snapshot!(metric.abc);
11955            },
11956        );
11957    }
11958
11959    #[cfg(feature = "typescript")]
11960    #[test]
11961    fn tsx_if_boolean_literal_condition() {
11962        check_metrics::<TsxParser>(
11963            "function f() {\n\
11964             \x20   if (true) {}\n\
11965             \x20   if (!false) {}\n\
11966             \x20   while (true) {}\n\
11967             \x20   do {} while (false);\n\
11968             }\n",
11969            "foo.tsx",
11970            |metric| {
11971                assert_eq!(metric.abc.conditions_sum(), 4);
11972                insta::assert_json_snapshot!(metric.abc);
11973            },
11974        );
11975    }
11976
11977    #[cfg(feature = "typescript")]
11978    #[test]
11979    fn tsx_methods_arguments_with_conditions() {
11980        check_metrics::<TsxParser>(
11981            "function f(a: boolean, b: boolean) {\n\
11982             \x20   m(a, b);\n\
11983             \x20   m(!a, !b);\n\
11984             }\n",
11985            "foo.tsx",
11986            |metric| {
11987                assert_eq!(metric.abc.branches_sum(), 2);
11988                assert_eq!(metric.abc.conditions_sum(), 2);
11989                insta::assert_json_snapshot!(metric.abc);
11990            },
11991        );
11992    }
11993
11994    #[cfg(feature = "typescript")]
11995    #[test]
11996    fn tsx_return_with_conditions() {
11997        check_metrics::<TsxParser>(
11998            "function m1(z: number): boolean { return !(z >= 0); }\n\
11999             function m2(x: boolean): boolean { return (((!x))); }\n\
12000             function m3(x: boolean, y: boolean): boolean { return x && y; }\n",
12001            "foo.tsx",
12002            |metric| {
12003                assert_eq!(metric.abc.conditions_sum(), 4);
12004                insta::assert_json_snapshot!(metric.abc);
12005            },
12006        );
12007    }
12008
12009    #[cfg(feature = "mozjs")]
12010    #[test]
12011    fn mozjs_jsx_elements_are_not_conditions() {
12012        // #1297 in the second expansion of `js_abc_compute!`. The
12013        // vendored mozjs fork carries the same three JSX productions as
12014        // upstream tree-sitter-javascript with its own `kind_id`s, so
12015        // the gate is a distinct instantiation and needs its own
12016        // fixture — a passing JavaScript test says nothing about the
12017        // macro's other expansion. Same shape and same discriminating
12018        // numbers as `javascript_jsx_elements_are_not_conditions`: 8
12019        // pre-fix, 1 after.
12020        check_metrics::<MozjsParser>(
12021            "function f(a, b) {
12022                if (a < b) { return <div className=\"x\"><span>hi</span></div>; }
12023                return <br />;
12024            }",
12025            "foo.js",
12026            |metric| {
12027                assert_eq!(metric.abc.conditions_sum(), 1);
12028                // Fixture-presence anchor: the `className="x"` attribute
12029                // `=` is this fixture's only assignment, so the test
12030                // fails if a later edit drops the JSX. Without it the
12031                // surviving `a < b` alone also scores 1, and the
12032                // conditions assertion would pass while proving nothing.
12033                assert_eq!(metric.abc.assignments_sum(), 1);
12034            },
12035        );
12036    }
12037
12038    #[cfg(feature = "mozjs")]
12039    #[test]
12040    fn mozjs_if_boolean_literal_condition() {
12041        check_metrics::<MozjsParser>(
12042            "function f() {\n\
12043             \x20   if (true) {}\n\
12044             \x20   if (!false) {}\n\
12045             \x20   while (true) {}\n\
12046             \x20   do {} while (false);\n\
12047             }\n",
12048            "foo.js",
12049            |metric| {
12050                assert_eq!(metric.abc.conditions_sum(), 4);
12051                insta::assert_json_snapshot!(metric.abc);
12052            },
12053        );
12054    }
12055
12056    #[cfg(feature = "mozjs")]
12057    #[test]
12058    fn mozjs_methods_arguments_with_conditions() {
12059        check_metrics::<MozjsParser>(
12060            "function f(a, b) {\n\
12061             \x20   m(a, b);\n\
12062             \x20   m(!a, !b);\n\
12063             }\n",
12064            "foo.js",
12065            |metric| {
12066                assert_eq!(metric.abc.branches_sum(), 2);
12067                assert_eq!(metric.abc.conditions_sum(), 2);
12068                insta::assert_json_snapshot!(metric.abc);
12069            },
12070        );
12071    }
12072
12073    #[cfg(feature = "mozjs")]
12074    #[test]
12075    fn mozjs_return_with_conditions() {
12076        check_metrics::<MozjsParser>(
12077            "function m1(z) { return !(z >= 0); }\n\
12078             function m2(x) { return (((!x))); }\n\
12079             function m3(x, y) { return x && y; }\n",
12080            "foo.js",
12081            |metric| {
12082                assert_eq!(metric.abc.conditions_sum(), 4);
12083                insta::assert_json_snapshot!(metric.abc);
12084            },
12085        );
12086    }
12087
12088    // ----- JS / TS / Tsx / Mozjs unary-conditional walker -----
12089
12090    #[cfg(feature = "javascript")]
12091    #[test]
12092    fn javascript_if_multiple_conditions() {
12093        check_metrics::<JavascriptParser>(
12094            "function f(a, b, c, d) {\n\
12095             \x20   if (a || b || c || d) {}        // +4c\n\
12096             \x20   if (a && b && c) {}             // +3c\n\
12097             \x20   if (!a && !b) {}                // +2c\n\
12098             }\n",
12099            "foo.js",
12100            |metric| {
12101                assert_eq!(metric.abc.conditions_sum(), 9);
12102                insta::assert_json_snapshot!(metric.abc);
12103            },
12104        );
12105    }
12106
12107    #[cfg(feature = "javascript")]
12108    #[test]
12109    fn javascript_while_and_do_while_conditions() {
12110        check_metrics::<JavascriptParser>(
12111            "function f(a, b) {\n\
12112             \x20   while (a || b) {}              // +2c\n\
12113             \x20   do {} while (a && !b);         // +2c\n\
12114             }\n",
12115            "foo.js",
12116            |metric| {
12117                assert_eq!(metric.abc.conditions_sum(), 4);
12118                insta::assert_json_snapshot!(metric.abc);
12119            },
12120        );
12121    }
12122
12123    #[cfg(feature = "javascript")]
12124    #[test]
12125    fn javascript_short_circuit_with_boolean_literal_operand() {
12126        check_metrics::<JavascriptParser>(
12127            "function f(a) { return a && true; }\n",
12128            "foo.js",
12129            |metric| {
12130                assert_eq!(metric.abc.conditions_sum(), 2);
12131                insta::assert_json_snapshot!(metric.abc);
12132            },
12133        );
12134    }
12135
12136    #[cfg(feature = "typescript")]
12137    #[test]
12138    fn typescript_if_multiple_conditions() {
12139        check_metrics::<TypescriptParser>(
12140            "function f(a: boolean, b: boolean, c: boolean, d: boolean) {\n\
12141             \x20   if (a || b || c || d) {}        // +4c\n\
12142             \x20   if (a && b && c) {}             // +3c\n\
12143             \x20   if (!a && !b) {}                // +2c\n\
12144             }\n",
12145            "foo.ts",
12146            |metric| {
12147                assert_eq!(metric.abc.conditions_sum(), 9);
12148                insta::assert_json_snapshot!(metric.abc);
12149            },
12150        );
12151    }
12152
12153    #[cfg(feature = "typescript")]
12154    #[test]
12155    fn typescript_while_and_do_while_conditions() {
12156        check_metrics::<TypescriptParser>(
12157            "function f(a: boolean, b: boolean) {\n\
12158             \x20   while (a || b) {}              // +2c\n\
12159             \x20   do {} while (a && !b);         // +2c\n\
12160             }\n",
12161            "foo.ts",
12162            |metric| {
12163                assert_eq!(metric.abc.conditions_sum(), 4);
12164                insta::assert_json_snapshot!(metric.abc);
12165            },
12166        );
12167    }
12168
12169    #[cfg(feature = "typescript")]
12170    #[test]
12171    fn typescript_short_circuit_with_boolean_literal_operand() {
12172        check_metrics::<TypescriptParser>(
12173            "function f(a: boolean): boolean { return a && true; }\n",
12174            "foo.ts",
12175            |metric| {
12176                assert_eq!(metric.abc.conditions_sum(), 2);
12177                insta::assert_json_snapshot!(metric.abc);
12178            },
12179        );
12180    }
12181
12182    #[cfg(feature = "typescript")]
12183    #[test]
12184    fn tsx_if_multiple_conditions() {
12185        check_metrics::<TsxParser>(
12186            "function f(a: boolean, b: boolean, c: boolean, d: boolean) {\n\
12187             \x20   if (a || b || c || d) {}        // +4c\n\
12188             \x20   if (a && b && c) {}             // +3c\n\
12189             \x20   if (!a && !b) {}                // +2c\n\
12190             }\n",
12191            "foo.tsx",
12192            |metric| {
12193                assert_eq!(metric.abc.conditions_sum(), 9);
12194                insta::assert_json_snapshot!(metric.abc);
12195            },
12196        );
12197    }
12198
12199    #[cfg(feature = "typescript")]
12200    #[test]
12201    fn tsx_while_and_do_while_conditions() {
12202        check_metrics::<TsxParser>(
12203            "function f(a: boolean, b: boolean) {\n\
12204             \x20   while (a || b) {}              // +2c\n\
12205             \x20   do {} while (a && !b);         // +2c\n\
12206             }\n",
12207            "foo.tsx",
12208            |metric| {
12209                assert_eq!(metric.abc.conditions_sum(), 4);
12210                insta::assert_json_snapshot!(metric.abc);
12211            },
12212        );
12213    }
12214
12215    #[cfg(feature = "typescript")]
12216    #[test]
12217    fn tsx_short_circuit_with_boolean_literal_operand() {
12218        check_metrics::<TsxParser>(
12219            "function f(a: boolean): boolean { return a && true; }\n",
12220            "foo.tsx",
12221            |metric| {
12222                assert_eq!(metric.abc.conditions_sum(), 2);
12223                insta::assert_json_snapshot!(metric.abc);
12224            },
12225        );
12226    }
12227
12228    #[cfg(feature = "mozjs")]
12229    #[test]
12230    fn mozjs_if_multiple_conditions() {
12231        check_metrics::<MozjsParser>(
12232            "function f(a, b, c, d) {\n\
12233             \x20   if (a || b || c || d) {}        // +4c\n\
12234             \x20   if (a && b && c) {}             // +3c\n\
12235             \x20   if (!a && !b) {}                // +2c\n\
12236             }\n",
12237            "foo.js",
12238            |metric| {
12239                assert_eq!(metric.abc.conditions_sum(), 9);
12240                insta::assert_json_snapshot!(metric.abc);
12241            },
12242        );
12243    }
12244
12245    #[cfg(feature = "mozjs")]
12246    #[test]
12247    fn mozjs_while_and_do_while_conditions() {
12248        check_metrics::<MozjsParser>(
12249            "function f(a, b) {\n\
12250             \x20   while (a || b) {}              // +2c\n\
12251             \x20   do {} while (a && !b);         // +2c\n\
12252             }\n",
12253            "foo.js",
12254            |metric| {
12255                assert_eq!(metric.abc.conditions_sum(), 4);
12256                insta::assert_json_snapshot!(metric.abc);
12257            },
12258        );
12259    }
12260
12261    #[cfg(feature = "mozjs")]
12262    #[test]
12263    fn mozjs_short_circuit_with_boolean_literal_operand() {
12264        check_metrics::<MozjsParser>(
12265            "function f(a) { return a && true; }\n",
12266            "foo.js",
12267            |metric| {
12268                assert_eq!(metric.abc.conditions_sum(), 2);
12269                insta::assert_json_snapshot!(metric.abc);
12270            },
12271        );
12272    }
12273
12274    // ---------- Perl ABC tests ----------
12275
12276    #[cfg(feature = "perl")]
12277    #[test]
12278    fn perl_empty_unit_zero() {
12279        // Empty source produces zero ABC magnitude — pins the trait
12280        // wiring without exercising any compute branch.
12281        check_metrics::<PerlParser>("", "empty.pl", |metric| {
12282            assert_eq!(metric.abc.assignments_sum(), 0);
12283            assert_eq!(metric.abc.branches_sum(), 0);
12284            assert_eq!(metric.abc.conditions_sum(), 0);
12285            insta::assert_json_snapshot!(metric.abc);
12286        });
12287    }
12288
12289    #[cfg(feature = "perl")]
12290    #[test]
12291    fn perl_plain_and_compound_assignments_count() {
12292        // `my $x = 0` parses as a `binary_expression` with an `=`
12293        // token, so the initialiser counts (Perl has no equivalent of
12294        // the JS `const` initialiser-suppression rule). Each
12295        // assignment operator token contributes one assignment:
12296        // `=`, `=`, `+=`, `.=`, `**=` → A = 5. Two of those `=` come
12297        // from the `my $x = 0` initialiser and the later `$x = 5`
12298        // reassignment.
12299        check_metrics::<PerlParser>(
12300            "sub f { my $x = 0; $x = 5; $x += 2; $x .= \"a\"; $x **= 3; }",
12301            "foo.pl",
12302            |metric| {
12303                assert_eq!(metric.abc.assignments_sum(), 5);
12304                assert_eq!(metric.abc.branches_sum(), 0);
12305                assert_eq!(metric.abc.conditions_sum(), 0);
12306                insta::assert_json_snapshot!(metric.abc);
12307            },
12308        );
12309    }
12310
12311    #[cfg(feature = "perl")]
12312    #[test]
12313    fn perl_calls_are_branches() {
12314        // `foo()` parses as `call_expression_with_args_with_brackets`
12315        // wrapping an inner `call_expression_with_bareword(foo)`;
12316        // `bar 1, 2` wraps `bar` likewise under spaced-args; `shift`
12317        // appears as a standalone bareword. The bareword-inside-
12318        // wrapper case must NOT double-count — only the outer wrapper
12319        // contributes a branch. So B = 3 (foo, bar, shift), not 5.
12320        check_metrics::<PerlParser>(
12321            "sub f { foo(); bar 1, 2; my $a = shift; }",
12322            "foo.pl",
12323            |metric| {
12324                // shift's `my $a = shift` initialiser contributes one
12325                // assignment via the `=` token.
12326                assert_eq!(metric.abc.assignments_sum(), 1);
12327                assert_eq!(metric.abc.branches_sum(), 3);
12328                assert_eq!(metric.abc.conditions_sum(), 0);
12329                insta::assert_json_snapshot!(metric.abc);
12330            },
12331        );
12332    }
12333
12334    #[cfg(feature = "perl")]
12335    #[test]
12336    fn perl_method_invocation_counts_as_branch() {
12337        // `$obj->method(...)` parses as `method_invocation`. Any
12338        // arrow-dispatch counts as one branch regardless of how the
12339        // arguments are passed.
12340        check_metrics::<PerlParser>(
12341            "sub f { my $obj = shift; $obj->run($x); $obj->ping; }",
12342            "foo.pl",
12343            |metric| {
12344                // `my $obj = shift` → A=1, B=1 (shift bareword).
12345                // `$obj->run($x)` and `$obj->ping` → 2 more branches.
12346                assert_eq!(metric.abc.assignments_sum(), 1);
12347                assert_eq!(metric.abc.branches_sum(), 3);
12348                assert_eq!(metric.abc.conditions_sum(), 0);
12349                insta::assert_json_snapshot!(metric.abc);
12350            },
12351        );
12352    }
12353
12354    // #1297's own sweep cleared Perl, and it was wrong. `<STDIN>` does
12355    // lex as a single `standard_input` token and scores 0, which is
12356    // what the issue measured — but the filehandle and lexical-handle
12357    // readlines do not: `<FH>` is `standard_input_to_identifier` and
12358    // `<$fh>` is `standard_input_to_variable`, each a plain three-token
12359    // sequence whose brackets are the same bare `<` / `>` a comparison
12360    // uses, and the arm was ungated. Each scored two phantom
12361    // conditions.
12362    //
12363    // All three readline spellings against two genuine comparisons, so
12364    // every mis-aim lands on its own number: 6 pre-fix, 2 once the arm
12365    // allows `BinaryExpression`, 0 if it allows the wrong parent or the
12366    // fixture stops parsing. The assertion is also the grammar-dispatch
12367    // §8 pin — `cyclomatic()` is 3 on this space, so decisions is 2 and
12368    // the two counts agree exactly.
12369    #[cfg(feature = "perl")]
12370    #[test]
12371    fn perl_readline_angle_brackets_are_not_conditions() {
12372        check_func_space::<PerlParser, _>(
12373            "sub g {\n\
12374                 my ($a, $b, $fh) = @_;\n\
12375                 my $l = <FH>;\n\
12376                 my $m = <$fh>;\n\
12377                 my $n = <STDIN>;\n\
12378                 if ($a < $b) { return 1; }\n\
12379                 if ($a > $b) { return 2; }\n\
12380                 return 0;\n\
12381             }",
12382            "foo.pl",
12383            |space| {
12384                assert_deepest_conditions_match_cyclomatic(&space, 2);
12385                // Fixture-presence anchor: the three readlines are
12386                // three of this sub's four assignments (the fourth is
12387                // the `my (...) = @_` unpack), so dropping them from
12388                // the fixture fails here rather than leaving a test
12389                // that still reports 2 conditions and proves nothing.
12390                assert_eq!(space.metrics.abc.assignments_sum(), 4);
12391            },
12392        );
12393    }
12394
12395    #[cfg(feature = "perl")]
12396    #[test]
12397    fn perl_numeric_and_string_comparisons_count_conditions() {
12398        // Numeric ops `==`, `!=`, `<`, `>`, `<=`, `>=`, `<=>` and
12399        // string ops `eq`, `ne`, `lt`, `gt`, `le`, `ge`, `cmp` each
12400        // fire once per token. The sample below uses one of each →
12401        // C = 14. No assignments, no branches.
12402        check_metrics::<PerlParser>(
12403            "sub f {\n\
12404                 my $r;\n\
12405                 $r = $a == $b;\n\
12406                 $r = $a != $b;\n\
12407                 $r = $a <  $b;\n\
12408                 $r = $a >  $b;\n\
12409                 $r = $a <= $b;\n\
12410                 $r = $a >= $b;\n\
12411                 $r = $a <=> $b;\n\
12412                 $r = $a eq $b;\n\
12413                 $r = $a ne $b;\n\
12414                 $r = $a lt $b;\n\
12415                 $r = $a gt $b;\n\
12416                 $r = $a le $b;\n\
12417                 $r = $a ge $b;\n\
12418                 $r = $a cmp $b;\n\
12419             }",
12420            "foo.pl",
12421            |metric| {
12422                // 15 `=` tokens: one declaration `my $r` (no `=`),
12423                // then 14 `$r = …` plus there's no `=` in `my $r;`.
12424                // Actually: `my $r;` has no `=`; the 14 `$r = …` are
12425                // 14 `=` tokens. So A=14, C=14.
12426                assert_eq!(metric.abc.assignments_sum(), 14);
12427                assert_eq!(metric.abc.branches_sum(), 0);
12428                assert_eq!(metric.abc.conditions_sum(), 14);
12429                insta::assert_json_snapshot!(metric.abc);
12430            },
12431        );
12432    }
12433
12434    #[cfg(feature = "perl")]
12435    #[test]
12436    fn perl_short_circuit_not_counted_directly_ternary_counts() {
12437        // `&&`, `||`, `//`, low-precedence `and`, `or`, `xor` are
12438        // NOT counted as conditions on their own (Fitzpatrick Rule
12439        // 5; #395) — instead each operand is counted as a unary
12440        // conditional by the walker (Rule 9; #403). At the pinned
12441        // tree-sitter-perl grammar version, only the four
12442        // punctuation forms plus one keyword form parse under a
12443        // `binary_expression` parent that triggers the walker; the
12444        // other two keyword forms parse under a distinct grammar
12445        // node and contribute zero. Net: 4 walker-firing lines × 2
12446        // scalar-variable operands + 1 ternary node + 1 for the
12447        // ternary's bare `$a` condition operand (#1102) = 10. The
12448        // exact mix of "which two keyword forms are silent" is
12449        // grammar-version-dependent; a future grammar bump that
12450        // normalises the keyword forms' parent kind will shift this
12451        // count to 14. See follow-up note above the test name.
12452        check_metrics::<PerlParser>(
12453            "sub f {\n\
12454                 my $r;\n\
12455                 $r = $a && $b;\n\
12456                 $r = $a || $b;\n\
12457                 $r = $a // $b;\n\
12458                 $r = $a and $b;\n\
12459                 $r = $a or  $b;\n\
12460                 $r = $a xor $b;\n\
12461                 $r = $a ? 1 : 2;\n\
12462             }",
12463            "foo.pl",
12464            |metric| {
12465                // 7 `=` tokens (one per reassignment line).
12466                assert_eq!(metric.abc.assignments_sum(), 7);
12467                assert_eq!(metric.abc.branches_sum(), 0);
12468                // 4 walker-triggered lines × 2 operands + 1 ternary
12469                // node + 1 for its bare `$a` condition operand = 10.
12470                // The two remaining low-precedence keyword forms (one
12471                // of `and`/`or`/`xor`) fall under a
12472                // non-binary_expression parent in this grammar
12473                // version and contribute zero via the walker.
12474                assert_eq!(metric.abc.conditions_sum(), 10);
12475                insta::assert_json_snapshot!(metric.abc);
12476            },
12477        );
12478    }
12479
12480    // Issue #1102, Perl half. See
12481    // `cpp_ternary_operand_slots_count_as_unary_conditions` for the
12482    // rule. Like PHP, Perl's ABC dispatcher has no `?`-token arm — the
12483    // grammar does emit the token, but the `ternary_expression` node is
12484    // what carries the tally's +1. tree-sitter-perl names the branch
12485    // fields `true` / `false` rather than the C-family `consequence` /
12486    // `alternative`, so a copied C-family gate would match nothing.
12487    #[cfg(feature = "perl")]
12488    #[test]
12489    fn perl_ternary_operand_slots_count_as_unary_conditions() {
12490        // ternary (1) + condition `$a` (1) + `!$b` (1) + `!$c` (1) = 4.
12491        check_metrics::<PerlParser>("sub f { my $x = $a ? !$b : !$c; }", "foo.pl", |metric| {
12492            assert_eq!(metric.abc.conditions_sum(), 4);
12493        });
12494        // No-double-count pin: ternary (1) + `>` (1) = 2, unchanged by
12495        // the fix.
12496        check_metrics::<PerlParser>(
12497            "sub f { my $x = ($a > 0) ? $b : -$b; }",
12498            "foo.pl",
12499            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
12500        );
12501        // A negated *condition* takes the walker's `else` fallback —
12502        // `!$a` is neither a boolean terminal nor a paren wrapper, so
12503        // only `perl_inspect_container` can classify it. Delete the
12504        // fallback and this reads 1. ternary (1) + `!$a` (1) = 2.
12505        check_metrics::<PerlParser>("sub f { my $x = !$a ? $b : $c; }", "foo.pl", |metric| {
12506            assert_eq!(metric.abc.conditions_sum(), 2);
12507        });
12508        // Nested: two ternary nodes plus the two bare-variable
12509        // conditions = 4.
12510        check_metrics::<PerlParser>(
12511            "sub f { my $x = $a ? ($b ? $c : $d) : $e; }",
12512            "foo.pl",
12513            |metric| assert_eq!(metric.abc.conditions_sum(), 4),
12514        );
12515    }
12516
12517    #[cfg(feature = "perl")]
12518    #[test]
12519    fn perl_elsif_and_else_count_conditions() {
12520        // `if (… == …) { … } elsif (… < …) { … } else { … }` →
12521        // 2 comparison tokens (`==`, `<`), plus `elsif_clause` and
12522        // `else_clause` each + 1 → C = 4. Branches: 0 (only
12523        // assignments). Assignments: just the `=` initialisers /
12524        // reassignments — there are 4 here (`$x` init plus three
12525        // `$x = …` reassigns).
12526        check_metrics::<PerlParser>(
12527            "sub f {\n\
12528                 my $x = 0;\n\
12529                 if ($a == $b) {\n\
12530                     $x = 1;\n\
12531                 } elsif ($a < $b) {\n\
12532                     $x = 2;\n\
12533                 } else {\n\
12534                     $x = 3;\n\
12535                 }\n\
12536             }",
12537            "foo.pl",
12538            |metric| {
12539                assert_eq!(metric.abc.assignments_sum(), 4);
12540                assert_eq!(metric.abc.branches_sum(), 0);
12541                assert_eq!(metric.abc.conditions_sum(), 4);
12542                insta::assert_json_snapshot!(metric.abc);
12543            },
12544        );
12545    }
12546
12547    #[cfg(feature = "perl")]
12548    #[test]
12549    fn perl_regex_match_operators_count_conditions() {
12550        // `=~` and `!~` are pattern-match operators; we count both
12551        // as conditions because they evaluate the regex match in a
12552        // boolean context.
12553        check_metrics::<PerlParser>(
12554            "sub f { my $s = shift; my $m = $s =~ /foo/; my $n = $s !~ /bar/; }",
12555            "foo.pl",
12556            |metric| {
12557                // 3 `=` tokens, 0 branches except `shift` bareword.
12558                assert_eq!(metric.abc.assignments_sum(), 3);
12559                assert_eq!(metric.abc.branches_sum(), 1);
12560                assert_eq!(metric.abc.conditions_sum(), 2);
12561                insta::assert_json_snapshot!(metric.abc);
12562            },
12563        );
12564    }
12565
12566    #[cfg(feature = "perl")]
12567    #[test]
12568    fn perl_complex_function_abc() {
12569        // Mixed program exercising every category. Computed
12570        // expected:
12571        //   Assignments: `my $i = 0` (1), `$i++` is a unary
12572        //     increment — Perl's grammar emits `PLUSPLUS` not an `=`
12573        //     operator, so it does NOT count under the operator-
12574        //     token rule. The for-loop's `$i++` is similarly
12575        //     uncounted.
12576        //     Total A: 1 from `my $i = 0`, 1 from `$total += $i`
12577        //     (the `+=` token) → A = 2.
12578        //   Branches: `do_work($i)` → 1; `print "done\n"` is a
12579        //     call_expression_with_spaced_args → 1; `return $total`
12580        //     uses the `return` keyword not a call → 0. B = 2.
12581        //   Conditions: `$i < 10` (`<`) → 1; `$i % 2 == 0` (`==`) →
12582        //     1; `else_clause` → 1. C = 3.
12583        check_metrics::<PerlParser>(
12584            "sub run {\n\
12585                 my $total = 0;\n\
12586                 for (my $i = 0; $i < 10; $i++) {\n\
12587                     if ($i % 2 == 0) {\n\
12588                         do_work($i);\n\
12589                     } else {\n\
12590                         $total += $i;\n\
12591                     }\n\
12592                 }\n\
12593                 print \"done\\n\";\n\
12594                 return $total;\n\
12595             }",
12596            "foo.pl",
12597            |metric| {
12598                // `my $total = 0` is one `=`; `my $i = 0` is another
12599                // `=`; `$total += $i` is one `+=`. Total = 3.
12600                assert_eq!(metric.abc.assignments_sum(), 3);
12601                assert_eq!(metric.abc.branches_sum(), 2);
12602                assert_eq!(metric.abc.conditions_sum(), 3);
12603                insta::assert_json_snapshot!(metric.abc);
12604            },
12605        );
12606    }
12607
12608    #[cfg(feature = "perl")]
12609    #[test]
12610    fn perl_if_multiple_conditions() {
12611        // Fitzpatrick Rule 9 walker (issue #403): each operand of a
12612        // `&&` / `||` / `//` / `and` / `or` / `xor` chain is one
12613        // condition. ScalarVariable operands ($a, $b, …) qualify as
12614        // terminal-bool kinds for the walker.
12615        check_metrics::<PerlParser>(
12616            "sub f {\n\
12617                 my ($a, $b, $c, $d) = @_;\n\
12618                 if ($a || $b || $c || $d) { return 1; }    # +4c\n\
12619                 if ($a && $b && $c) { return 2; }          # +3c\n\
12620                 if (!$a && !$b) { return 3; }              # +2c\n\
12621                 return 0;\n\
12622             }",
12623            "foo.pl",
12624            |metric| {
12625                assert_eq!(metric.abc.conditions_sum(), 9);
12626                insta::assert_json_snapshot!(metric.abc);
12627            },
12628        );
12629    }
12630
12631    #[cfg(feature = "perl")]
12632    #[test]
12633    fn perl_while_and_until_conditions() {
12634        // Perl has no `do { ... } while(cond);` shape in this grammar
12635        // — `while` and `until` are the loop forms with a condition
12636        // slot. The walker fires on each `&&` / `||` token inside
12637        // those headers.
12638        check_metrics::<PerlParser>(
12639            "sub f {\n\
12640                 my ($a, $b) = @_;\n\
12641                 while ($a || $b) { last; }            # +2c\n\
12642                 until ($a && !$b) { last; }           # +2c\n\
12643             }",
12644            "foo.pl",
12645            |metric| {
12646                assert_eq!(metric.abc.conditions_sum(), 4);
12647                insta::assert_json_snapshot!(metric.abc);
12648            },
12649        );
12650    }
12651
12652    #[cfg(feature = "perl")]
12653    #[test]
12654    fn perl_for_header_condition_slot_counts_unary_conditions() {
12655        // The Perl half of #1276. The C-style `for` header's condition
12656        // slot is a Rule 9 unary condition like the `if` slot: a bare
12657        // `$ok`, a negated `!$ok` and a parenthesised `($ok)` each count
12658        // one, an empty header and a `foreach` count zero, and a
12659        // comparison-shaped `$i < $n` stays at the one the `<` arm
12660        // already counts. Every C-style row is the three-clause spelling
12661        // because tree-sitter-perl does not parse an empty initializer.
12662        let cases = [
12663            ("for (my $i = 0; $ok; $i++) { }", 1),
12664            ("for (my $i = 0; !$ok; $i++) { }", 1),
12665            ("for (my $i = 0; ($ok); $i++) { }", 1),
12666            ("for (my $i = 0; $i < $n; $i++) { }", 1),
12667            ("for (my $i = 0; $ok && $j; $i++) { }", 2),
12668            ("for (;;) { last; }", 0),
12669            ("for my $x (@l) { }", 0),
12670        ];
12671        let mut ran = 0;
12672        for (body, expected) in cases {
12673            let src = format!("sub f {{ {body} }}\n");
12674            assert_eq!(abc_conditions(LANG::Perl, &src), expected, "`{body}`");
12675            ran += 1;
12676        }
12677        assert_eq!(ran, cases.len());
12678        assert!(cases.iter().any(|&(_, n)| n == 0));
12679        assert!(cases.iter().any(|&(_, n)| n == 2));
12680
12681        // The slot agrees with the `if` slot for every shape, which is
12682        // the property the fix restores; the reference is non-degenerate
12683        // by the table above.
12684        for condition in ["$ok", "!$ok", "($ok)", "$ok && $j"] {
12685            let in_for = format!("sub f {{ for (my $i = 0; {condition}; $i++) {{ }} }}\n");
12686            let in_if = format!("sub f {{ if ({condition}) {{ }} }}\n");
12687            assert_eq!(
12688                abc_conditions(LANG::Perl, &in_for),
12689                abc_conditions(LANG::Perl, &in_if),
12690                "`{condition}` must score alike in a `for` header and an `if`",
12691            );
12692        }
12693    }
12694
12695    #[cfg(feature = "perl")]
12696    #[test]
12697    fn perl_short_circuit_counts_scalar_variable_operands() {
12698        // `$a && $b` reports 2 conditions — one walker count per
12699        // `ScalarVariable` operand. Renamed from the cross-language
12700        // `_with_boolean_literal_operand` convention because Perl has
12701        // no readily-grammar-exposed boolean literal in an `&&`
12702        // operand slot at the pinned grammar version (the `Boolean`
12703        // kind only fires on the `boolean` pragma's named constants,
12704        // not bareword `1` / `0`). Two scalar variables are the
12705        // grammar-stable terminal-set witness for Perl.
12706        check_metrics::<PerlParser>(
12707            "sub f { my ($a) = @_; return $a && $b; }\n",
12708            "foo.pl",
12709            |metric| {
12710                assert_eq!(metric.abc.conditions_sum(), 2);
12711                insta::assert_json_snapshot!(metric.abc);
12712            },
12713        );
12714    }
12715
12716    #[cfg(feature = "perl")]
12717    #[test]
12718    fn perl_array_in_binary_operand_descends_to_scalar_context_value() {
12719        // Regression test for the code-review findings on the
12720        // Phase-2B Perl walker:
12721        //   - Pre-fix-A: `perl_inspect_container` descended `Array`
12722        //     via `node.child(1)` — the FIRST element — wrongly
12723        //     attributing `$x` for `($x, $y)` (semantically `$y`
12724        //     is the scalar-context value).
12725        //   - Fix-A (the `array_is_paren` guard, 5db8078): dropped
12726        //     Array-as-paren entirely in `BinaryExpression` operand
12727        //     contexts to avoid the wrong attribution — but
12728        //     regressed `$a || ($x)` (single paren-grouped operand)
12729        //     to C=1 instead of 2.
12730        //   - Fix-B (this change): keeps Array-as-paren unconditional
12731        //     but descends via the LAST named child. `$a || ($x)`
12732        //     reaches `$x` (count both operands → 2);
12733        //     `$a || ($x, $y)` reaches `$y` (count `$a` + `$y` →
12734        //     still 2, matching Fitzpatrick Rule 7 "one per
12735        //     operand"); `if ($a)` still reaches `$a` (single-
12736        //     element grouping → 1).
12737        check_metrics::<PerlParser>(
12738            "sub f { my ($a, $x, $y) = @_;\n\
12739             \x20   my $r = $a || ($x, $y);    # +2c: $a + last-named $y\n\
12740             \x20   my $s = $a || ($x);        # +2c: $a + only-named $x\n\
12741             \x20   $r + $s;\n\
12742             }\n",
12743            "foo.pl",
12744            |metric| {
12745                // 2 + 2 = 4 unary conditions from the two `||`s.
12746                assert_eq!(metric.abc.conditions_sum(), 4);
12747                insta::assert_json_snapshot!(metric.abc);
12748            },
12749        );
12750    }
12751
12752    #[cfg(feature = "perl")]
12753    #[test]
12754    fn perl_if_scalar_variable_condition() {
12755        // Renamed from the cross-language
12756        // `_if_boolean_literal_condition` convention because
12757        // Perl has no readily-grammar-exposed boolean literal in
12758        // an `if (cond)` slot at the pinned grammar version:
12759        // tree-sitter-perl's `Boolean` kind only fires for the
12760        // `boolean` pragma's named constants (not bareword `1` /
12761        // `0`, which surface as `Integer` / not in the
12762        // terminal-bool set). A scalar-variable condition is the
12763        // grammar-stable witness — `if ($a)` reaches
12764        // `scalar_variable` via the `Array` paren unwrap.
12765        check_metrics::<PerlParser>(
12766            "sub f { my ($a) = @_; if ($a) { return 1; } }\n",
12767            "foo.pl",
12768            |metric| {
12769                assert_eq!(metric.abc.conditions_sum(), 1);
12770                insta::assert_json_snapshot!(metric.abc);
12771            },
12772        );
12773    }
12774
12775    #[cfg(feature = "perl")]
12776    #[test]
12777    fn perl_methods_arguments_with_conditions() {
12778        // `call(!$a, !$b)` — argument list walker counts each
12779        // unary-conditional argument once. Cannot use `m(...)` as
12780        // the function name — tree-sitter-perl parses `m(...)` as
12781        // the regex-match operator, not a function call.
12782        check_metrics::<PerlParser>(
12783            "sub f { my ($a, $b) = @_; call($a, $b); call(!$a, !$b); }\n",
12784            "foo.pl",
12785            |metric| {
12786                // Two calls × 1 branch each = 2 branches.
12787                // `call(!$a, !$b)` contributes 2 walker conditions
12788                // (one per `!`-wrapped scalar-variable argument);
12789                // `call($a, $b)` contributes 0 (bare-args don't
12790                // count via the Arguments walker — list_kind !=
12791                // BinaryExpression).
12792                assert_eq!(metric.abc.branches_sum(), 2);
12793                assert_eq!(metric.abc.conditions_sum(), 2);
12794                insta::assert_json_snapshot!(metric.abc);
12795            },
12796        );
12797    }
12798
12799    #[cfg(feature = "perl")]
12800    #[test]
12801    fn perl_return_with_conditions() {
12802        // `return !$a` reports 1 condition via the walker (unary
12803        // unwrap to scalar-variable terminal). `return $a` reports
12804        // 0 (no paren / unary wrap, has_boolean_content stays
12805        // false from ReturnExpression parent).
12806        check_metrics::<PerlParser>(
12807            "sub m1 { my ($z) = @_; return !($z); }\n\
12808             sub m2 { my ($x) = @_; return (((!$x))); }\n\
12809             sub m3 { my ($x, $y) = @_; return $x && $y; }\n",
12810            "foo.pl",
12811            |metric| {
12812                // m1: !($z) → walker on `!` unwraps paren to $z (1).
12813                // m2: (((!$x))) → walker unwraps three parens + one
12814                //     unary to $x (1).
12815                // m3: $x && $y → walker on `&&` counts both (2).
12816                // Sum: 4.
12817                assert_eq!(metric.abc.conditions_sum(), 4);
12818                insta::assert_json_snapshot!(metric.abc);
12819            },
12820        );
12821    }
12822
12823    // ---------- Lua ABC tests ----------
12824
12825    #[cfg(feature = "lua")]
12826    #[test]
12827    fn lua_empty_unit_zero() {
12828        check_metrics::<LuaParser>("", "empty.lua", |metric| {
12829            assert_eq!(metric.abc.assignments_sum(), 0);
12830            assert_eq!(metric.abc.branches_sum(), 0);
12831            assert_eq!(metric.abc.conditions_sum(), 0);
12832            insta::assert_json_snapshot!(metric.abc);
12833        });
12834    }
12835
12836    #[cfg(feature = "lua")]
12837    #[test]
12838    fn lua_assignments_count_locals_and_plain() {
12839        // `local x = 0` wraps an `assignment_statement` under a
12840        // `variable_declaration`; the inner wrapper still counts.
12841        // Multi-target assignment `a, b = 1, 2` is a single
12842        // `assignment_statement` and contributes 1, NOT 2 — the
12843        // wrapper is the unit of counting (matches the Python rule:
12844        // one `Assignment` node, one assignment).
12845        check_metrics::<LuaParser>(
12846            "function f()\n\
12847                 local x = 0\n\
12848                 x = 1\n\
12849                 local a, b = 1, 2\n\
12850                 a, b = b, a\n\
12851             end",
12852            "foo.lua",
12853            |metric| {
12854                assert_eq!(metric.abc.assignments_sum(), 4);
12855                assert_eq!(metric.abc.branches_sum(), 0);
12856                assert_eq!(metric.abc.conditions_sum(), 0);
12857                insta::assert_json_snapshot!(metric.abc);
12858            },
12859        );
12860    }
12861
12862    #[cfg(feature = "lua")]
12863    #[test]
12864    fn lua_calls_are_branches() {
12865        // `print(x)`, `obj.m(x)`, `obj:m(x)`, `f(g(1))` — every
12866        // call form is a `function_call` node. The nested
12867        // `f(g(1))` counts as 2 branches (one per dispatch).
12868        check_metrics::<LuaParser>(
12869            "function r(x)\n\
12870                 print(x)\n\
12871                 obj.m(x)\n\
12872                 obj:m(x)\n\
12873                 return f(g(1))\n\
12874             end",
12875            "foo.lua",
12876            |metric| {
12877                assert_eq!(metric.abc.assignments_sum(), 0);
12878                assert_eq!(metric.abc.branches_sum(), 5);
12879                assert_eq!(metric.abc.conditions_sum(), 0);
12880                insta::assert_json_snapshot!(metric.abc);
12881            },
12882        );
12883    }
12884
12885    #[cfg(feature = "lua")]
12886    #[test]
12887    fn lua_comparisons_count_logical_ops_do_not() {
12888        // Each comparison token contributes one condition; `and` /
12889        // `or` are NOT counted on their own (Fitzpatrick Rule 5;
12890        // #395) — instead each operand is counted as a unary
12891        // conditional by the walker (Rule 9; #403). The two
12892        // `a and b` / `a or b` lines add 2 walker conditions each.
12893        check_metrics::<LuaParser>(
12894            "function f(a, b)\n\
12895                 local r\n\
12896                 r = a == b\n\
12897                 r = a ~= b\n\
12898                 r = a <  b\n\
12899                 r = a >  b\n\
12900                 r = a <= b\n\
12901                 r = a >= b\n\
12902                 r = a and b\n\
12903                 r = a or  b\n\
12904             end",
12905            "foo.lua",
12906            |metric| {
12907                // 8 `r = …` reassignments, plus `local r` (no `=`).
12908                assert_eq!(metric.abc.assignments_sum(), 8);
12909                assert_eq!(metric.abc.branches_sum(), 0);
12910                // 6 comparisons (+6) + 2 logical lines × 2 walker
12911                // operands (+4) = 10.
12912                assert_eq!(metric.abc.conditions_sum(), 10);
12913                insta::assert_json_snapshot!(metric.abc);
12914            },
12915        );
12916    }
12917
12918    // #1297: Lua 5.4 brackets a variable attribute with the same two
12919    // bare tokens a comparison uses, and the `<` / `>` arm had no gate,
12920    // so `local x <const> = 1` scored two conditions. Two attributes
12921    // against one genuine `a < b` separate every mis-aim: 5 pre-fix, 1
12922    // once the arm allows `BinaryExpression`, 0 if it allows the wrong
12923    // parent or the fixture stops parsing.
12924    //
12925    // No fixture-presence anchor is available, unlike the JSX and Perl
12926    // fixtures: once excluded, a Lua attribute contributes to no ABC
12927    // axis at all (`local x <const> = 1` and `local x = 1` score
12928    // identically on assignments, branches and conditions), and it
12929    // opens no space. Deleting the two attributes would leave this test
12930    // passing on the `a < b` alone. The coverage that the attributes
12931    // are reaching the arm is the revert test: removing the gate makes
12932    // this the only failing test in the suite.
12933    #[cfg(feature = "lua")]
12934    #[test]
12935    fn lua_variable_attributes_are_not_conditions() {
12936        check_metrics::<LuaParser>(
12937            "local function f(a, b)\n\
12938                 local x <const> = 1\n\
12939                 local y <close> = nil\n\
12940                 if a < b then return x end\n\
12941                 return y\n\
12942             end",
12943            "foo.lua",
12944            |metric| {
12945                assert_eq!(metric.abc.conditions_sum(), 1);
12946            },
12947        );
12948    }
12949
12950    #[cfg(feature = "lua")]
12951    #[test]
12952    fn lua_elseif_and_else_count_conditions() {
12953        // Each elseif / else arm of the if contributes one
12954        // condition, mirroring the Python rule.
12955        check_metrics::<LuaParser>(
12956            "function f(x)\n\
12957                 if x > 0 then\n\
12958                     return 1\n\
12959                 elseif x < 0 then\n\
12960                     return -1\n\
12961                 else\n\
12962                     return 0\n\
12963                 end\n\
12964             end",
12965            "foo.lua",
12966            |metric| {
12967                // Comparisons: `>`, `<` → 2; elseif_statement → 1;
12968                // else_statement → 1. C = 4. No branches (no calls).
12969                assert_eq!(metric.abc.assignments_sum(), 0);
12970                assert_eq!(metric.abc.branches_sum(), 0);
12971                assert_eq!(metric.abc.conditions_sum(), 4);
12972                insta::assert_json_snapshot!(metric.abc);
12973            },
12974        );
12975    }
12976
12977    #[cfg(feature = "lua")]
12978    #[test]
12979    fn lua_complex_function_abc() {
12980        // Combines every category to pin the metric.
12981        check_metrics::<LuaParser>(
12982            "function run(n)\n\
12983                 local total = 0\n\
12984                 for i = 1, n do\n\
12985                     if i % 2 == 0 then\n\
12986                         do_work(i)\n\
12987                     else\n\
12988                         total = total + i\n\
12989                     end\n\
12990                 end\n\
12991                 print(\"done\")\n\
12992                 return total\n\
12993             end",
12994            "foo.lua",
12995            |metric| {
12996                // Assignments: `local total = 0` (1), `total = total + i` (1) → 2.
12997                // Branches: `do_work(i)` (1), `print(\"done\")` (1) → 2.
12998                // Conditions: `==` (1), `else_statement` (1) → 2.
12999                assert_eq!(metric.abc.assignments_sum(), 2);
13000                assert_eq!(metric.abc.branches_sum(), 2);
13001                assert_eq!(metric.abc.conditions_sum(), 2);
13002                insta::assert_json_snapshot!(metric.abc);
13003            },
13004        );
13005    }
13006
13007    #[cfg(feature = "lua")]
13008    #[test]
13009    fn lua_if_multiple_conditions() {
13010        // Fitzpatrick Rule 9 walker (issue #403). Lua's `and` / `or`
13011        // are keyword tokens inside a `binary_expression`.
13012        check_metrics::<LuaParser>(
13013            "function f(a, b, c, d)\n\
13014                 if a or b or c or d then return 1 end       -- +4c\n\
13015                 if a and b and c then return 2 end          -- +3c\n\
13016                 if not a and not b then return 3 end        -- +2c\n\
13017                 return 0\n\
13018             end",
13019            "foo.lua",
13020            |metric| {
13021                assert_eq!(metric.abc.conditions_sum(), 9);
13022                insta::assert_json_snapshot!(metric.abc);
13023            },
13024        );
13025    }
13026
13027    #[cfg(feature = "lua")]
13028    #[test]
13029    fn lua_while_conditions() {
13030        // Lua has no `do { ... } while(cond);` — `while cond do …
13031        // end` and `repeat … until cond` are the loop forms.
13032        check_metrics::<LuaParser>(
13033            "function f(a, b)\n\
13034                 while a or b do break end                   -- +2c\n\
13035                 repeat break until a and not b              -- +2c\n\
13036             end",
13037            "foo.lua",
13038            |metric| {
13039                assert_eq!(metric.abc.conditions_sum(), 4);
13040                insta::assert_json_snapshot!(metric.abc);
13041            },
13042        );
13043    }
13044
13045    #[cfg(feature = "lua")]
13046    #[test]
13047    fn lua_short_circuit_with_boolean_literal_operand() {
13048        // `a and true` reports 2 conditions: one Identifier, one
13049        // True keyword literal.
13050        check_metrics::<LuaParser>("function f(a) return a and true end", "foo.lua", |metric| {
13051            assert_eq!(metric.abc.conditions_sum(), 2);
13052            insta::assert_json_snapshot!(metric.abc);
13053        });
13054    }
13055
13056    #[cfg(feature = "lua")]
13057    #[test]
13058    fn lua_number_truthy_condition_counts() {
13059        // Regression for findings.md #2: Lua treats every non-nil,
13060        // non-false value as truthy, so `if 1 then ... end` and
13061        // `return a and 2` should each count their numeric literal
13062        // as a Fitzpatrick Rule 6 / 7 unary condition. Pre-fix,
13063        // `lua_bool_terminal_kinds!()` listed `True` / `False` /
13064        // `Nil` but omitted `Number`, so the walker dropped every
13065        // numeric-truthy operand. The walker comment at the top of
13066        // `lua_inspect_container` already promised numbers were
13067        // terminal-bool kinds; this commit closes the gap.
13068        check_metrics::<LuaParser>(
13069            "function f(a)\n\
13070             \x20   if 1 then return 1 end\n\
13071             \x20   return a and 2\n\
13072             end",
13073            "foo.lua",
13074            |metric| {
13075                // `if 1 then` → walker counts the Number literal (+1).
13076                // `a and 2` → `and` walker counts both operands:
13077                //   identifier `a` (+1), Number `2` (+1).
13078                // Total: 3.
13079                assert_eq!(metric.abc.conditions_sum(), 3);
13080                insta::assert_json_snapshot!(metric.abc);
13081            },
13082        );
13083    }
13084
13085    #[cfg(feature = "lua")]
13086    #[test]
13087    fn lua_if_boolean_literal_condition() {
13088        check_metrics::<LuaParser>(
13089            "function f()\n\
13090                 if true then end                  -- +1c\n\
13091                 if not false then end             -- +1c\n\
13092                 while true do break end           -- +1c\n\
13093                 repeat break until false          -- +1c\n\
13094             end",
13095            "foo.lua",
13096            |metric| {
13097                assert_eq!(metric.abc.conditions_sum(), 4);
13098                insta::assert_json_snapshot!(metric.abc);
13099            },
13100        );
13101    }
13102
13103    #[cfg(feature = "lua")]
13104    #[test]
13105    fn lua_methods_arguments_with_conditions() {
13106        // `m(not a, not b)` — argument list walker counts each
13107        // unary-conditional argument once. Bare-identifier args
13108        // (`m(a, b)`) do not count (list_kind != BinaryExpression).
13109        check_metrics::<LuaParser>(
13110            "function f(a, b) m(a, b); m(not a, not b) end",
13111            "foo.lua",
13112            |metric| {
13113                assert_eq!(metric.abc.branches_sum(), 2);
13114                assert_eq!(metric.abc.conditions_sum(), 2);
13115                insta::assert_json_snapshot!(metric.abc);
13116            },
13117        );
13118    }
13119
13120    #[cfg(feature = "lua")]
13121    #[test]
13122    fn lua_return_with_conditions() {
13123        // `return not (z >= 0)` → walker on `not` unwraps the paren
13124        // chain and reaches the inner BinaryExpression; the inner
13125        // `>=` comparison is the actual Fitzpatrick condition.
13126        check_metrics::<LuaParser>(
13127            "function m1(z) return not (z >= 0) end\n\
13128             function m2(x) return (((not x))) end\n\
13129             function m3(x, y) return x and y end",
13130            "foo.lua",
13131            |metric| {
13132                // m1: `>=` (1). `not` wraps a paren'd
13133                //     BinaryExpression — Lua's lua_inspect_container
13134                //     reaches the inner BinaryExpression and stops,
13135                //     no walker count. +1.
13136                // m2: ReturnStatement → iterate expression_list →
13137                //     inspect_container on the outermost paren →
13138                //     unwraps to `x` in has_boolean_content-true
13139                //     (seeded by the `not`). +1.
13140                // m3: x and y → `and` walker counts both → +2.
13141                // Sum: 4.
13142                assert_eq!(metric.abc.conditions_sum(), 4);
13143                insta::assert_json_snapshot!(metric.abc);
13144            },
13145        );
13146    }
13147
13148    // ---------- Tcl ABC tests ----------
13149
13150    #[cfg(feature = "tcl")]
13151    #[test]
13152    fn tcl_empty_unit_zero() {
13153        check_metrics::<TclParser>("", "empty.tcl", |metric| {
13154            assert_eq!(metric.abc.assignments_sum(), 0);
13155            assert_eq!(metric.abc.branches_sum(), 0);
13156            assert_eq!(metric.abc.conditions_sum(), 0);
13157            insta::assert_json_snapshot!(metric.abc);
13158        });
13159    }
13160
13161    #[cfg(feature = "tcl")]
13162    #[test]
13163    fn tcl_set_command_counts_assignment() {
13164        // `set` has its own grammar production; each invocation is
13165        // one assignment.
13166        check_metrics::<TclParser>(
13167            "proc f {} {\n\
13168                 set x 1\n\
13169                 set y 2\n\
13170                 set x [expr {$x + $y}]\n\
13171             }",
13172            "foo.tcl",
13173            |metric| {
13174                // 3 `set` invocations → A=3. The inner `expr` is a
13175                // sub-command (`command_substitution` + `expr_cmd`),
13176                // not a `command` node, so it doesn't add a branch.
13177                assert_eq!(metric.abc.assignments_sum(), 3);
13178                assert_eq!(metric.abc.branches_sum(), 0);
13179                assert_eq!(metric.abc.conditions_sum(), 0);
13180                insta::assert_json_snapshot!(metric.abc);
13181            },
13182        );
13183    }
13184
13185    #[cfg(feature = "tcl")]
13186    #[test]
13187    fn tcl_incr_append_lappend_count_assignment() {
13188        // Variable-mutation commands (`incr`, `append`, `lappend`)
13189        // are recognised by name and count as assignments, not
13190        // branches.
13191        check_metrics::<TclParser>(
13192            "proc f {} {\n\
13193                 set x 0\n\
13194                 incr x\n\
13195                 append s \"hi\"\n\
13196                 lappend lst 1\n\
13197             }",
13198            "foo.tcl",
13199            |metric| {
13200                // `set` (1) + `incr` (1) + `append` (1) + `lappend`
13201                // (1) → A=4. No branches, no conditions.
13202                assert_eq!(metric.abc.assignments_sum(), 4);
13203                assert_eq!(metric.abc.branches_sum(), 0);
13204                assert_eq!(metric.abc.conditions_sum(), 0);
13205                insta::assert_json_snapshot!(metric.abc);
13206            },
13207        );
13208    }
13209
13210    #[cfg(feature = "tcl")]
13211    #[test]
13212    fn tcl_qualified_mutator_commands_count_assignment() {
13213        // `::incr` is `incr` through the global namespace. Anchored on
13214        // `branches_sum() == 0` as well as the assignment total: an
13215        // unresolved name falls to the branch arm, so the two columns
13216        // move in opposite directions and no single total can mask the
13217        // regression (#1381 review).
13218        check_metrics::<TclParser>(
13219            "proc f {} {\n\
13220                 ::incr x\n\
13221                 ::append s \"hi\"\n\
13222                 ::lappend lst 1\n\
13223             }",
13224            "foo.tcl",
13225            |metric| {
13226                assert_eq!(metric.abc.assignments_sum(), 3);
13227                assert_eq!(metric.abc.branches_sum(), 0);
13228                assert_eq!(metric.abc.conditions_sum(), 0);
13229            },
13230        );
13231    }
13232
13233    #[cfg(feature = "tcl")]
13234    #[test]
13235    fn tcl_namespaced_mutator_command_stays_a_branch() {
13236        // Control: only the *leading* qualifier names the core command,
13237        // so `ns::incr` is a user proc and keeps the branch column — the
13238        // direction `BRACED_WORD_VALUE_CASES` pins for `ns::eval`.
13239        check_metrics::<TclParser>(
13240            "proc f {} {\n\
13241                 ns::incr x\n\
13242             }",
13243            "foo.tcl",
13244            |metric| {
13245                assert_eq!(metric.abc.assignments_sum(), 0);
13246                assert_eq!(metric.abc.branches_sum(), 1);
13247            },
13248        );
13249    }
13250
13251    #[cfg(feature = "tcl")]
13252    #[test]
13253    fn tcl_computed_command_name_is_not_an_assignment() {
13254        // A command whose leading word is computed (`$cmd args`) names no
13255        // builtin the parser can resolve, so it stays a branch. Pins the
13256        // field-addressed, `simple_word`-gated read the classifier shares
13257        // with the Cognitive / Cyclomatic detectors (grammar-dispatch §3):
13258        // the earlier `child(0)` byte-slice addressed the slot by position
13259        // and compared whatever literal text sat there, which agrees with
13260        // the gated read on today's grammar only because no mutator name is
13261        // spelled with a leading `$`. A grammar that moved the name out of
13262        // `child(0)`, or a mutator list that grew a computed-looking entry,
13263        // would diverge — this fixture is where that surfaces.
13264        check_metrics::<TclParser>(
13265            "proc f {cmd x} {\n\
13266                 $cmd $x\n\
13267                 incr x\n\
13268             }",
13269            "foo.tcl",
13270            |metric| {
13271                // `incr x` is the only assignment; `$cmd $x` is a branch.
13272                assert_eq!(metric.abc.assignments_sum(), 1);
13273                assert_eq!(metric.abc.branches_sum(), 1);
13274                assert_eq!(metric.abc.conditions_sum(), 0);
13275            },
13276        );
13277    }
13278
13279    #[cfg(feature = "tcl")]
13280    #[test]
13281    fn tcl_generic_commands_are_branches() {
13282        // Anything that isn't `set` or a known mutator command
13283        // counts as a branch — including builtins like `puts` and
13284        // `return`.
13285        check_metrics::<TclParser>(
13286            "proc f {} {\n\
13287                 puts \"hello\"\n\
13288                 do_work 1 2\n\
13289                 return 0\n\
13290             }",
13291            "foo.tcl",
13292            |metric| {
13293                // 3 commands, all branches.
13294                assert_eq!(metric.abc.assignments_sum(), 0);
13295                assert_eq!(metric.abc.branches_sum(), 3);
13296                assert_eq!(metric.abc.conditions_sum(), 0);
13297                insta::assert_json_snapshot!(metric.abc);
13298            },
13299        );
13300    }
13301
13302    #[cfg(feature = "tcl")]
13303    #[test]
13304    fn tcl_comparisons_count_logical_ops_do_not() {
13305        // `expr` predicates expose comparison / logical tokens at
13306        // the leaf level. Each comparison token contributes one
13307        // condition; `&&` and `||` are NOT counted on their own
13308        // (Fitzpatrick Rule 5; #395) — instead each operand is
13309        // counted as a unary conditional by the walker (Rule 9;
13310        // #403). The two logical lines add 2 walker conditions
13311        // each (variable-substitution operands).
13312        check_metrics::<TclParser>(
13313            "proc f {a b} {\n\
13314                 set r [expr {$a == $b}]\n\
13315                 set r [expr {$a != $b}]\n\
13316                 set r [expr {$a <  $b}]\n\
13317                 set r [expr {$a >  $b}]\n\
13318                 set r [expr {$a <= $b}]\n\
13319                 set r [expr {$a >= $b}]\n\
13320                 set r [expr {$a eq $b}]\n\
13321                 set r [expr {$a ne $b}]\n\
13322                 set r [expr {$a && $b}]\n\
13323                 set r [expr {$a || $b}]\n\
13324             }",
13325            "foo.tcl",
13326            |metric| {
13327                // 10 `set` assignments.
13328                assert_eq!(metric.abc.assignments_sum(), 10);
13329                assert_eq!(metric.abc.branches_sum(), 0);
13330                // 8 comparisons (+8) + 2 logical lines × 2 walker
13331                // operands (+4) = 12.
13332                assert_eq!(metric.abc.conditions_sum(), 12);
13333                insta::assert_json_snapshot!(metric.abc);
13334            },
13335        );
13336    }
13337
13338    #[cfg(feature = "tcl")]
13339    #[test]
13340    fn tcl_ternary_counts_condition() {
13341        // The `ternary_expr` node is one condition and its condition
13342        // slot `$a` — a bare truthy test — is another, matching C++'s
13343        // `int r = a ? b : c;` (also 2). The two branch operands are
13344        // unnegated and so contribute nothing (#1180).
13345        check_metrics::<TclParser>(
13346            "proc f {a b c} {\n\
13347                 set r [expr {$a ? $b : $c}]\n\
13348             }",
13349            "foo.tcl",
13350            |metric| {
13351                assert_eq!(metric.abc.assignments_sum(), 1);
13352                assert_eq!(metric.abc.branches_sum(), 0);
13353                assert_eq!(metric.abc.conditions_sum(), 2);
13354                insta::assert_json_snapshot!(metric.abc);
13355            },
13356        );
13357    }
13358
13359    /// The Tcl half of the ternary slot-location guard (#1180).
13360    ///
13361    /// `ternary_expr` exposes no grammar fields, so the slots are found
13362    /// relative to the `?` and `:` tokens. A *parenthesised* condition is
13363    /// the input that discriminates that from a fixed-index reading:
13364    /// `_expr` inlines `( … )` as anonymous children of `ternary_expr`,
13365    /// so `($a) ? !$b : !$c` shifts every operand right by one and
13366    /// `child(0)` / `child(2)` / `child(4)` land on `(`, `)` and `?`.
13367    /// Without this case the whole fixed-index revert passes.
13368    #[cfg(feature = "tcl")]
13369    #[test]
13370    fn tcl_parenthesised_ternary_condition_matches_the_bare_form() {
13371        let conditions = |source: &str| {
13372            crate::test_support::metrics_verbatim(
13373                crate::LANG::Tcl,
13374                source.as_bytes(),
13375                crate::MetricsOptions::default(),
13376            )
13377            .abc
13378            .conditions_sum()
13379        };
13380        let bare = conditions("proc f {a b c} {\n set r [expr {$a ? !$b : !$c}]\n}");
13381        assert_eq!(bare, 4, "the bare form is the documented reference value");
13382        assert_eq!(
13383            conditions("proc f {a b c} {\n set r [expr {($a) ? !$b : !$c}]\n}"),
13384            bare,
13385            "parenthesising the condition must not change the count"
13386        );
13387    }
13388
13389    /// A parenthesised operand under `!` counts the same as a bare one.
13390    ///
13391    /// `_expr` inlines `( … )` as anonymous children, so `!($a)` puts
13392    /// `(` where a positional read expects the operand. The walker's
13393    /// negation branch kept a fixed `child(1)` through the first draft of
13394    /// #1180 and scored these 0 while the unparenthesised forms scored 1
13395    /// — an inconsistency the fix itself introduced, since before it
13396    /// neither form counted. Found in review, not by the tests: the
13397    /// parenthesised fixtures added with #1180 covered the ternary
13398    /// *condition* slot only.
13399    #[cfg(feature = "tcl")]
13400    #[test]
13401    fn tcl_parenthesised_negated_operands_match_the_bare_form() {
13402        let conditions = |source: &str| {
13403            crate::test_support::metrics_verbatim(
13404                crate::LANG::Tcl,
13405                source.as_bytes(),
13406                crate::MetricsOptions::default(),
13407            )
13408            .abc
13409            .conditions_sum()
13410        };
13411        for (bare, parenthesised) in [
13412            (
13413                "proc f {a} {\n if {!$a} { puts x }\n}",
13414                "proc f {a} {\n if {!($a)} { puts x }\n}",
13415            ),
13416            (
13417                "proc f {a b} {\n if {$a && !$b} { puts x }\n}",
13418                "proc f {a b} {\n if {$a && !($b)} { puts x }\n}",
13419            ),
13420            (
13421                "proc f {a b c} {\n set r [expr {$a ? !$b : !$c}]\n}",
13422                "proc f {a b c} {\n set r [expr {$a ? !($b) : !$c}]\n}",
13423            ),
13424        ] {
13425            let want = conditions(bare);
13426            assert!(want > 0, "the bare form must count something: {bare}");
13427            assert_eq!(
13428                conditions(parenthesised),
13429                want,
13430                "parenthesising the negated operand changed the count\n  bare:  {bare}\n  paren: {parenthesised}"
13431            );
13432        }
13433    }
13434
13435    #[cfg(feature = "irules")]
13436    #[test]
13437    fn irules_abc_parenthesised_negated_operands_match_the_bare_form() {
13438        let conditions = |source: &str| {
13439            crate::test_support::metrics_verbatim(
13440                crate::LANG::Irules,
13441                source.as_bytes(),
13442                crate::MetricsOptions::default(),
13443            )
13444            .abc
13445            .conditions_sum()
13446        };
13447        let want = conditions("when X {\n    if { !$a } { log local0. hi }\n}\n");
13448        assert_eq!(want, 1);
13449        assert_eq!(
13450            conditions("when X {\n    if { !($a) } { log local0. hi }\n}\n"),
13451            want
13452        );
13453    }
13454
13455    #[cfg(feature = "tcl")]
13456    #[test]
13457    fn tcl_bare_truthy_and_negated_predicates_count_one_condition() {
13458        // The headline #1180 fix, on the Tcl side: both were 0 before.
13459        let conditions = |source: &str| {
13460            crate::test_support::metrics_verbatim(
13461                crate::LANG::Tcl,
13462                source.as_bytes(),
13463                crate::MetricsOptions::default(),
13464            )
13465            .abc
13466            .conditions_sum()
13467        };
13468        assert_eq!(conditions("proc f {a} {\n if {$a} { puts x }\n}"), 1);
13469        assert_eq!(conditions("proc f {a} {\n if {!$a} { puts x }\n}"), 1);
13470        assert_eq!(conditions("proc f {a} {\n while {$a} { puts x }\n}"), 1);
13471        assert_eq!(conditions("proc f {a} {\n while {!$a} { puts x }\n}"), 1);
13472    }
13473
13474    #[cfg(feature = "tcl")]
13475    #[test]
13476    fn tcl_bare_truthy_elseif_predicate_counts_one_condition() {
13477        // The `Tcl::Elseif` arm routes its predicate through
13478        // `tcl_condition_expr` exactly as `If` / `While` do (#1180), but
13479        // a comparison predicate is counted by its leaf operator, so it
13480        // cannot tell the routing from its absence. Only a bare truthy
13481        // predicate can: `$b` scores through the routed walker or not at
13482        // all.
13483        let conditions = |source: &str| {
13484            crate::test_support::metrics_verbatim(
13485                crate::LANG::Tcl,
13486                source.as_bytes(),
13487                crate::MetricsOptions::default(),
13488            )
13489            .abc
13490            .conditions_sum()
13491        };
13492        // `$a` truthy (1) + `elseif` clause (1) + `$b` truthy (1).
13493        assert_eq!(
13494            conditions("proc f {a b} {\n if {$a} { puts x } elseif {$b} { puts y }\n}"),
13495            3
13496        );
13497    }
13498
13499    #[cfg(feature = "tcl")]
13500    #[test]
13501    fn tcl_elseif_and_else_count_conditions() {
13502        // `if` / `elseif` / `else` clause productions each
13503        // contribute one condition. The leaf comparison inside the
13504        // predicate is counted independently.
13505        check_metrics::<TclParser>(
13506            "proc f {x} {\n\
13507                 if {$x > 0} {\n\
13508                     return 1\n\
13509                 } elseif {$x < 0} {\n\
13510                     return -1\n\
13511                 } else {\n\
13512                     return 0\n\
13513                 }\n\
13514             }",
13515            "foo.tcl",
13516            |metric| {
13517                // Branches: three `return` commands → 3.
13518                // Conditions: `>` (1), `<` (1), `elseif` (1), `else`
13519                // (1) → 4.
13520                assert_eq!(metric.abc.assignments_sum(), 0);
13521                assert_eq!(metric.abc.branches_sum(), 3);
13522                assert_eq!(metric.abc.conditions_sum(), 4);
13523                insta::assert_json_snapshot!(metric.abc);
13524            },
13525        );
13526    }
13527
13528    #[cfg(feature = "tcl")]
13529    #[test]
13530    fn tcl_if_multiple_conditions() {
13531        // Fitzpatrick Rule 9 walker (issue #403). Tcl's `expr` slot
13532        // exposes `&&` / `||` operands as variable substitutions
13533        // (`$a`, `$b`, …) inside a `binop_expr`.
13534        check_metrics::<TclParser>(
13535            "proc f {a b c d} {\n\
13536                 if {[expr {$a || $b || $c || $d}]} { return 1 }    \n\
13537                 if {[expr {$a && $b && $c}]} { return 2 }          \n\
13538                 return 0\n\
13539             }",
13540            "foo.tcl",
13541            |metric| {
13542                // The two chains feed the walker: 4 + 3 = 7. Each `if`
13543                // predicate is additionally a bare truthy test of a
13544                // command substitution — `{[expr {…}]}` is structurally
13545                // `if {[somecmd]}`, which counts 1 exactly as `if {$a}`
13546                // does — so 7 + 2 = 9 (#1180). Written without the
13547                // redundant `[expr …]` wrapper, `if {$a || $b}` scores
13548                // 2, matching C++'s `if (a || b)`.
13549                assert_eq!(metric.abc.conditions_sum(), 9);
13550                insta::assert_json_snapshot!(metric.abc);
13551            },
13552        );
13553    }
13554
13555    #[cfg(feature = "tcl")]
13556    #[test]
13557    fn tcl_while_conditions() {
13558        // Tcl has no `do { ... } while(cond);` — `while {…} {…}` is
13559        // the standard loop. The walker fires on `&&` / `||` tokens
13560        // inside the `expr` predicate.
13561        check_metrics::<TclParser>(
13562            "proc f {a b} {\n\
13563                 while {[expr {$a || $b}]} { break }    \n\
13564                 while {[expr {$a && $b}]} { break }    \n\
13565             }",
13566            "foo.tcl",
13567            |metric| {
13568                // 2 + 2 from the chains, plus one bare truthy test per
13569                // `while` predicate — see `tcl_if_multiple_conditions`
13570                // (#1180).
13571                assert_eq!(metric.abc.conditions_sum(), 6);
13572                insta::assert_json_snapshot!(metric.abc);
13573            },
13574        );
13575    }
13576
13577    #[cfg(feature = "tcl")]
13578    #[test]
13579    fn tcl_short_circuit_with_boolean_literal_operand() {
13580        // `$a && 1` reports 2 conditions: a VariableSubstitution
13581        // operand plus a Number-literal operand. Confirms `Number`
13582        // is in the walker terminal set. `true` / `false` Tcl
13583        // keywords are not literal tokens in tree-sitter-tcl —
13584        // they're emitted as the operator-context word, which is
13585        // captured separately by the `Tcl::Boolean` kind for
13586        // dedicated `expr {true}` predicates but not as a `&&`
13587        // operand at this iteration; using a numeric literal keeps
13588        // the assertion grammar-stable.
13589        check_metrics::<TclParser>(
13590            "proc f {a} { return [expr {$a && 1}] }\n",
13591            "foo.tcl",
13592            |metric| {
13593                assert_eq!(metric.abc.conditions_sum(), 2);
13594                insta::assert_json_snapshot!(metric.abc);
13595            },
13596        );
13597    }
13598
13599    #[cfg(feature = "tcl")]
13600    #[test]
13601    fn tcl_complex_function_abc() {
13602        // Mixed program covering every category. Tcl's grammar
13603        // re-parses braced content that looks command-shaped as a
13604        // nested `command` node, which inflates the branch count
13605        // relative to a naive read of the source — see breakdown.
13606        check_metrics::<TclParser>(
13607            "proc run {n} {\n\
13608                 set total 0\n\
13609                 for {set i 0} {$i < $n} {incr i} {\n\
13610                     if {$i % 2 == 0} {\n\
13611                         do_work $i\n\
13612                     } else {\n\
13613                         incr total $i\n\
13614                     }\n\
13615                 }\n\
13616                 puts \"done\"\n\
13617                 return $total\n\
13618             }",
13619            "foo.tcl",
13620            |metric| {
13621                // Assignments: `set total 0` (1), `set i 0` (1),
13622                // `incr i` (1), `incr total $i` (1) → A = 4.
13623                // Branches: the outer `for …` is one `command` node;
13624                // the `{$i < $n}` predicate ALSO re-parses as a
13625                // `command` node (tree-sitter-tcl treats braced
13626                // predicates as nested commands at the pinned
13627                // grammar version); plus `do_work $i`, `puts
13628                // "done"`, and `return $total`. The for-loop body's
13629                // `incr` and `incr total $i` are assignment commands
13630                // and don't add branches. Total B = 5.
13631                // Conditions: `==` (1) and `else` (1) → C = 2. The
13632                // `<` inside `{$i < $n}` is NOT `Tcl::LT`: because
13633                // that predicate re-parses as a `command`, the `<`
13634                // is emitted as `simple_word`. Only `<` inside a
13635                // real `expr` production becomes `Tcl::LT`.
13636                assert_eq!(metric.abc.assignments_sum(), 4);
13637                assert_eq!(metric.abc.branches_sum(), 5);
13638                assert_eq!(metric.abc.conditions_sum(), 2);
13639                insta::assert_json_snapshot!(metric.abc);
13640            },
13641        );
13642    }
13643
13644    /// The dedicated `set name value` production counts as one assignment.
13645    #[cfg(feature = "irules")]
13646    #[test]
13647    fn irules_abc_set_assignment() {
13648        check_metrics::<IrulesParser>("when X {\n    set x 1\n}\n", "foo.irule", |metric| {
13649            assert_eq!(metric.abc.assignments_sum(), 1);
13650            assert_eq!(metric.abc.branches_sum(), 0);
13651            assert_eq!(metric.abc.conditions_sum(), 0);
13652        });
13653    }
13654
13655    /// Mutator commands (`incr` / `append` / `lappend`) count as
13656    /// assignments, not branches — iRules has no assignment operators, so
13657    /// mutation is always a command invocation.
13658    #[cfg(feature = "irules")]
13659    #[test]
13660    fn irules_abc_mutator_commands() {
13661        check_metrics::<IrulesParser>(
13662            "when X {\n    incr x\n    append s \"y\"\n    lappend l 1\n}\n",
13663            "foo.irule",
13664            |metric| {
13665                assert_eq!(metric.abc.assignments_sum(), 3);
13666                assert_eq!(metric.abc.branches_sum(), 0);
13667            },
13668        );
13669    }
13670
13671    /// `::incr` is `incr` through the global namespace. iRules resolves
13672    /// the leading word in `irules_command_is_assignment` rather than
13673    /// through `tcl_command_name`, so this pins the strip on that second
13674    /// path (#1381 review).
13675    #[cfg(feature = "irules")]
13676    #[test]
13677    fn irules_abc_qualified_mutator_commands() {
13678        check_metrics::<IrulesParser>(
13679            "when X {\n    ::incr x\n    ::append s \"y\"\n    ::lappend l 1\n}\n",
13680            "foo.irule",
13681            |metric| {
13682                assert_eq!(metric.abc.assignments_sum(), 3);
13683                assert_eq!(metric.abc.branches_sum(), 0);
13684            },
13685        );
13686    }
13687
13688    /// Control: `ns::incr` is a proc in `ns`, so it stays a branch.
13689    #[cfg(feature = "irules")]
13690    #[test]
13691    fn irules_abc_namespaced_mutator_command_stays_a_branch() {
13692        check_metrics::<IrulesParser>("when X {\n    ns::incr x\n}\n", "foo.irule", |metric| {
13693            assert_eq!(metric.abc.assignments_sum(), 0);
13694            assert_eq!(metric.abc.branches_sum(), 1);
13695        });
13696    }
13697
13698    /// Generic (non-mutator) commands count as branches.
13699    #[cfg(feature = "irules")]
13700    #[test]
13701    fn irules_abc_branch_commands() {
13702        check_metrics::<IrulesParser>(
13703            "when X {\n    log local0. hi\n    pool p1\n}\n",
13704            "foo.irule",
13705            |metric| {
13706                assert_eq!(metric.abc.assignments_sum(), 0);
13707                assert_eq!(metric.abc.branches_sum(), 2);
13708                assert_eq!(metric.abc.conditions_sum(), 0);
13709            },
13710        );
13711    }
13712
13713    /// A numeric comparison (`==`) is one condition; the `log` inside the
13714    /// `if` body is one branch.
13715    #[cfg(feature = "irules")]
13716    #[test]
13717    fn irules_abc_comparison_condition() {
13718        check_metrics::<IrulesParser>(
13719            "when X {\n    if { $a == 1 } { log local0. hi }\n}\n",
13720            "foo.irule",
13721            |metric| {
13722                assert_eq!(metric.abc.branches_sum(), 1);
13723                assert_eq!(metric.abc.conditions_sum(), 1);
13724            },
13725        );
13726    }
13727
13728    /// A word-form string comparator (`contains`) is a condition just like
13729    /// `==` — iRules-specific (Tcl has only `eq`/`ne`/`in`/`ni`). If
13730    /// `contains` were dropped from the condition set this would report 0.
13731    #[cfg(feature = "irules")]
13732    #[test]
13733    fn irules_abc_string_op_condition() {
13734        check_metrics::<IrulesParser>(
13735            "when X {\n    if { $a contains \"x\" } { log local0. hi }\n}\n",
13736            "foo.irule",
13737            |metric| {
13738                assert_eq!(metric.abc.branches_sum(), 1);
13739                assert_eq!(metric.abc.conditions_sum(), 1);
13740            },
13741        );
13742    }
13743
13744    /// Each `elseif` / `else` clause is one condition; the three `set`s are
13745    /// assignments. The leading `if` is not itself a condition.
13746    #[cfg(feature = "irules")]
13747    #[test]
13748    fn irules_abc_elseif_else_conditions() {
13749        check_metrics::<IrulesParser>(
13750            "when X {\n    if { $a } { set r 1 } elseif { $b } { set r 2 } else { set r 3 }\n}\n",
13751            "foo.irule",
13752            |metric| {
13753                assert_eq!(metric.abc.assignments_sum(), 3);
13754                // The `elseif` and `else` clauses are one condition each,
13755                // as before; #1180 adds the two bare truthy predicates
13756                // (`{ $a }`, `{ $b }`). C++'s
13757                // `if(a){} else if(b){} else {}` also scores 4.
13758                assert_eq!(metric.abc.conditions_sum(), 4);
13759            },
13760        );
13761    }
13762
13763    /// A ternary contributes its own condition plus the `>` comparison in
13764    /// its test: conditions 2; the `set` is one assignment.
13765    #[cfg(feature = "irules")]
13766    #[test]
13767    fn irules_abc_ternary_condition() {
13768        check_metrics::<IrulesParser>(
13769            "when X {\n    set y [expr { $a > 0 ? 1 : 0 }]\n}\n",
13770            "foo.irule",
13771            |metric| {
13772                assert_eq!(metric.abc.assignments_sum(), 1);
13773                assert_eq!(metric.abc.conditions_sum(), 2);
13774            },
13775        );
13776    }
13777
13778    /// Fitzpatrick Rule 9: the short-circuit `&&` is not itself a condition,
13779    /// but each negated bare operand (`!$a`, `!$b`) in the chain is. Guards
13780    /// the `irules_count_unary_conditions` / `irules_inspect_container`
13781    /// walker — conditions 2.
13782    #[cfg(feature = "irules")]
13783    #[test]
13784    fn irules_abc_negated_operands_in_chain() {
13785        check_metrics::<IrulesParser>(
13786            "when X {\n    if { !$a && !$b } { log local0. hi }\n}\n",
13787            "foo.irule",
13788            |metric| {
13789                assert_eq!(metric.abc.branches_sum(), 1);
13790                assert_eq!(metric.abc.conditions_sum(), 2);
13791            },
13792        );
13793    }
13794
13795    /// A bare-truthy `if {$a}` predicate is one condition (#1180).
13796    ///
13797    /// It carries no comparison, ternary or short-circuit operator, so
13798    /// before the Phase 2B slot routing landed nothing invoked the
13799    /// unary-conditional walker and the whole predicate scored 0 — this
13800    /// test previously pinned that absence, and the metrics book's ABC
13801    /// deviation table said so. Now the `if` node routes its `expr`
13802    /// predicate and the count matches C++'s `if (a)`, which is also 1.
13803    /// The `log` command remains the single branch.
13804    #[cfg(feature = "irules")]
13805    #[test]
13806    fn irules_abc_bare_truthy_counts_one_condition() {
13807        check_metrics::<IrulesParser>(
13808            "when X {\n    if { $a } { log local0. hi }\n}\n",
13809            "foo.irule",
13810            |metric| {
13811                assert_eq!(metric.abc.branches_sum(), 1);
13812                assert_eq!(metric.abc.conditions_sum(), 1);
13813            },
13814        );
13815    }
13816
13817    /// The negated form of the same predicate, which was *also* 0 before
13818    /// #1180: `!$a` reached the walker but no parent seeded boolean
13819    /// context, so the terminal operand was never counted. Distinct from
13820    /// `irules_abc_negated_operands_in_chain`, whose `&&` supplied the
13821    /// seed the bare form lacked.
13822    #[cfg(feature = "irules")]
13823    #[test]
13824    fn irules_abc_negated_bare_truthy_counts_one_condition() {
13825        check_metrics::<IrulesParser>(
13826            "when X {\n    if { !$a } { log local0. hi }\n}\n",
13827            "foo.irule",
13828            |metric| {
13829                assert_eq!(metric.abc.branches_sum(), 1);
13830                assert_eq!(metric.abc.conditions_sum(), 1);
13831            },
13832        );
13833    }
13834
13835    /// The ternary's three operand slots, located relative to the `?`
13836    /// and `:` tokens because the grammar exposes no fields (#1180).
13837    ///
13838    /// `$a ? !$b : !$c` is four: the `ternary_expr` node, the bare
13839    /// truthy condition, and one per negated branch — the same value
13840    /// Java, C#, Groovy, the C family, the JS family, PHP, Perl, Ruby
13841    /// and Python report for the identical expression.
13842    #[cfg(feature = "irules")]
13843    #[test]
13844    fn irules_abc_ternary_routes_its_operand_slots() {
13845        check_metrics::<IrulesParser>(
13846            "when X {\n    set y [expr { $a ? !$b : !$c }]\n}\n",
13847            "foo.irule",
13848            |metric| {
13849                assert_eq!(metric.abc.conditions_sum(), 4);
13850            },
13851        );
13852    }
13853
13854    /// The #1161 control: a ternary whose condition is a *comparison*
13855    /// must not move. The `>` already supplied its condition and the
13856    /// branches are unnegated, so routing the slots adds nothing.
13857    #[cfg(feature = "irules")]
13858    #[test]
13859    fn irules_abc_comparison_ternary_is_unchanged_by_slot_routing() {
13860        check_metrics::<IrulesParser>(
13861            "when X {\n    set y [expr { $a > 0 ? 1 : 0 }]\n}\n",
13862            "foo.irule",
13863            |metric| {
13864                assert_eq!(metric.abc.conditions_sum(), 2);
13865            },
13866        );
13867    }
13868
13869    /// A parenthesised ternary condition scores the same as a bare one.
13870    ///
13871    /// The grammar inlines `( … )` as anonymous children of
13872    /// `ternary_expr` rather than wrapping them in a node, so a
13873    /// fixed-index reading of the slots would shift right by one and
13874    /// mis-assign every operand. This is the input that discriminates
13875    /// the token-relative location the fix uses.
13876    #[cfg(feature = "irules")]
13877    #[test]
13878    fn irules_abc_parenthesised_ternary_condition_matches_the_bare_form() {
13879        // `check_metrics` takes a bare `fn`, so it cannot carry the
13880        // first measurement into the second comparison; `metrics_verbatim`
13881        // returns a value instead.
13882        let conditions = |source: &str| {
13883            crate::test_support::metrics_verbatim(
13884                crate::LANG::Irules,
13885                source.as_bytes(),
13886                crate::MetricsOptions::default(),
13887            )
13888            .abc
13889            .conditions_sum()
13890        };
13891        let bare = conditions("when X {\n    set y [expr { $a ? !$b : !$c }]\n}\n");
13892        assert_eq!(bare, 4, "the bare form is the documented reference value");
13893        assert_eq!(
13894            conditions("when X {\n    set y [expr { ($a) ? !$b : !$c }]\n}\n"),
13895            bare,
13896            "parenthesising the condition must not change the count"
13897        );
13898    }
13899
13900    // #1454's five-language sweep of #1422's C# rule: a pattern-match
13901    // guard is a condition *slot*, so every spelling of one contributes
13902    // exactly one condition, and the guard is a cyclomatic decision the
13903    // arm it guards does not already pay for.
13904    //
13905    // Each fixture is the §11 triple — an operator guard (`> 5`, whose
13906    // comparison token already counted before the fix), a call guard,
13907    // and a bare-identifier guard (the two that counted nothing) —
13908    // plus a parenthesised guard, which is the only member that
13909    // exercises the boolean-context seed, plus an unguarded control
13910    // that must stay where it was. A single-spelling fixture cannot
13911    // show the slot works: before the fix the operator member already
13912    // read the right number.
13913    //
13914    // The controls are load-bearing twice over. `none` pins that the
13915    // slot fires on the guard and not on the arm — a `+1` on every arm
13916    // would satisfy every guarded row and break this one — and the
13917    // helper (`is_even` / `isEven`) pins that the call-guard row is
13918    // scoring its guard rather than its call, since a call is an ABC
13919    // *branch* everywhere and a condition nowhere.
13920    //
13921    // `assert_members_score` rather than `assert_every_member_scores`:
13922    // the guarded members and their control sit at different values,
13923    // which is the comparison these tests exist to make.
13924
13925    #[cfg(feature = "rust")]
13926    #[test]
13927    fn rust_match_guard_scores_one_condition_however_spelled() {
13928        let src = "fn is_even(n: i32) -> bool {
13929                n == 0
13930            }
13931            fn tok(x: i32) -> i32 {
13932                match x { n if n > 5 => 1, _ => 0 }
13933            }
13934            fn call(x: i32) -> i32 {
13935                match x { n if is_even(n) => 1, _ => 0 }
13936            }
13937            fn bare(x: i32, b: bool) -> i32 {
13938                match x { _ if b => 1, _ => 0 }
13939            }
13940            fn paren(x: i32, b: bool) -> i32 {
13941                match x { n if (b) => n, _ => 0 }
13942            }
13943            fn none(x: i32) -> i32 {
13944                match x { 1 => 1, _ => 0 }
13945            }";
13946        assert_fixture_spells::<RustParser>(
13947            src,
13948            "foo.rs",
13949            &[
13950                // The guard's own `if` keyword, one per guarded member.
13951                // It is also how Rust cyclomatic already saw the guard,
13952                // which is why only the ABC half moved here.
13953                (Rust::If as u16, 4, "match guards"),
13954                (Rust::GT as u16, 1, "`tok`'s `>`"),
13955                (Rust::CallExpression as u16, 1, "the `is_even` call"),
13956                (
13957                    Rust::ParenthesizedExpression as u16,
13958                    1,
13959                    "`paren`'s parenthesised guard",
13960                ),
13961                // Two arms per member: the guarded one and its bare `_`.
13962                (Rust::MatchPattern as u16, 10, "match arms"),
13963            ],
13964        );
13965        check_func_space::<RustParser, _>(src, "foo.rs", |space| {
13966            assert_members_score(
13967                &space,
13968                &[
13969                    ("is_even", 1, 1),
13970                    ("tok", 2, 3),
13971                    // Was 1 — the guard spelling nothing counted.
13972                    ("call", 2, 3),
13973                    ("bare", 2, 3),
13974                    ("paren", 2, 3),
13975                    ("none", 1, 2),
13976                ],
13977            );
13978        });
13979    }
13980
13981    #[cfg(feature = "java")]
13982    #[test]
13983    fn java_pattern_switch_guard_scores_one_condition_however_spelled() {
13984        let src = "class T {
13985                static boolean isEven(int i) { return i == 0; }
13986                int tok(Object o) { return switch (o) { case Integer i when i > 5 -> 1; default -> 0; }; }
13987                int call(Object o) { return switch (o) { case Integer i when isEven(i) -> 1; default -> 0; }; }
13988                int bare(Object o, boolean b) { return switch (o) { case Integer i when b -> 1; default -> 0; }; }
13989                int paren(Object o, boolean b) { return switch (o) { case Integer i when (b) -> 1; default -> 0; }; }
13990                int none(Object o) { return switch (o) { case Integer i -> 1; default -> 0; }; }
13991            }";
13992        assert_fixture_spells::<JavaParser>(
13993            src,
13994            "foo.java",
13995            &[
13996                (Java::Guard as u16, 4, "`when` guards"),
13997                (Java::GT as u16, 1, "`tok`'s `>`"),
13998                (Java::MethodInvocation as u16, 1, "the `isEven` call"),
13999                // Five `switch (o)` subjects plus `paren`'s `when (b)`.
14000                // Java wraps a switch subject in the same kind, so this
14001                // count moves if either is edited out.
14002                (
14003                    Java::ParenthesizedExpression as u16,
14004                    6,
14005                    "the switch subjects and `paren`'s guard",
14006                ),
14007            ],
14008        );
14009        check_func_space::<JavaParser, _>(src, "foo.java", |space| {
14010            assert_members_score(
14011                &space.spaces[0],
14012                &[
14013                    ("isEven", 1, 1),
14014                    // Was 2 conditions against a *flat* cyclomatic of 2:
14015                    // Java had neither half of the rule, so the guard
14016                    // was invisible to cyclomatic and visible to ABC
14017                    // only through whatever operator it spelled.
14018                    ("tok", 2, 3),
14019                    ("call", 2, 3),
14020                    ("bare", 2, 3),
14021                    ("paren", 2, 3),
14022                    ("none", 1, 2),
14023                ],
14024            );
14025        });
14026    }
14027
14028    #[cfg(feature = "python")]
14029    #[test]
14030    fn python_case_guard_scores_one_condition_however_spelled() {
14031        let src = "def is_even(n):
14032    return n == 0
14033def tok(x):
14034    match x:
14035        case n if n > 5:
14036            return 1
14037        case _:
14038            return 0
14039def call(x):
14040    match x:
14041        case n if is_even(x):
14042            return 1
14043        case _:
14044            return 0
14045def bare(x, b):
14046    match x:
14047        case _ if b:
14048            return 1
14049        case _:
14050            return 0
14051def paren(x, b):
14052    match x:
14053        case n if (b):
14054            return n
14055        case _:
14056            return 0
14057def none(x):
14058    match x:
14059        case 1:
14060            return 1
14061        case _:
14062            return 0
14063";
14064        assert_fixture_spells::<PythonParser>(
14065            src,
14066            "foo.py",
14067            &[
14068                // The `case` guard clause. A comprehension filter is the
14069                // same kind in a different role, and the `guard` field
14070                // is what keeps it out of the slot — there is none in
14071                // this fixture, and `python_comprehension_if_clause_is_not_a_case_guard`
14072                // is where that separation is pinned.
14073                (Python::IfClause as u16, 4, "`case` guards"),
14074                (
14075                    Python::ComparisonOperator as u16,
14076                    2,
14077                    "`tok`'s `>` and `is_even`'s `==`",
14078                ),
14079                (Python::Call as u16, 1, "the `is_even` call"),
14080                (
14081                    Python::ParenthesizedExpression as u16,
14082                    1,
14083                    "`paren`'s parenthesised guard",
14084                ),
14085            ],
14086        );
14087        check_func_space::<PythonParser, _>(src, "foo.py", |space| {
14088            assert_members_score(
14089                &space,
14090                &[
14091                    ("is_even", 1, 1),
14092                    ("tok", 2, 3),
14093                    ("call", 2, 3),
14094                    ("bare", 2, 3),
14095                    ("paren", 2, 3),
14096                    ("none", 1, 2),
14097                ],
14098            );
14099        });
14100    }
14101
14102    // A comprehension's `if` filter is an `if_clause` too, and it is not
14103    // a `case` guard: the slot reads `case_clause`'s `guard` field, so
14104    // the two cannot be confused by construction. Python cyclomatic does
14105    // count the filter (through the same `If` keyword token it counts a
14106    // guard by), and ABC does not — a pre-existing divergence this
14107    // change deliberately leaves where it found it. The test is here so
14108    // that a later `IfClause` arm added without the field read fails
14109    // loudly rather than moving comprehensions silently.
14110    #[cfg(feature = "python")]
14111    #[test]
14112    fn python_comprehension_if_clause_is_not_a_case_guard() {
14113        let src = "def m(xs):
14114    return [x for x in xs if x]
14115";
14116        assert_fixture_spells::<PythonParser>(
14117            src,
14118            "foo.py",
14119            &[(Python::IfClause as u16, 1, "the comprehension filter")],
14120        );
14121        check_func_space::<PythonParser, _>(src, "foo.py", |space| {
14122            assert_members_score(&space, &[("m", 0, 3)]);
14123        });
14124    }
14125
14126    #[cfg(feature = "ruby")]
14127    #[test]
14128    fn ruby_in_clause_guard_scores_one_condition_however_spelled() {
14129        let src = "def is_even(x)
14130  x == 0
14131end
14132def tok(x)
14133  case x
14134  in [n] if n > 5 then 1
14135  in _ then 0
14136  end
14137end
14138def call(x)
14139  case x
14140  in [n] if n.even? then 1
14141  in _ then 0
14142  end
14143end
14144def bare(x, b)
14145  case x
14146  in [n] if b then 1
14147  in _ then 0
14148  end
14149end
14150def unguard(x, b)
14151  case x
14152  in [n] unless b then 1
14153  in _ then 0
14154  end
14155end
14156def paren(x, b)
14157  case x
14158  in [n] if (b) then 1
14159  in _ then 0
14160  end
14161end
14162def none(x)
14163  case x
14164  in [n] then 1
14165  in _ then 0
14166  end
14167end
14168";
14169        assert_fixture_spells::<RubyParser>(
14170            src,
14171            "foo.rb",
14172            &[
14173                (Ruby::IfGuard as u16, 4, "`if` guards"),
14174                (Ruby::UnlessGuard as u16, 1, "the `unless` guard"),
14175                (Ruby::GT as u16, 1, "`tok`'s `>`"),
14176                (
14177                    Ruby::ParenthesizedStatements as u16,
14178                    1,
14179                    "`paren`'s parenthesised guard",
14180                ),
14181            ],
14182        );
14183        // `Ruby::Guard` (210) is the hidden `_guard` supertype: the two
14184        // dispatchers list it beside the concrete kinds so a grammar
14185        // that starts emitting it keeps working, and this pins that it
14186        // does not emit it today — otherwise the defensive arm is
14187        // indistinguishable from a dead one
14188        // (`.claude/rules/grammar-dispatch.md` §2).
14189        let parser = RubyParser::new(
14190            src.as_bytes().to_vec(),
14191            &std::path::PathBuf::from("foo.rb"),
14192            None,
14193        );
14194        assert!(
14195            !ast_has_kind_id(&parser, Ruby::Guard as u16),
14196            "`_guard` stopped being hidden — the defensive arms now fire"
14197        );
14198        check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
14199            assert_members_score(
14200                &space,
14201                &[
14202                    ("is_even", 1, 1),
14203                    // Was 2 / 2: Ruby had neither half either, and the
14204                    // arm's own condition supplied the 2 that made the
14205                    // operator spelling look correct.
14206                    ("tok", 2, 3),
14207                    ("call", 2, 3),
14208                    ("bare", 2, 3),
14209                    ("unguard", 2, 3),
14210                    ("paren", 2, 3),
14211                    ("none", 1, 2),
14212                ],
14213            );
14214        });
14215    }
14216
14217    // Elixir is the inverse of the four above: ABC counted the `when`
14218    // token from the start and cyclomatic had no arm at all, so a guard
14219    // read as a condition with no decision behind it. #1454 adds the
14220    // decision, which is why `tok` / `call` / `bare` move on the
14221    // cyclomatic axis here and on the ABC axis everywhere else.
14222    //
14223    // Elixir was also the one language of the six that #1454 left
14224    // *without* the slot. Its `when` arm added a flat one on top of
14225    // whatever the guard's sub-structure already scored, so `tok` came
14226    // back 3 against `call` and `bare`'s 2 — the spelling-dependence the
14227    // batch exists to remove, and a `.claude/rules/grammar-dispatch.md`
14228    // §5 double count with the `>` token arm. A whole-branch review of
14229    // the batch found it; all three rows now read 2, level with the
14230    // sibling fixtures above and with their own decision counts.
14231    #[cfg(feature = "elixir")]
14232    #[test]
14233    fn elixir_guard_is_a_decision_however_spelled() {
14234        let src = "defmodule T do
14235  def is_even(x) do
14236    x == 0
14237  end
14238  def tok(x) do
14239    case x do
14240      n when n > 5 -> 1
14241      _ -> 0
14242    end
14243  end
14244  def call(x) do
14245    case x do
14246      n when is_integer(n) -> 1
14247      _ -> 0
14248    end
14249  end
14250  def bare(x, b) do
14251    case x do
14252      _n when b -> 1
14253      _ -> 0
14254    end
14255  end
14256  def none(x) do
14257    case x do
14258      1 -> 1
14259      _ -> 0
14260    end
14261  end
14262end
14263";
14264        assert_fixture_spells::<ElixirParser>(
14265            src,
14266            "foo.ex",
14267            &[
14268                (Elixir::When as u16, 3, "`when` guards"),
14269                (Elixir::GT as u16, 1, "`tok`'s `>`"),
14270            ],
14271        );
14272        check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
14273            assert_members_score(
14274                &space.spaces[0],
14275                &[
14276                    ("is_even", 1, 1),
14277                    // All three guarded members were cyclomatic 2 —
14278                    // level with `none` — before the decision arm, and
14279                    // `tok` was abc 3 before the slot.
14280                    ("tok", 2, 3),
14281                    ("call", 2, 3),
14282                    ("bare", 2, 3),
14283                    ("none", 1, 2),
14284                ],
14285            );
14286        });
14287    }
14288
14289    // The slot's whole claim, on every spelling the Elixir grammar gives
14290    // a guard — and on `assert_every_member_scores`, which asserts §8
14291    // parity where the fixture above cannot (its unguarded control and
14292    // its bare-comparison member legitimately sit off their decision
14293    // counts, which is why that one takes a per-member table).
14294    //
14295    // Nine members, nine routes to the same 2:
14296    //
14297    //   * `tok` / `eq` — the guard is an operator application, so the
14298    //     slot adds nothing and the `>` / `==` token arm supplies the
14299    //     one. These were 3 before the slot.
14300    //   * `call` / `bare` — `Call` and `Identifier` are
14301    //     `elixir_bool_terminal_kinds!()` members, so the slot supplies
14302    //     the one directly.
14303    //   * `paren` / `negated` — `block` and `unary_operator` are
14304    //     wrappers, peeled by `elixir_inspect_container`. `negated`
14305    //     covers the keyword `not`, which that walker did not recognise
14306    //     as a negation until this change and which would otherwise have
14307    //     scored zero here.
14308    //   * `membership` / `nonmembership` — `in` / `not in` are
14309    //     relational operators, given the by-use arm #1461 gave the
14310    //     other five languages. Without it these two would score zero,
14311    //     since the slot correctly declines an operator application.
14312    //
14313    // A repeated guard (`when a when b`) is deliberately absent: it is
14314    // an or-chain and scores one per alternative, so it belongs with
14315    // `elixir_repeated_guard_matches_the_or_chain` below rather than in
14316    // a table whose every row reads 2.
14317    //
14318    // §11: each bullet is an independent path, and no other member can
14319    // stand in for it — deleting the `in` arm leaves `membership` and
14320    // `nonmembership` alone failing, deleting the `not` recognition
14321    // leaves `negated` alone failing.
14322    #[cfg(feature = "elixir")]
14323    #[test]
14324    fn elixir_guard_scores_one_however_spelled() {
14325        let src = "defmodule T do
14326  def tok(x) do
14327    case x do
14328      n when n > 5 -> 1
14329      _ -> 0
14330    end
14331  end
14332  def eq(x) do
14333    case x do
14334      n when n == 5 -> 1
14335      _ -> 0
14336    end
14337  end
14338  def call(x) do
14339    case x do
14340      n when is_integer(n) -> 1
14341      _ -> 0
14342    end
14343  end
14344  def bare(x, b) do
14345    case x do
14346      _n when b -> 1
14347      _ -> 0
14348    end
14349  end
14350  def paren(x, b) do
14351    case x do
14352      _n when (b) -> 1
14353      _ -> 0
14354    end
14355  end
14356  def negated(x, b) do
14357    case x do
14358      _n when not b -> 1
14359      _ -> 0
14360    end
14361  end
14362  def membership(x) do
14363    case x do
14364      n when n in [1, 2] -> 1
14365      _ -> 0
14366    end
14367  end
14368  def nonmembership(x) do
14369    case x do
14370      n when n not in [1, 2] -> 1
14371      _ -> 0
14372    end
14373  end
14374end
14375";
14376        assert_fixture_spells::<ElixirParser>(
14377            src,
14378            "foo.ex",
14379            &[
14380                (Elixir::When as u16, 8, "one `when` guard per member"),
14381                (Elixir::GT as u16, 1, "`tok`'s `>`"),
14382                (Elixir::EQEQ as u16, 1, "`eq`'s `==`"),
14383                (Elixir::Block as u16, 1, "`paren`'s parenthesised guard"),
14384                (Elixir::Not as u16, 1, "`negated`'s `not`"),
14385                (Elixir::In as u16, 1, "`membership`'s `in`"),
14386                (Elixir::Notin as u16, 1, "`nonmembership`'s `not in`"),
14387            ],
14388        );
14389        check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
14390            assert_every_member_scores(
14391                &space.spaces[0],
14392                8,
14393                2,
14394                "one for the `case` arm and one for the guard, however the guard is spelled",
14395            );
14396        });
14397    }
14398
14399    // Elixir's repeated guard is an or-chain, not a longer spelling of
14400    // one guard: each `when` expression is tried in turn and evaluation
14401    // moves to the next when the previous one is false *or raises*. So
14402    // `when a when b` carries the alternatives `when a or b` does, and
14403    // the two spellings have to score alike.
14404    //
14405    // They did not. The nesting is right-associative
14406    // (`head when (a when (b when c))`), only the outermost `when` sat
14407    // on the anchor `elixir_when_is_guard` tests, and the `Abc` slot
14408    // peeled to the last alternative and scored that one — so a
14409    // repeated guard read *identically to a single guard* on both axes
14410    // while the `or` form read one higher per alternative.
14411    //
14412    // Asserted as an equality against the `or` spelling rather than
14413    // against separate literals, so neither row can drift alone; the
14414    // literals are here too, because a pair of equal wrong numbers
14415    // would satisfy the equality on its own.
14416    //
14417    // Both anchors, because the climb has to reach each: a definition
14418    // `Call`'s `arguments` (`repeated` / `ored`) and a `stab_clause`'s
14419    // `left` (`clause` / `clause_or`). `single` is the control the
14420    // repeated rows must sit *above*, which is the comparison that
14421    // failed before.
14422    #[cfg(feature = "elixir")]
14423    #[test]
14424    fn elixir_repeated_guard_matches_the_or_chain() {
14425        let src = "defmodule T do
14426  def repeated(n) when is_integer(n) when is_float(n) when is_atom(n) do
14427    n
14428  end
14429  def ored(n) when is_integer(n) or is_float(n) or is_atom(n) do
14430    n
14431  end
14432  def single(n) when is_integer(n) do
14433    n
14434  end
14435  def clause(x) do
14436    case x do
14437      n when is_integer(n) when is_float(n) -> 1
14438      _ -> 0
14439    end
14440  end
14441  def clause_or(x) do
14442    case x do
14443      n when is_integer(n) or is_float(n) -> 1
14444      _ -> 0
14445    end
14446  end
14447end
14448";
14449        assert_fixture_spells::<ElixirParser>(
14450            src,
14451            "foo.ex",
14452            &[
14453                (
14454                    Elixir::When as u16,
14455                    8,
14456                    "three repeated, one `or`-spelled, one single, two \
14457                     repeated and one `or`-spelled in a clause",
14458                ),
14459                (Elixir::Or as u16, 3, "the two `or` spellings' operators"),
14460            ],
14461        );
14462        check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
14463            // Positional: a `def` whose head carries a guard parses its
14464            // name out of a `binary_operator` rather than a plain
14465            // `Call`, so `repeated`, `ored` and `single` all come back
14466            // `<anonymous>` (the same naming gap
14467            // `elixir_typespec_when_is_not_a_guard` reads around).
14468            let members: Vec<(u64, u64)> = space.spaces[0]
14469                .spaces
14470                .iter()
14471                .map(|m| {
14472                    (
14473                        m.metrics.abc.conditions(),
14474                        m.metrics.cyclomatic.cyclomatic(),
14475                    )
14476                })
14477                .collect();
14478            assert_eq!(
14479                members,
14480                vec![(3, 4), (3, 4), (1, 2), (3, 4), (3, 4)],
14481                "a repeated guard scores one alternative per `when`; the \
14482                 single guard is the control one alternative lower"
14483            );
14484            assert_eq!(
14485                members[0], members[1],
14486                "`when a when b when c` is the or-chain `when a or b or c`"
14487            );
14488            assert_eq!(
14489                members[3], members[4],
14490                "and the same holds on a `stab_clause` anchor"
14491            );
14492        });
14493    }
14494
14495    // The gate that makes the Elixir arm safe. Elixir has no dedicated
14496    // guard production, and a typespec's binding clause spells the same
14497    // `when` token — so an ungated arm would have made type syntax a
14498    // decision. It was already an ABC condition against no decision
14499    // anywhere, which this removes.
14500    //
14501    // Both members carry the same `@spec`; only `guarded` carries a real
14502    // head guard, so the difference between the two rows is the guard
14503    // and nothing else.
14504    #[cfg(feature = "elixir")]
14505    #[test]
14506    fn elixir_typespec_when_is_not_a_guard() {
14507        let src = "defmodule T do
14508  @spec plain(a) :: a when a: integer
14509  def plain(x) do
14510    x
14511  end
14512  @spec guarded(a) :: a when a: integer
14513  def guarded(x) when is_integer(x) do
14514    x
14515  end
14516end
14517";
14518        assert_fixture_spells::<ElixirParser>(
14519            src,
14520            "foo.ex",
14521            &[(
14522                Elixir::When as u16,
14523                3,
14524                "two typespec `when`s and one head guard",
14525            )],
14526        );
14527        check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
14528            let module = &space.spaces[0];
14529            // Positional rather than by name: a `def` whose head carries
14530            // a guard parses its name out of a `binary_operator` instead
14531            // of a plain `Call` target, and the space comes back
14532            // `<anonymous>`. That naming gap predates this change and is
14533            // why `assert_members_score` cannot serve here.
14534            let members: Vec<(u64, u64)> = module
14535                .spaces
14536                .iter()
14537                .map(|m| {
14538                    (
14539                        m.metrics.abc.conditions(),
14540                        m.metrics.cyclomatic.cyclomatic(),
14541                    )
14542                })
14543                .collect();
14544            assert_eq!(
14545                members,
14546                vec![(0, 1), (1, 2)],
14547                "`plain` scores nothing; `guarded` scores its head guard \
14548                 in both metrics"
14549            );
14550            // The typespecs sit in the module body, outside either
14551            // member, so their (non-)contribution has to be read off
14552            // the container. Was 2 before the gate: one per `@spec`.
14553            assert_eq!(
14554                module.metrics.abc.conditions(),
14555                0,
14556                "a typespec `when` is type syntax, not a guard"
14557            );
14558        });
14559    }
14560
14561    // The other half of that gate's allowlist. `elixir_when_is_guard`
14562    // accepts a definition `Call`'s `arguments` anchor when the call's
14563    // keyword is a method macro *or* one of `defguard` / `defguardp`,
14564    // and the second disjunct is its own path: `elixir_is_method_macro`
14565    // spells only `def` / `defp` / `defmacro` / `defmacrop`, so a
14566    // `defguard` head reaches the anchor test solely through the
14567    // `matches!`. Nothing in the suite spelled `defguard` before this,
14568    // which left the disjunct scored by no fixture at all.
14569    //
14570    // `defguard` is where a guard is most obviously a decision — the
14571    // macro exists to name one — so an ungated head would have scored
14572    // the construct zero on both axes while the `def f(x) when g`
14573    // spelling it expands into scores one.
14574    //
14575    // Read off the *module*, not off a member: `defguard` is not a
14576    // method macro, so it opens no function space and its guard lands
14577    // in the container's body. `plain` is the control that pins the
14578    // module's rows to the two guards and nothing else, and the guard
14579    // bodies are calls (`is_integer/1`, `is_atom/1`) rather than
14580    // comparisons so neither can supply a condition of its own.
14581    #[cfg(feature = "elixir")]
14582    #[test]
14583    fn elixir_defguard_head_is_a_guard() {
14584        let src = "defmodule T do
14585  defguard is_int(x) when is_integer(x)
14586  defguardp is_at(x) when is_atom(x)
14587  def plain(x) do
14588    x
14589  end
14590end
14591";
14592        assert_fixture_spells::<ElixirParser>(
14593            src,
14594            "foo.ex",
14595            &[(
14596                Elixir::When as u16,
14597                2,
14598                "one per `defguard` / `defguardp` head",
14599            )],
14600        );
14601        check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
14602            let module = &space.spaces[0];
14603            assert_eq!(
14604                (
14605                    module.metrics.abc.conditions(),
14606                    module.metrics.cyclomatic.cyclomatic()
14607                ),
14608                (2, 3),
14609                "each of `defguard` and `defguardp` carries one guard, on \
14610                 both axes (cyclomatic counts from its base of 1)"
14611            );
14612            let members: Vec<(u64, u64)> = module
14613                .spaces
14614                .iter()
14615                .map(|m| {
14616                    (
14617                        m.metrics.abc.conditions(),
14618                        m.metrics.cyclomatic.cyclomatic(),
14619                    )
14620                })
14621                .collect();
14622            assert_eq!(
14623                members,
14624                vec![(0, 1)],
14625                "`defguard` opens no function space, so `plain` is the \
14626                 module's only member and scores nothing"
14627            );
14628        });
14629    }
14630
14631    // #1461's structural half: a relational operator scores by *use*,
14632    // a value-bearing operand scores in a boolean *slot*.
14633    //
14634    // Until this, the relational constructs a grammar spells as their
14635    // own production — C#'s two `is` tests, Java's and Groovy's
14636    // `instanceof`, Groovy's `in`, Kotlin's `is` / `in`, Ruby's
14637    // one-line `in` — reached `stats.conditions` only through
14638    // `<lang>_bool_terminal_kinds!()`, which every walker consults
14639    // inside a boolean slot and nowhere else. The comparison token
14640    // beside each of them carried no such gate, so one language scored
14641    // two spellings of the same relational test differently by
14642    // position:
14643    //
14644    //     val b = a == c        // 1 condition
14645    //     val b = a is String   // 0 conditions
14646    //
14647    // Fitzpatrick Rule 5 scores a relational operator wherever it is
14648    // written, so the asymmetry was in the mechanism. Each construct
14649    // now has an unconditional arm in its language's `compute` and has
14650    // left the terminal set; listing it in both would score it twice
14651    // (`.claude/rules/grammar-dispatch.md` §5).
14652    //
14653    // Every fixture below carries the same four shapes, because they
14654    // are structurally independent paths and covering one says nothing
14655    // about the others (§11):
14656    //
14657    // - `out*` — the construct with no boolean slot above it. This is
14658    //   the case the change exists for, and the one no prior fixture
14659    //   covered: measured against the pre-change binary, every `out*`
14660    //   member here scored **0**.
14661    // - `outCtl` — the same member spelling `==`. It read 1 before and
14662    //   after, so it is the anchor that makes `out*`'s 1 a claim about
14663    //   the construct rather than about the member shape
14664    //   (`.claude/rules/testing.md`, "Perturb the fixture as well as
14665    //   the production line").
14666    // - `in*` / `inAnd` — the predicate slot and the `&&`-chain walker,
14667    //   the two paths that already owned the count. They must be
14668    //   *unchanged*; a §5 double count surfaces here as a 2 against the
14669    //   control's 1, which is how #1459 caught the Kotlin `as?`
14670    //   collision.
14671    // - `forIn` (Kotlin / Groovy / Ruby) — the `for … in …` header,
14672    //   which spells the same keyword from a different production and
14673    //   must stay at 0. It is the reason those three count the node and
14674    //   not the token.
14675
14676    #[cfg(feature = "csharp")]
14677    #[test]
14678    fn csharp_is_tests_score_outside_a_boolean_slot() {
14679        let src = "class A {
14680                bool outIs(object x) => x is int;
14681                bool outPat(object x) => x is null;
14682                bool outCtl(int x) => x == 1;
14683                int inIs(object x) { if (x is int) { return 1; } return 0; }
14684                int inAnd(object x, bool b) { if (x is int && b) { return 1; } return 0; }
14685            }";
14686        assert_csharp_fixture_spells(
14687            src,
14688            &[
14689                (Csharp::IsExpression as u16, 3, "the bare `is` type tests"),
14690                (
14691                    Csharp::IsPatternExpression as u16,
14692                    1,
14693                    "`outPat`'s null pattern",
14694                ),
14695                (Csharp::EQEQ as u16, 1, "`outCtl`'s comparison"),
14696            ],
14697        );
14698        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
14699            assert_members_score(
14700                &space.spaces[0],
14701                &[
14702                    // Both were 0, against `outCtl`'s 1 on the same
14703                    // expression-bodied shape.
14704                    ("outIs", 1, 1),
14705                    ("outPat", 1, 1),
14706                    ("outCtl", 1, 1),
14707                    // Unchanged: the slot no longer counts the test, the
14708                    // new arm does, and the total stays put.
14709                    ("inIs", 1, 2),
14710                    ("inAnd", 2, 3),
14711                ],
14712            );
14713        });
14714    }
14715
14716    #[cfg(feature = "java")]
14717    #[test]
14718    fn java_instanceof_scores_outside_a_boolean_slot() {
14719        let src = "class A {
14720                boolean outOf(Object x) { return x instanceof String; }
14721                boolean outCtl(int x) { return x == 1; }
14722                int inOf(Object x) { if (x instanceof String) { return 1; } return 0; }
14723                int inAnd(Object x, boolean b) { if (x instanceof String && b) { return 1; } return 0; }
14724            }";
14725        assert_fixture_spells::<JavaParser>(
14726            src,
14727            "foo.java",
14728            &[
14729                (
14730                    Java::InstanceofExpression as u16,
14731                    3,
14732                    "the `instanceof` tests",
14733                ),
14734                (Java::EQEQ as u16, 1, "`outCtl`'s comparison"),
14735            ],
14736        );
14737        check_func_space::<JavaParser, _>(src, "foo.java", |space| {
14738            assert_members_score(
14739                &space.spaces[0],
14740                &[
14741                    // Was 0, against `outCtl`'s 1 on the same
14742                    // `return`-a-predicate shape.
14743                    ("outOf", 1, 1),
14744                    ("outCtl", 1, 1),
14745                    ("inOf", 1, 2),
14746                    ("inAnd", 2, 3),
14747                ],
14748            );
14749        });
14750    }
14751
14752    // Groovy carries item 4 as well as the structural pass. `<=>`
14753    // yields -1 / 0 / 1 rather than a boolean, which is why it never
14754    // belonged in the terminal set — that set holds operands — but it
14755    // is a relational operator, and `LTEQGT` is already a condition
14756    // token in Ruby, PHP, C++ and Mozcpp. Groovy was the outlier at 0.
14757    #[cfg(feature = "groovy")]
14758    #[test]
14759    fn groovy_relational_productions_score_outside_a_boolean_slot() {
14760        let src = "class A {
14761                def outIs(x) { def b = x instanceof String; return b }
14762                def outIn(x, l) { def b = x in l; return b }
14763                def outShip(a, c) { def r = a <=> c; return r }
14764                def outCtl(x) { def b = x == 1; return b }
14765                def inIs(x) { if (x instanceof String) { return 1 }; return 0 }
14766                def inIn(x, l) { if (x in l) { return 1 }; return 0 }
14767                def forIn(l) { for (q in l) { }; return 0 }
14768            }";
14769        assert_fixture_spells::<GroovyParser>(
14770            src,
14771            "foo.groovy",
14772            &[
14773                (
14774                    Groovy::InstanceofExpression as u16,
14775                    2,
14776                    "the `instanceof` tests",
14777                ),
14778                (Groovy::MembershipExpression as u16, 2, "the `in` tests"),
14779                (Groovy::SpaceshipExpression as u16, 1, "`outShip`'s `<=>`"),
14780                (
14781                    Groovy::ForInStatement as u16,
14782                    1,
14783                    "`forIn`'s loop header, which spells the same `in`",
14784                ),
14785                (Groovy::EQEQ as u16, 1, "`outCtl`'s comparison"),
14786            ],
14787        );
14788        check_func_space::<GroovyParser, _>(src, "foo.groovy", |space| {
14789            assert_members_score(
14790                &space.spaces[0],
14791                &[
14792                    // All three were 0, against `outCtl`'s 1 on the
14793                    // identical declare-and-return shape.
14794                    ("outIs", 1, 1),
14795                    ("outIn", 1, 1),
14796                    ("outShip", 1, 1),
14797                    ("outCtl", 1, 1),
14798                    ("inIs", 1, 2),
14799                    ("inIn", 1, 2),
14800                    // The loop header is not a membership test and
14801                    // stays at 0 — counting the `in` *token* would have
14802                    // scored it.
14803                    ("forIn", 0, 2),
14804                ],
14805            );
14806        });
14807    }
14808
14809    #[cfg(feature = "kotlin")]
14810    #[test]
14811    fn kotlin_is_and_in_score_outside_a_boolean_slot() {
14812        let src = "class K {
14813                fun outIs(a: Any): Boolean { val b = a is String; return b }
14814                fun outIn(a: Int): Boolean { val b = a in 1..2; return b }
14815                fun outCtl(a: Int): Boolean { val b = a == 1; return b }
14816                fun inIs(a: Any): Int { if (a is String) { return 1 }; return 0 }
14817                fun forIn(l: List<Int>): Int { for (q in l) { }; return 0 }
14818            }";
14819        assert_kotlin_class_members(
14820            src,
14821            &[
14822                (Kotlin::IsExpression as u16, 2, "the `is` tests"),
14823                (Kotlin::InExpression as u16, 1, "`outIn`'s `in` test"),
14824                (
14825                    Kotlin::ForStatement as u16,
14826                    1,
14827                    "`forIn`'s loop header, which spells the same `in`",
14828                ),
14829                (Kotlin::EQEQ as u16, 1, "`outCtl`'s comparison"),
14830            ],
14831            &[
14832                // Both were 0, against `outCtl`'s 1 on the identical
14833                // `val`-and-return shape.
14834                ("outIs", 1, 1),
14835                ("outIn", 1, 1),
14836                ("outCtl", 1, 1),
14837                ("inIs", 1, 2),
14838                ("forIn", 0, 2),
14839            ],
14840        );
14841    }
14842
14843    #[cfg(feature = "ruby")]
14844    #[test]
14845    fn ruby_test_pattern_scores_outside_a_boolean_slot() {
14846        let src = "def out_in(a)
14847  b = a in Integer
14848  b
14849end
14850def out_ctl(a)
14851  b = a == 1
14852  b
14853end
14854def in_in(a)
14855  if a in Integer
14856    return 1
14857  end
14858  0
14859end
14860def for_in(l)
14861  for q in l do end
14862  0
14863end
14864";
14865        assert_fixture_spells::<RubyParser>(
14866            src,
14867            "foo.rb",
14868            &[
14869                (Ruby::TestPattern as u16, 2, "the one-line `in` tests"),
14870                (
14871                    Ruby::For as u16,
14872                    1,
14873                    "`for_in`'s loop header, which spells the same `in`",
14874                ),
14875                (Ruby::EQEQ as u16, 1, "`out_ctl`'s comparison"),
14876            ],
14877        );
14878        check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
14879            assert_members_score(
14880                &space,
14881                &[
14882                    // Was 0, against `out_ctl`'s 1 on the identical
14883                    // assign-and-return shape.
14884                    ("out_in", 1, 1),
14885                    ("out_ctl", 1, 1),
14886                    ("in_in", 1, 2),
14887                    ("for_in", 0, 2),
14888                ],
14889            );
14890        });
14891    }
14892
14893    // #1461 item 1. The walrus is an *operand*, not an operator, so it
14894    // joins `python_bool_terminal_kinds!()` and stays slot-scoped —
14895    // `b = (n := g())` outside a predicate is a binding, not a
14896    // decision. Inside one the slot tests `g()`'s truth, and
14897    // `if (n := g()):` scored 0 where the `if g():` it is a
14898    // refactoring of scored 1.
14899    //
14900    // `assignments` is asserted beside `conditions` because the walrus
14901    // is the one construct in the survey that pays on two axes, and
14902    // that is deliberate rather than a §5 double count: ABC's axes are
14903    // independent measurements of the same source, and `if (n := g()):`
14904    // genuinely both binds a name and decides a branch. Without this
14905    // row a later reader has no way to tell the intent from an
14906    // oversight.
14907    #[cfg(feature = "python")]
14908    #[test]
14909    fn python_walrus_is_a_unary_condition() {
14910        let src = "def in_slot(g):
14911    if (n := g()):
14912        return 1
14913    return 0
14914
14915def chain(g, b):
14916    return b and (n := g())
14917
14918def ctrl(g):
14919    if g():
14920        return 1
14921    return 0
14922";
14923        assert_fixture_spells::<PythonParser>(
14924            src,
14925            "foo.py",
14926            &[(Python::NamedExpression as u16, 2, "the walrus bindings")],
14927        );
14928        check_func_space::<PythonParser, _>(src, "foo.py", |space| {
14929            assert_members_score(
14930                &space,
14931                &[
14932                    // Was 0, against `ctrl`'s 1 for the same predicate
14933                    // without the binding.
14934                    ("in_slot", 1, 2),
14935                    // The `and`-chain walker is the second, independent
14936                    // path (§11): was 1, the bare `b` operand alone.
14937                    ("chain", 2, 2),
14938                    ("ctrl", 1, 2),
14939                ],
14940            );
14941            for (name, assignments) in [("in_slot", 1u64), ("chain", 1), ("ctrl", 0)] {
14942                assert_eq!(
14943                    child_space(&space, name).metrics.abc.assignments(),
14944                    assignments,
14945                    "{name}: the walrus also pays on the A axis, on purpose"
14946                );
14947            }
14948        });
14949    }
14950
14951    // #1461 item 2. A macro in a boolean slot expands to a boolean
14952    // expression, and `if matches!(x, Some(_))` scored 0 against a
14953    // cyclomatic decision of 1.
14954    //
14955    // `cfg_slot` is in the fixture because the arm's *breadth* is the
14956    // decision, not a side effect: these sets discriminate on slot and
14957    // never on return type, so `cfg!` counts exactly as `matches!`
14958    // does. It is also the construct the corpus actually moves —
14959    // serde's one changed snapshot is `if cfg!(no_underscore_consts)`,
14960    // not a `matches!` — so asserting only `matches!` would leave the
14961    // measured case uncovered.
14962    #[cfg(feature = "rust")]
14963    #[test]
14964    fn rust_macro_invocation_is_a_unary_condition() {
14965        let src = "fn in_slot(x: Option<u8>) -> u8 { if matches!(x, Some(_)) { 1 } else { 0 } }
14966fn cfg_slot() -> u8 { if cfg!(unix) { 1 } else { 0 } }
14967fn chain(x: Option<u8>, b: bool) -> bool { b && matches!(x, Some(_)) }
14968fn ctrl(x: Option<u8>) -> u8 { if x.is_some() { 1 } else { 0 } }
14969";
14970        assert_fixture_spells::<RustParser>(
14971            src,
14972            "foo.rs",
14973            &[(
14974                Rust::MacroInvocation as u16,
14975                3,
14976                "the `matches!` / `cfg!` predicates",
14977            )],
14978        );
14979        check_func_space::<RustParser, _>(src, "foo.rs", |space| {
14980            assert_members_score(
14981                &space,
14982                &[
14983                    // Each was 1 — the `else` alone — against `ctrl`'s
14984                    // 2 for the same shape with a method call in the
14985                    // predicate.
14986                    ("in_slot", 2, 2),
14987                    ("cfg_slot", 2, 2),
14988                    // The `&&`-chain walker, the second independent
14989                    // path (§11): was 1, the bare `b` operand alone.
14990                    ("chain", 2, 2),
14991                    ("ctrl", 2, 2),
14992                ],
14993            );
14994        });
14995    }
14996}
14997
14998/// A comment inside a ternary must not change its ABC conditions
14999/// (#1181).
15000///
15001/// Two opposite defects, one cause: tree-sitter counts a comment among a
15002/// node's children, so it is the operand's previous sibling *and* it
15003/// shifts every positional index.
15004///
15005/// * Languages whose seed asked "is my previous sibling `?` or `:`"
15006///   (C family, PHP, Perl, JS family) read the comment as "not a
15007///   ternary token", flipped the boolean-context seed on for a *branch*
15008///   slot, and **over**-counted: `a ? /*n*/ (b) : c` scored 3 where
15009///   `a ? (b) : c` scores 2.
15010/// * Languages whose branch walk read `child(2)` / `child(4)` (Java,
15011///   C#, Groovy) landed on the comment instead of the operand, never
15012///   inspected it, and **under**-counted: `a ? /*n*/ !b : c` scored 2
15013///   where `a ? !b : c` scores 3.
15014///
15015/// Both slots are now addressed by grammar field. The parenthesised
15016/// operand is the only input that discriminates the first defect and
15017/// the negated operand the only one that discriminates the second —
15018/// existing ternary fixtures use neither.
15019// Gated on the union of the features [`ternary_comment_invariance::cases`]
15020// has a row for, so a build enabling none of them — `--no-default-features
15021// --features go` — drops both tests rather than tripping their
15022// `checked > 0` guards (`.claude/rules/testing.md`, #1286). Both tests
15023// read the same table, so one gate on the module is exact for each.
15024#[cfg(test)]
15025#[cfg(any(
15026    feature = "c",
15027    feature = "cpp",
15028    feature = "csharp",
15029    feature = "groovy",
15030    feature = "java",
15031    feature = "javascript",
15032    feature = "mozcpp",
15033    feature = "mozjs",
15034    feature = "objc",
15035    feature = "perl",
15036    feature = "php",
15037    feature = "typescript"
15038))]
15039mod ternary_comment_invariance {
15040    use crate::test_support::metrics_verbatim;
15041    use crate::{LANG, MetricsOptions};
15042
15043    fn conditions(lang: LANG, source: &str) -> u64 {
15044        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
15045            .abc
15046            .conditions_sum()
15047    }
15048
15049    /// `(base, with_comment)` for a parenthesised and a negated branch
15050    /// operand, per language.
15051    fn cases(lang: LANG) -> Option<[(String, String); 2]> {
15052        // `{}` marks the consequence slot; `/*n*/` the inserted comment.
15053        let (template, paren, negated, comment) = match lang {
15054            LANG::Cpp | LANG::C | LANG::Objc | LANG::Mozcpp => {
15055                ("int f(){ int x = a ? {} : c; }", "(b)", "!b", "/*n*/ ")
15056            }
15057            LANG::Java => (
15058                "class K{ void f(){ int x = a ? {} : c; } }",
15059                "(b)",
15060                "!b",
15061                "/*n*/ ",
15062            ),
15063            LANG::Csharp => (
15064                "class K{ void f(){ var x = a ? {} : c; } }",
15065                "(b)",
15066                "!b",
15067                "/*n*/ ",
15068            ),
15069            LANG::Groovy => ("def f(){ def x = a ? {} : c }", "(b)", "!b", "/*n*/ "),
15070            LANG::Javascript | LANG::Typescript | LANG::Tsx | LANG::Mozjs => {
15071                ("function f(){ var x = a ? {} : c; }", "(b)", "!b", "/*n*/ ")
15072            }
15073            LANG::Php => (
15074                "<?php function f(){ $x = $a ? {} : $c; }",
15075                "($b)",
15076                "!$b",
15077                "/*n*/ ",
15078            ),
15079            // Perl has no block comment: `#` runs to end of line, so the
15080            // comment must carry its own newline.
15081            LANG::Perl => ("sub f { my $x = $a ? {} : $c; }", "($b)", "!$b", "# n\n "),
15082            _ => return None,
15083        };
15084        let build = |operand: &str, with_comment: bool| {
15085            let slot = if with_comment {
15086                format!("{comment}{operand}")
15087            } else {
15088                operand.to_owned()
15089            };
15090            template.replace("{}", &slot)
15091        };
15092        Some([
15093            (build(paren, false), build(paren, true)),
15094            (build(negated, false), build(negated, true)),
15095        ])
15096    }
15097
15098    #[test]
15099    fn a_comment_before_a_branch_operand_changes_nothing() {
15100        let mut checked = 0;
15101        for lang in LANG::into_enum_iter() {
15102            if !lang.is_enabled() {
15103                continue;
15104            }
15105            let Some(pairs) = cases(lang) else { continue };
15106            checked += 1;
15107            for (base, commented) in pairs {
15108                assert_eq!(
15109                    conditions(lang, &commented),
15110                    conditions(lang, &base),
15111                    "{lang:?}: a comment changed the ABC conditions of a ternary\n  \
15112                     without: {base}\n  with:    {commented}"
15113                );
15114            }
15115        }
15116        assert!(
15117            checked > 0,
15118            "no ternary language enabled; this test asserted nothing"
15119        );
15120    }
15121
15122    /// The absolute values the invariance test compares against, so a
15123    /// regression that moved *both* sides equally still fails.
15124    ///
15125    /// expected: `a ? (b) : c` counts the `?` marker plus the condition
15126    /// `a` in boolean context = 2. Negating the consequence adds one
15127    /// more, since `!b` establishes boolean content for that slot = 3.
15128    #[test]
15129    fn the_baseline_values_are_two_and_three() {
15130        let mut checked = 0;
15131        for lang in LANG::into_enum_iter() {
15132            if !lang.is_enabled() {
15133                continue;
15134            }
15135            let Some([(paren, _), (negated, _)]) = cases(lang) else {
15136                continue;
15137            };
15138            assert_eq!(
15139                conditions(lang, &paren),
15140                2,
15141                "{lang:?}: parenthesised branch"
15142            );
15143            assert_eq!(conditions(lang, &negated), 3, "{lang:?}: negated branch");
15144            checked += 1;
15145        }
15146        assert!(
15147            checked > 0,
15148            "no ternary language enabled; this test asserted nothing"
15149        );
15150    }
15151}
15152
15153/// A keyword negation must score like its symbolic twin (#1182).
15154///
15155/// `not b` and `!b` are the same negation — they differ in precedence,
15156/// not in meaning, and ABC counts the negation rather than the parse.
15157/// Ruby and Perl tested only the `!` token, so `if not b` scored 0
15158/// against `if !b`'s 1, and a `not` ternary scored 2 against the `!`
15159/// form's 4.
15160///
15161/// Lua and Elixir were checked in the same sweep and were already
15162/// correct: Lua's only negation keyword *is* `not` and it was the token
15163/// being tested, and Elixir reaches the same count by another path.
15164/// They are exercised here so a future edit cannot regress them
15165/// silently. Python counts `not` through its own dispatcher arm and has
15166/// no `!` spelling to compare against.
15167// Gated on the union of every row below — the three `pairs` languages
15168// plus Lua, whose test is separate — so a build enabling none of them
15169// drops the module instead of failing its guards
15170// (`.claude/rules/testing.md`, #1286 / #1411). The three tests do *not*
15171// share a row set, so each carries its own narrower gate: a Lua-only
15172// build must not be asked to run the Ruby/Perl baselines.
15173#[cfg(test)]
15174#[cfg(any(
15175    feature = "elixir",
15176    feature = "lua",
15177    feature = "perl",
15178    feature = "ruby"
15179))]
15180mod keyword_negation_parity {
15181    use crate::test_support::metrics_verbatim;
15182    use crate::{LANG, MetricsOptions};
15183
15184    fn conditions(lang: LANG, source: &str) -> u64 {
15185        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
15186            .abc
15187            .conditions_sum()
15188    }
15189
15190    /// `(bang_form, keyword_form)` pairs that must score identically.
15191    #[cfg(any(feature = "elixir", feature = "perl", feature = "ruby"))]
15192    fn pairs(lang: LANG) -> Option<Vec<(String, String)>> {
15193        let build =
15194            |t: &str| -> (String, String) { (t.replace("{NOT}", "!"), t.replace("{NOT}", "not ")) };
15195        let templates: &[&str] = match lang {
15196            LANG::Ruby => &[
15197                "def f(b)\n  if {NOT}b\n    1\n  end\nend\n",
15198                "def f(a, b, c)\n  x = a ? ({NOT}b) : ({NOT}c)\nend\n",
15199                "def f(a, b, c)\n  x = a ? b : ({NOT}c)\nend\n",
15200            ],
15201            LANG::Perl => &[
15202                "sub f { if ({NOT}$b) { 1; } }",
15203                "sub f { my $x = $a ? ({NOT}$b) : ({NOT}$c); }",
15204                "sub f { my $x = $a ? $b : ({NOT}$c); }",
15205            ],
15206            // Already correct before #1182; pinned so they stay that way.
15207            LANG::Elixir => &["def f(b) do\n  if {NOT}b do\n    1\n  end\nend\n"],
15208            _ => return None,
15209        };
15210        Some(templates.iter().map(|t| build(t)).collect())
15211    }
15212
15213    #[test]
15214    #[cfg(any(feature = "elixir", feature = "perl", feature = "ruby"))]
15215    fn the_not_keyword_scores_like_bang() {
15216        let mut checked = 0;
15217        for lang in LANG::into_enum_iter() {
15218            if !lang.is_enabled() {
15219                continue;
15220            }
15221            let Some(pairs) = pairs(lang) else { continue };
15222            checked += 1;
15223            for (bang, keyword) in pairs {
15224                assert_eq!(
15225                    conditions(lang, &keyword),
15226                    conditions(lang, &bang),
15227                    "{lang:?}: `not` and `!` scored differently\n  bang:    {bang}\n  keyword: {keyword}"
15228                );
15229            }
15230        }
15231        assert!(
15232            checked > 0,
15233            "no language enabled; this test asserted nothing"
15234        );
15235    }
15236
15237    /// The absolute values, so a regression that moved both spellings
15238    /// equally still fails.
15239    ///
15240    /// expected: `if !b` is one condition — the negated bare operand.
15241    /// `a ? (!b) : (!c)` is four: the `?` marker, the condition `a` in
15242    /// boolean context, and one per negated branch operand.
15243    #[test]
15244    #[cfg(any(feature = "perl", feature = "ruby"))]
15245    fn the_baseline_values_are_one_and_four() {
15246        let mut checked = 0;
15247        for (lang, guard, ternary) in [
15248            (
15249                LANG::Ruby,
15250                "def f(b)\n  if not b\n    1\n  end\nend\n",
15251                "def f(a, b, c)\n  x = a ? (not b) : (not c)\nend\n",
15252            ),
15253            (
15254                LANG::Perl,
15255                "sub f { if (not $b) { 1; } }",
15256                "sub f { my $x = $a ? (not $b) : (not $c); }",
15257            ),
15258        ] {
15259            if !lang.is_enabled() {
15260                continue;
15261            }
15262            assert_eq!(conditions(lang, guard), 1, "{lang:?}: `if not b`");
15263            assert_eq!(conditions(lang, ternary), 4, "{lang:?}: `not` ternary");
15264            checked += 1;
15265        }
15266        assert!(
15267            checked > 0,
15268            "no language enabled; this test asserted nothing"
15269        );
15270    }
15271
15272    /// Lua's only negation keyword is `not`, so it has no `!` twin to
15273    /// compare against — its guard is that the keyword counts at all.
15274    #[test]
15275    #[cfg(feature = "lua")]
15276    fn lua_counts_its_only_negation_keyword() {
15277        // Was a silent `if !is_enabled() { return; }`, which made the
15278        // whole test a no-op rather than absent whenever the runtime
15279        // check and the feature disagreed. The `cfg` above is the
15280        // absence half; this is the loud residual half (#1286).
15281        assert!(
15282            LANG::Lua.is_enabled(),
15283            "compiled under `feature = \"lua\"` but `LANG::Lua` reports disabled; \
15284             this test asserted nothing"
15285        );
15286        assert_eq!(
15287            conditions(
15288                LANG::Lua,
15289                "function f(b)\n  if not b then return 1 end\nend\n"
15290            ),
15291            1
15292        );
15293    }
15294}
15295
15296/// A numeric literal used as a bare `&&` / `||` operand is a Fitzpatrick
15297/// Rule 9 unary condition in every truthy-valued language (#1379).
15298///
15299/// Ruby's terminal-bool set named `integer` and none of `float` /
15300/// `rational` / `complex`, Elixir's named `integer` and not `float`, and
15301/// Perl's named no numeric kind at all — so `a && 1.0` scored one
15302/// condition where `a && 1` scored two, and in Perl even `$a && 1`
15303/// scored one. Python had both kinds since #772 and is the control the
15304/// other three were brought level with.
15305///
15306/// #1410 closed the six sets #1379 deferred: PHP, Groovy, and the
15307/// name-keyed C / C++ / Mozcpp / Objective-C set, where the omission
15308/// was visible *within* one language — `if (true)` scored one condition
15309/// and `if (1)` none, because `"true"` was in the set and
15310/// `number_literal` was not.
15311///
15312/// The headline claim of each case is a *comparison*: a numeric operand
15313/// must score exactly what an identifier operand scores in the same slot.
15314/// That is what discriminates the defect — with the kind missing from the
15315/// set the numeric form drops while the identifier form does not — and it
15316/// gives a failure message naming both spellings.
15317/// `every_operand_scores_its_recorded_values` then pins the absolute
15318/// numbers, so a regression moving *both* sides equally still fails, and
15319/// carries the cross-metric anchor `.claude/rules/grammar-dispatch.md` §8
15320/// asks for.
15321///
15322/// Two slots per language, because the sets feed two independent walker
15323/// paths (grammar-dispatch §11) and Perl's defect showed in both: the
15324/// operands of a `&&` chain, and the predicate of an `if`. A fixture of
15325/// only the first leaves `perl_count_condition` and `ruby_count_condition`
15326/// untested.
15327///
15328/// The recorded cyclomatic figure is the same 3 for both slots — one file
15329/// space, one function space, one decision — while conditions differ (2
15330/// for the chain, 1 for the predicate, since ABC scores an `if` through
15331/// its predicate rather than the keyword). So §8's `conditions ==
15332/// cyclomatic - 1` identity is chain-specific arithmetic, not a law; what
15333/// generalises is that cyclomatic must not move when only the operand
15334/// spelling does, which is what makes a `conditions` move unambiguously
15335/// ABC's.
15336///
15337/// `for_each_case` guards the two ways this table could decay into
15338/// asserting nothing: an emptied `numerics` slice, and a template that
15339/// loses its `{}` slot (which would make every comparison `x == x`).
15340/// Both were reachable in the first draft and neither failed a test.
15341#[cfg(test)]
15342#[cfg(any(
15343    feature = "ruby",
15344    feature = "elixir",
15345    feature = "perl",
15346    feature = "python",
15347    feature = "lua",
15348    feature = "javascript",
15349    feature = "php",
15350    feature = "groovy",
15351    feature = "c",
15352    feature = "cpp",
15353    feature = "mozcpp",
15354    feature = "objc"
15355))]
15356mod numeric_bool_operands {
15357    use crate::test_support::metrics_verbatim;
15358    use crate::{LANG, MetricsOptions};
15359
15360    /// One fixture shape: a source template with a `{}` operand slot, and
15361    /// the `abc.conditions_sum` / `cyclomatic_sum` every spelling of that
15362    /// operand must produce.
15363    type Slot = (&'static str, u64, u64);
15364
15365    /// A language's two slots, its identifier baseline operand, the
15366    /// truthy literal operands that must score the same, and how many of
15367    /// those there should be. Every row but the C family's lists numeric
15368    /// literals only; that one adds `char_literal`, which is a second
15369    /// integral-literal kind rather than a second numeric spelling.
15370    ///
15371    /// The count is not bookkeeping. `for_each_case` counts *languages*,
15372    /// so trimming a row's operand list back to `&["1"]` — which is
15373    /// exactly the pre-#1379 fixture — left the whole module green when
15374    /// measured. Pinning the length makes that a deliberate two-line
15375    /// edit instead of a silent one.
15376    type Case = ([Slot; 2], &'static str, &'static [&'static str], usize);
15377
15378    fn conditions(lang: LANG, source: &str) -> u64 {
15379        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
15380            .abc
15381            .conditions_sum()
15382    }
15383
15384    fn cyclomatic_sum(lang: LANG, source: &str) -> u64 {
15385        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
15386            .cyclomatic
15387            .cyclomatic_sum()
15388    }
15389
15390    /// `([chain_slot, condition_slot], identifier, numerics)` per language.
15391    ///
15392    /// `{}` is the right-hand operand of a two-operand short-circuit chain
15393    /// in the first slot and the whole `if` predicate in the second, so
15394    /// every fixture differs from its own baseline in exactly one token.
15395    ///
15396    /// Each row lists **one spelling per numeric kind the grammar emits**,
15397    /// which is the unit that matters: #1379's misses were sibling rules
15398    /// under a shared supertype, not aliases of one rule.
15399    ///
15400    /// - Ruby: `integer` / `float` / `rational` / `complex`. `1r` parses
15401    ///   to `rational(integer)`, `2i` to `complex(integer)` and `1ri` to
15402    ///   `complex(rational(integer))` — verified by `bca dump` — and the
15403    ///   walker cannot descend into any wrapper, so listing the wrapper
15404    ///   is the only way each scores at all. Radix prefixes and `_`
15405    ///   separators fold into `integer`.
15406    /// - Perl: all five members of the hidden `_numeric_literals` choice.
15407    ///   `0xff` and `1.5e10` are here because the first cut of #1379
15408    ///   named only `integer` and `floating_point` and left them scoring
15409    ///   1 against an identifier's 2. `017` is *not* a sixth row — it
15410    ///   lexes as `integer`, and the `octal` kind is unreachable.
15411    /// - Elixir: `integer` / `float` / `char`. `?a` is the codepoint 97,
15412    ///   a numeric literal wearing a sigil.
15413    /// - PHP: `integer` / `float`. Every radix prefix, `_` separator and
15414    ///   exponent folds into those two, and the `float` *type* keyword
15415    ///   `Float2` is a third id rendering to the same name that must
15416    ///   stay out of the set (#1410).
15417    /// - Groovy: one consolidated `number_literal`, type suffixes
15418    ///   (`1L`, `1.5f`, `1G`) included.
15419    /// - C, C++, Mozcpp and Objective-C: one consolidated
15420    ///   `number_literal`, plus `char_literal` — not a numeric spelling
15421    ///   but a second integral-literal *kind*, since `'c'` has type
15422    ///   `int` in C and `char` in C++ and is truthy for the same reason.
15423    ///   All four share one name-keyed set and so run the same fixture,
15424    ///   which is the only coverage `Mozcpp` can have: it owns no file
15425    ///   extension, and `metrics_verbatim` is the one entry point that
15426    ///   reaches a `LANG` without going through one.
15427    ///
15428    /// Python, Lua and JavaScript were already correct and ride along as
15429    /// controls — they are what the first three were measured against,
15430    /// and a future edit that breaks them fails here too. Each folds
15431    /// every radix into one `Number` / `integer` kind, checked by
15432    /// measurement.
15433    fn cases(lang: LANG) -> Option<Case> {
15434        Some(match lang {
15435            LANG::Ruby => (
15436                [
15437                    ("def f(a)\n  a && {}\nend\n", 2, 3),
15438                    ("def f\n  if {}\n    1\n  end\nend\n", 1, 3),
15439                ],
15440                "b",
15441                &["1", "1.0", "1r", "2i", "1ri"],
15442                5,
15443            ),
15444            LANG::Elixir => (
15445                [
15446                    ("def f(a) do\n  a && {}\nend\n", 2, 3),
15447                    ("def f() do\n  if {} do\n    1\n  end\nend\n", 1, 3),
15448                ],
15449                "b",
15450                &["1", "1.0", "?a"],
15451                3,
15452            ),
15453            LANG::Perl => (
15454                [
15455                    ("sub f {\n  my $x = $a && {};\n}\n", 2, 3),
15456                    ("sub f {\n  if ({}) { 1; }\n}\n", 1, 3),
15457                ],
15458                "$b",
15459                &["1", "1.0", "1.5e10", "0xff"],
15460                4,
15461            ),
15462            LANG::Python => (
15463                [
15464                    ("def f(a):\n    return a and {}\n", 2, 3),
15465                    ("def f():\n    if {}:\n        return 1\n", 1, 3),
15466                ],
15467                "b",
15468                &["1", "1.0", "0xff", "1j"],
15469                4,
15470            ),
15471            LANG::Lua => (
15472                [
15473                    ("function f(a)\n  return a and {}\nend\n", 2, 3),
15474                    ("function f()\n  if {} then return 1 end\nend\n", 1, 3),
15475                ],
15476                "b",
15477                &["1", "1.0", "0xff"],
15478                3,
15479            ),
15480            LANG::Javascript => (
15481                [
15482                    ("function f(a) {\n  return a && {};\n}\n", 2, 3),
15483                    ("function f() {\n  if ({}) { return 1; }\n}\n", 1, 3),
15484                ],
15485                "b",
15486                &["1", "1.0", "0xff"],
15487                3,
15488            ),
15489            LANG::Php => (
15490                [
15491                    ("<?php\nfunction f($a) {\n  return $a && {};\n}\n", 2, 3),
15492                    ("<?php\nfunction f() {\n  if ({}) { return 1; }\n}\n", 1, 3),
15493                ],
15494                "$b",
15495                &["1", "1.0"],
15496                2,
15497            ),
15498            LANG::Groovy => (
15499                [
15500                    ("def f(a) {\n  return a && {}\n}\n", 2, 3),
15501                    ("def f() {\n  if ({}) { return 1 }\n}\n", 1, 3),
15502                ],
15503                "b",
15504                &["1"],
15505                1,
15506            ),
15507            LANG::C | LANG::Cpp | LANG::Mozcpp | LANG::Objc => (
15508                [
15509                    ("int f(int a) {\n  return a && {};\n}\n", 2, 3),
15510                    ("int f() {\n  if ({}) { return 1; }\n}\n", 1, 3),
15511                ],
15512                "b",
15513                &["1", "'c'"],
15514                2,
15515            ),
15516            _ => return None,
15517        })
15518    }
15519
15520    /// Runs `check` once per enabled language that has a case, having
15521    /// first established that the case can still assert something.
15522    ///
15523    /// Three guards, all of which had to be added after the first draft
15524    /// shipped without them and a measured perturbation of each left the
15525    /// whole module green:
15526    ///
15527    /// - **`checked > 0`** is the non-vacuity half of the rule in
15528    ///   `.claude/rules/testing.md`. The `#[cfg(any(feature = …))]` on
15529    ///   this module makes the tests *absent* when no truthy-valued
15530    ///   language is compiled in; this catches the residual case where
15531    ///   the runtime `is_enabled()` check stops agreeing with the feature
15532    ///   it compiled under.
15533    /// - **the `numerics` list is non-empty and keeps its recorded
15534    ///   length** — `checked` counts *languages*, so trimming a row's
15535    ///   numeric list back to `&["1"]` (the pre-#1379 fixture) or
15536    ///   emptying it left every test passing when measured, with that
15537    ///   language's coverage deleted. Two assertions, because the
15538    ///   length check passes an emptied row whose count was zeroed with
15539    ///   it.
15540    /// - **every template keeps its `{}`** — without the slot,
15541    ///   `str::replace` is a no-op, baseline and candidate are computed
15542    ///   from the same string, and the comparison degenerates to
15543    ///   `x == x`.
15544    ///
15545    /// Sharing one driver means no test can lose any of the three.
15546    fn for_each_case(check: impl Fn(LANG, Case)) {
15547        let mut checked = 0;
15548        for lang in LANG::into_enum_iter() {
15549            if !lang.is_enabled() {
15550                continue;
15551            }
15552            let Some(case @ (slots, _, numerics, expected_kinds)) = cases(lang) else {
15553                continue;
15554            };
15555            // Emptying a row is the half the length check below cannot
15556            // see: `&[]` against an `expected_kinds` of 0 agrees with
15557            // itself, the inner loops run zero times, and `checked` —
15558            // which counts languages, not spellings — still reads
15559            // satisfied.
15560            assert!(
15561                !numerics.is_empty(),
15562                "{lang:?}: the numeric-operand list is empty; this language asserted nothing"
15563            );
15564            assert_eq!(
15565                numerics.len(),
15566                expected_kinds,
15567                "{lang:?}: the numeric-operand list no longer covers one spelling \
15568                 per grammar numeric kind"
15569            );
15570            for (template, _, _) in slots {
15571                assert!(
15572                    template.contains("{}"),
15573                    "{lang:?}: template lost its `{{}}` operand slot: {template}"
15574                );
15575            }
15576            check(lang, case);
15577            checked += 1;
15578        }
15579        assert!(
15580            checked > 0,
15581            "no truthy-valued language enabled; this test asserted nothing"
15582        );
15583    }
15584
15585    #[test]
15586    fn a_numeric_operand_scores_like_an_identifier_operand() {
15587        for_each_case(|lang, (slots, identifier, numerics, _)| {
15588            for (template, _, _) in slots {
15589                let baseline = conditions(lang, &template.replace("{}", identifier));
15590                for numeric in numerics {
15591                    let source = template.replace("{}", numeric);
15592                    let scored = conditions(lang, &source);
15593                    assert_eq!(
15594                        scored, baseline,
15595                        "{lang:?}: `{numeric}` scored {scored} unary conditions against \
15596                         `{identifier}`'s {baseline}\n  source: {source}"
15597                    );
15598                }
15599            }
15600        });
15601    }
15602
15603    /// The absolute anchor under the comparison above: every operand
15604    /// spelling must produce the slot's recorded `conditions`, and must
15605    /// leave `cyclomatic` alone. The second half is what rules out a
15606    /// regression that moved both metrics together.
15607    #[test]
15608    fn every_operand_scores_its_recorded_values() {
15609        for_each_case(|lang, (slots, identifier, numerics, _)| {
15610            for (template, expected_conditions, expected_cyclomatic) in slots {
15611                for operand in std::iter::once(identifier).chain(numerics.iter().copied()) {
15612                    let source = template.replace("{}", operand);
15613                    assert_eq!(
15614                        conditions(lang, &source),
15615                        expected_conditions,
15616                        "{lang:?}: `{operand}` conditions\n  source: {source}"
15617                    );
15618                    assert_eq!(
15619                        cyclomatic_sum(lang, &source),
15620                        expected_cyclomatic,
15621                        "{lang:?}: `{operand}` cyclomatic_sum\n  source: {source}"
15622                    );
15623                }
15624            }
15625        });
15626    }
15627}
15628
15629/// A relational construct with its own grammar production must score
15630/// like an identifier in the same boolean slot (#1449, #1461).
15631///
15632/// Every language here spells at least one boolean test as a dedicated
15633/// node rather than as a `binary_expression`, so no comparison-token
15634/// arm ever sees it. A kind missing from `<lang>_bool_terminal_kinds!()`
15635/// scores **zero**, silently, and zero is indistinguishable from a
15636/// construct that legitimately scores zero — which is why the headline
15637/// claim of each case is a *comparison* against an identifier control
15638/// in the identical slot rather than an absolute number.
15639///
15640/// The constructs, and the route each fix took:
15641///
15642/// - **Groovy** `a in l` / `a !in l` (`membership_expression`) joins
15643///   the terminal set. The other four — `a === b` / `a !== b`
15644///   (`identity_expression`) and `s =~ /p/` / `s ==~ /p/`
15645///   (`regex_find_expression`, `regex_match_expression`) — are counted
15646///   as operator tokens in `groovy_count_token_condition` instead, so
15647///   they also score outside a boolean slot as `==` already did. Both
15648///   macro and arm carry the reasoning; the rule that matters here is
15649///   that a construct takes exactly one of the two routes, never both
15650///   (`.claude/rules/grammar-dispatch.md` §5).
15651/// - **Perl** `/^#/` (`pattern_matcher`) and `m{^#}`
15652///   (`pattern_matcher_m`), the two spellings of a match against the
15653///   implicit `$_`. They are sibling rules, not aliases, so both are
15654///   listed — a fixture carrying only the first would have left the
15655///   `m{}` half at zero and read as covered.
15656/// - **Ruby** `a in Integer` (`test_pattern`), the one-line pattern
15657///   test. Decided against `match_pattern` (`expr => pat`), which
15658///   raises rather than yielding a boolean.
15659/// - **Objective-C** `@available(iOS 13.0, *)` and its
15660///   `__builtin_available` synonym (`available_expression`), the runtime
15661///   OS-version check. Not relational, but boolean by definition and
15662///   invisible to every comparison-token arm, so it measured short in
15663///   exactly the same way (#1457). Its entry lands in the *name*-keyed
15664///   `cpp_bool_terminal_kinds!()` that C, C++ and Mozcpp share, which is
15665///   why `the_c_family_grammars_do_not_emit_available_expression` below
15666///   pins the inertness the other three rely on.
15667///
15668/// Rust's `matches!(…)` measures short in the same way, and is
15669/// deliberately **not** here: its fix is `macro_invocation`, which also
15670/// catches `cfg!` / `dbg!` / `todo!`, and the one corpus snapshot it
15671/// moves is a `cfg!` rather than a `matches!`. That breadth is what
15672/// #1461 item 2 exists to decide, so it is left to that issue rather
15673/// than settled by inclusion here.
15674///
15675/// Two slots per language, because the sets feed two independent walker
15676/// paths (§11) and every construct above measured short in **both**: the
15677/// operands of a `&&` chain, and the predicate of an `if`. A fixture of
15678/// only one leaves the other untested.
15679///
15680/// Each row also carries the construct's negated spelling. That is not
15681/// redundancy: `<lang>_inspect_container` is a *third* consumer of every
15682/// terminal set, reached only after a `(…)` or `!…` wrapper is peeled,
15683/// and it is the consumer that decides whether the peeled operand sits
15684/// in boolean context at all. Without a negated row, Groovy's and
15685/// Rust's `inspect_container` arms were the site no fixture here
15686/// exercised.
15687///
15688/// Even with it, no language exercises all three consumers through
15689/// these two slots, and which one is missed differs by language —
15690/// Perl's `perl_count_condition` (reached only from a ternary or a
15691/// C-style `for` header) and Ruby's `ruby_count_unary_conditions`
15692/// (unreachable here because `in` binds looser than `&&`, so the chain
15693/// slot must parenthesise and routes through `inspect_container`
15694/// instead). Both are covered elsewhere in this file; the gap is
15695/// recorded rather than papered over, because a reader comparing the
15696/// slot count to the consumer count will otherwise assume it is three.
15697///
15698/// The `cyclomatic` half of `every_construct_scores_its_recorded_values`
15699/// is what rules out a regression that moved both metrics together:
15700/// cyclomatic must not move when only the operand spelling does, which
15701/// is what makes a `conditions` move unambiguously ABC's. The recorded
15702/// figures differ per slot (Rust's chain slot carries the extra `&&`
15703/// decision), so each slot pins its own pair rather than sharing one.
15704// Gated on the union of the three languages with rows, so a build
15705// enabling none of them drops the module instead of failing its
15706// guards (`.claude/rules/testing.md`, #1286 / #1411).
15707#[cfg(test)]
15708#[cfg(any(
15709    feature = "groovy",
15710    feature = "perl",
15711    feature = "ruby",
15712    feature = "objc"
15713))]
15714mod own_production_bool_constructs {
15715    use crate::test_support::metrics_verbatim;
15716    use crate::{LANG, MetricsOptions};
15717
15718    /// One fixture shape: a source template with a `{}` slot for the
15719    /// whole boolean expression, and the `abc.conditions_sum` /
15720    /// `cyclomatic_sum` every spelling in that slot must produce.
15721    type Slot = (&'static str, u64, u64);
15722
15723    /// A language's two slots, its identifier control, the constructs
15724    /// that must score the same, and how many of those there should be.
15725    ///
15726    /// The count is not bookkeeping. `for_each_case` counts *languages*,
15727    /// so trimming Groovy's list back to a single construct — or
15728    /// dropping Perl's `m{}` half, which is a separate grammar rule from
15729    /// its `//` half — leaves the module green with that coverage
15730    /// deleted. Pinning the length makes such a trim a deliberate
15731    /// two-line edit instead of a silent one.
15732    type Case = ([Slot; 2], &'static str, &'static [&'static str], usize);
15733
15734    fn conditions(lang: LANG, source: &str) -> u64 {
15735        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
15736            .abc
15737            .conditions_sum()
15738    }
15739
15740    fn cyclomatic_sum(lang: LANG, source: &str) -> u64 {
15741        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
15742            .cyclomatic
15743            .cyclomatic_sum()
15744    }
15745
15746    /// `([chain_slot, predicate_slot], identifier, constructs)` per
15747    /// language.
15748    ///
15749    /// `{}` is the left-hand operand of a two-operand short-circuit
15750    /// chain in the first slot and the whole `if` predicate in the
15751    /// second, so every fixture differs from its own control in exactly
15752    /// the construct under test.
15753    ///
15754    /// Ruby's chain slot parenthesises the slot because `in` binds
15755    /// looser than `&&` there; the control is parenthesised identically,
15756    /// so the parens cancel out of the comparison and
15757    /// `ruby_inspect_container` unwraps them for both sides alike.
15758    fn cases(lang: LANG) -> Option<Case> {
15759        Some(match lang {
15760            LANG::Groovy => (
15761                [
15762                    ("def f(a, b, l, s) {\n  return {} && b\n}\n", 2, 3),
15763                    ("def f(a, b, l, s) {\n  if ({}) { return 1 }\n}\n", 1, 3),
15764                ],
15765                "b",
15766                &[
15767                    "a in l",
15768                    "a !in l",
15769                    "a === b",
15770                    "a !== b",
15771                    "s =~ /p/",
15772                    "s ==~ /p/",
15773                    "!(a in l)",
15774                    // The indexing / navigation kinds added by #1466.
15775                    // These two are the cyclomatic-neutral ones, so
15776                    // they belong in this module's same-as-the-control
15777                    // shape. The three *null-safe* spellings (`a?.b`,
15778                    // `a??.b`, `l?[0]`) each short-circuit on a null
15779                    // receiver and so add a cyclomatic decision, which
15780                    // a row whose contract is "the control's
15781                    // cyclomatic" cannot express; they get their own
15782                    // test,
15783                    // `groovy_safe_navigation_closes_the_two_below_gap`,
15784                    // which anchors them on that axis explicitly.
15785                    "l[0]",
15786                    "a.@b",
15787                ],
15788                9,
15789            ),
15790            LANG::Perl => (
15791                [
15792                    ("sub f {\n  my $x = {} && $b;\n}\n", 2, 3),
15793                    ("sub f {\n  if ({}) { 1; }\n}\n", 1, 3),
15794                ],
15795                "$a",
15796                &["/^#/", "m{^#}", "!/^#/"],
15797                3,
15798            ),
15799            LANG::Ruby => (
15800                [
15801                    ("def f(a)\n  ({}) && b\nend\n", 2, 3),
15802                    ("def f(a)\n  if {}\n    1\n  end\nend\n", 1, 3),
15803                ],
15804                "a",
15805                &["a in Integer", "!(a in Integer)"],
15806                2,
15807            ),
15808            // Objective-C's `@available` is not relational like the three
15809            // above, but it reaches the terminal set by the same route: a
15810            // dedicated `available_expression` production whose value is a
15811            // boolean, seen by no comparison-token arm (#1457). Both
15812            // spellings are listed because they are alternatives of one
15813            // rule's leading token rather than two rules — so unlike
15814            // Perl's `m{}` above they cannot drift apart, and the second
15815            // row is here to keep that claim measured rather than assumed.
15816            LANG::Objc => (
15817                [
15818                    ("int f(int a) {\n  return {} && b;\n}\n", 2, 3),
15819                    ("int f() {\n  if ({}) { return 1; }\n}\n", 1, 3),
15820                ],
15821                "a",
15822                &[
15823                    "@available(iOS 13.0, *)",
15824                    "__builtin_available(macOS 10.15, *)",
15825                    "!@available(iOS 13.0, *)",
15826                ],
15827                3,
15828            ),
15829            _ => return None,
15830        })
15831    }
15832
15833    /// Runs `check` once per enabled language that has a case, having
15834    /// first established that the case can still assert something.
15835    ///
15836    /// The four guards mirror `numeric_bool_operands::for_each_case`,
15837    /// where each was added only after a measured perturbation of it
15838    /// left that module green: `checked > 0` for the runtime half of
15839    /// the feature gate, a non-empty construct list and its recorded
15840    /// count so a trimmed row cannot vanish silently, and the `{}` slot
15841    /// so `str::replace` does not degenerate into comparing a string
15842    /// with itself.
15843    fn for_each_case(check: impl Fn(LANG, Case)) {
15844        let mut checked = 0;
15845        for lang in LANG::into_enum_iter() {
15846            if !lang.is_enabled() {
15847                continue;
15848            }
15849            let Some(case @ (slots, _, constructs, expected_constructs)) = cases(lang) else {
15850                continue;
15851            };
15852            // Both halves are needed, and the second does not imply the
15853            // first. The length check alone only asserts that the list
15854            // and its recorded count *agree*, so emptying a row and
15855            // setting its count to 0 — the shape a careless "the test
15856            // failed, fix the number" edit takes — deleted that
15857            // language's coverage with both tests still green when
15858            // measured.
15859            assert!(
15860                !constructs.is_empty(),
15861                "{lang:?}: the construct list is empty; this language asserted nothing"
15862            );
15863            assert_eq!(
15864                constructs.len(),
15865                expected_constructs,
15866                "{lang:?}: the construct list no longer covers every own-production \
15867                 boolean spelling this language was fixed for"
15868            );
15869            for (template, _, _) in slots {
15870                assert!(
15871                    template.contains("{}"),
15872                    "{lang:?}: template lost its `{{}}` slot: {template}"
15873                );
15874            }
15875            check(lang, case);
15876            checked += 1;
15877        }
15878        assert!(
15879            checked > 0,
15880            "no language with an own-production boolean construct enabled; \
15881             this test asserted nothing"
15882        );
15883    }
15884
15885    #[test]
15886    fn an_own_production_construct_scores_like_an_identifier() {
15887        for_each_case(|lang, (slots, identifier, constructs, _)| {
15888            for (template, _, _) in slots {
15889                let baseline = conditions(lang, &template.replace("{}", identifier));
15890                for construct in constructs {
15891                    let source = template.replace("{}", construct);
15892                    let scored = conditions(lang, &source);
15893                    assert_eq!(
15894                        scored, baseline,
15895                        "{lang:?}: `{construct}` scored {scored} conditions against \
15896                         `{identifier}`'s {baseline}\n  source: {source}"
15897                    );
15898                }
15899            }
15900        });
15901    }
15902
15903    /// Groovy's three null-safe spellings, on the cyclomatic axis.
15904    ///
15905    /// They cannot ride the rows above, whose contract is "scores the
15906    /// control's `conditions` *and* the control's `cyclomatic`":
15907    /// `groovy_bool_terminal_kinds!()` does not move cyclomatic, but
15908    /// `?.` (`QMARKDOT`), `??.` (`QMARKQMARKDOT`) and `?[`
15909    /// (`QMARKLBRACK`) are already cyclomatic decisions in their own
15910    /// right (`src/metrics/cyclomatic/groovy.rs`), so each spelling
15911    /// scores the control's conditions against the control's cyclomatic
15912    /// **plus one**. That is the whole reason this family was the worst
15913    /// case in #1466: before the fix ABC sat *two* below its own
15914    /// decision count on `if (a?.b)`, against one below for every other
15915    /// spelling.
15916    ///
15917    /// `l?[0]` joined the family in #1471. It sat in the neutral rows
15918    /// above until then, passing only because Groovy cyclomatic had no
15919    /// `?[` arm — a fixture that would have rejected the fix for the
15920    /// very gap it recorded.
15921    ///
15922    /// Asserting the offset rather than the bare conditions is what
15923    /// makes this a §5 double-count guard as well. If a later change
15924    /// added `QMARKDOT` to `groovy_count_token_condition`'s arm while
15925    /// the wrapper stayed in the terminal set, `conditions` would go to
15926    /// `control + 1` and this test — not a snapshot — would say so.
15927    #[test]
15928    #[cfg(feature = "groovy")]
15929    fn groovy_safe_navigation_closes_the_two_below_gap() {
15930        for (template, control_conditions, control_cyclomatic) in [
15931            ("def f(a, b, l) {\n  return {} && b\n}\n", 2, 3),
15932            ("def f(a, b, l) {\n  if ({}) { return 1 }\n}\n", 1, 3),
15933        ] {
15934            let control = template.replace("{}", "b");
15935            assert_eq!(conditions(LANG::Groovy, &control), control_conditions);
15936            assert_eq!(cyclomatic_sum(LANG::Groovy, &control), control_cyclomatic);
15937
15938            for spelling in ["a?.b", "a??.b", "l?[0]"] {
15939                let source = template.replace("{}", spelling);
15940                assert_eq!(
15941                    conditions(LANG::Groovy, &source),
15942                    control_conditions,
15943                    "`{spelling}` conditions\n  source: {source}"
15944                );
15945                assert_eq!(
15946                    cyclomatic_sum(LANG::Groovy, &source),
15947                    control_cyclomatic + 1,
15948                    "`{spelling}` cyclomatic_sum: the navigation operator's own \
15949                     decision has moved\n  source: {source}"
15950                );
15951            }
15952        }
15953    }
15954
15955    /// The `!` operand is read by field, so an `extra` cannot hide it.
15956    ///
15957    /// `groovy_wrapper_operand` took `child(1)` until #1466, which is
15958    /// the `!` operand only when nothing sits between the two. A comment
15959    /// is an `extra` and occupies that slot, so `if (! /*c*/ a)` scored
15960    /// zero conditions where `if (!a)` scored one — the positional-read
15961    /// class grammar-dispatch §3 is about, and the observable half of
15962    /// the peel rewrite.
15963    ///
15964    /// `parenthesized_expression` names nothing in node-types.json, so
15965    /// it keeps the positional read and keeps the bug; that half is
15966    /// pinned here as a *measured* gap rather than left to be discovered
15967    /// as a surprise. Kotlin records the identical pair
15968    /// (`kotlin_wrapper_operand`).
15969    #[test]
15970    #[cfg(feature = "groovy")]
15971    fn groovy_negation_operand_survives_an_interposed_comment() {
15972        let template = "def f(a) {\n  if ({}) { return 1 }\n}\n";
15973
15974        assert_eq!(conditions(LANG::Groovy, &template.replace("{}", "!a")), 1);
15975        assert_eq!(
15976            conditions(LANG::Groovy, &template.replace("{}", "! /*c*/ a")),
15977            1,
15978            "the `!` operand is being read positionally again; a comment displaces it"
15979        );
15980
15981        assert_eq!(conditions(LANG::Groovy, &template.replace("{}", "(a)")), 1);
15982        assert_eq!(
15983            conditions(LANG::Groovy, &template.replace("{}", "( /*c*/ a)")),
15984            0,
15985            "`parenthesized_expression` now survives an interposed comment — if the \
15986             grammar gained a field for its inner expression, read it and delete this"
15987        );
15988    }
15989
15990    /// The arithmetic unary operators stay out of the boolean slot.
15991    ///
15992    /// `groovy_count_condition` routed every `unary_expression` to the
15993    /// peel while the peel handled only the `!` spelling, so the arm
15994    /// claimed `~a` / `-a` / `+a` and dropped them
15995    /// (grammar-dispatch §7). #1466 made the arm ask the peel, which
15996    /// removes the divergence without moving a number — so this test
15997    /// cannot be verified by reverting the production change, and is
15998    /// here to pin the *answer* those three spellings give against a
15999    /// future peel that starts accepting them by accident.
16000    #[test]
16001    #[cfg(feature = "groovy")]
16002    fn groovy_arithmetic_unary_is_not_a_condition() {
16003        let template = "def f(a) {\n  if ({}) { return 1 }\n}\n";
16004
16005        assert_eq!(conditions(LANG::Groovy, &template.replace("{}", "a")), 1);
16006        for spelling in ["~a", "-a", "+a"] {
16007            assert_eq!(
16008                conditions(LANG::Groovy, &template.replace("{}", spelling)),
16009                0,
16010                "`{spelling}` is arithmetic, not a boolean operand"
16011            );
16012        }
16013    }
16014
16015    /// The two Perl spellings must remain two distinct grammar kinds.
16016    ///
16017    /// `perl_bool_terminal_kinds!()` lists `PatternMatcher` **and**
16018    /// `PatternMatcherM` on the stated grounds that `/^#/` and `m{^#}`
16019    /// are sibling rules rather than aliases of one. Nothing above pins
16020    /// that: if a grammar bump collapsed `m{}` onto `pattern_matcher`,
16021    /// both fixtures would keep scoring through the surviving entry,
16022    /// the `PatternMatcherM` arm would become dead code, and every test
16023    /// in this module would stay green — the silent-drift shape lesson
16024    /// 34 and grammar-dispatch §2 exist to catch.
16025    ///
16026    /// Asserting the ids differ is not enough on its own, since two
16027    /// distinct enum variants prove nothing about what the parser
16028    /// emits. So each spelling is parsed and required to produce its
16029    /// own kind and not the other's.
16030    #[test]
16031    #[cfg(feature = "perl")]
16032    fn the_two_perl_match_spellings_are_distinct_kinds() {
16033        use crate::{ParserTrait, Perl, PerlParser};
16034        use big_code_analysis_ast::test_support::ast_has_kind_id;
16035
16036        assert_ne!(
16037            Perl::PatternMatcher as u16,
16038            Perl::PatternMatcherM as u16,
16039            "the two spellings share one kind id; one terminal-set entry is dead"
16040        );
16041
16042        for (src, present, absent) in [
16043            (
16044                "sub f { if (/^#/) { 1; } }",
16045                ("pattern_matcher", Perl::PatternMatcher as u16),
16046                ("pattern_matcher_m", Perl::PatternMatcherM as u16),
16047            ),
16048            (
16049                "sub f { if (m{^#}) { 1; } }",
16050                ("pattern_matcher_m", Perl::PatternMatcherM as u16),
16051                ("pattern_matcher", Perl::PatternMatcher as u16),
16052            ),
16053        ] {
16054            let parser = PerlParser::new(
16055                src.as_bytes().to_vec(),
16056                &std::path::PathBuf::from("f.pl"),
16057                None,
16058            );
16059            let (present_name, present_id) = present;
16060            let (absent_name, absent_id) = absent;
16061            assert!(
16062                ast_has_kind_id(&parser, present_id),
16063                "`{src}` no longer emits `{present_name}`"
16064            );
16065            assert!(
16066                !ast_has_kind_id(&parser, absent_id),
16067                "`{src}` now also emits `{absent_name}`; the two spellings have \
16068                 collapsed and one `perl_bool_terminal_kinds!()` entry is dead"
16069            );
16070        }
16071    }
16072
16073    /// `"available_expression"` must stay an Objective-C-only kind.
16074    ///
16075    /// Alone among the terminal sets, `cpp_bool_terminal_kinds!()` keys
16076    /// on node-kind *names* so that C, C++, Mozcpp and Objective-C can
16077    /// share one list despite assigning different ids to the same kinds
16078    /// (#720 / #732). The price is that every entry is live in all four:
16079    /// the Objective-C rows above say nothing about what the addition
16080    /// did to the other three, and a grammar bump that gave any of them
16081    /// an `available_expression` node would start counting it with no
16082    /// test anywhere noticing.
16083    ///
16084    /// The Objective-C half is not decoration. Without it a fixture that
16085    /// stopped parsing — or a typo'd kind name — would leave the
16086    /// negative assertions passing for the wrong reason. Both halves
16087    /// were verified by perturbing the probed kind name: to a typo,
16088    /// which fails the positive, and to `if_statement`, which fails the
16089    /// negatives.
16090    #[test]
16091    #[cfg(all(
16092        feature = "objc",
16093        any(feature = "c", feature = "cpp", feature = "mozcpp")
16094    ))]
16095    fn the_c_family_grammars_do_not_emit_available_expression() {
16096        use crate::ParserTrait;
16097
16098        const SOURCE: &str = "int f(int a) {\n  if (@available(iOS 13.0, *)) { return 1; }\n}\n";
16099
16100        fn emits<P: ParserTrait>(path: &str) -> bool {
16101            let parser = P::new(
16102                SOURCE.as_bytes().to_vec(),
16103                &std::path::PathBuf::from(path),
16104                None,
16105            );
16106            parser
16107                .root()
16108                .preorder()
16109                .any(|node| node.kind() == "available_expression")
16110        }
16111
16112        assert!(
16113            emits::<crate::ObjcParser>("f.m"),
16114            "the fixture no longer parses to an `available_expression`; every \
16115             Objective-C row above is now measuring some other node"
16116        );
16117
16118        // The siblings are `#[cfg]`-gated array *elements* rather than
16119        // conditional pushes, so the `any(…)` half of this test's gate
16120        // makes the list non-empty by construction: a build reaching
16121        // this line always has at least one row. That is the same
16122        // non-vacuity guarantee the `checked > 0` counters elsewhere in
16123        // this module buy at runtime, moved to compile time because here
16124        // the row set is fixed rather than iterated over `LANG`.
16125        let c_family = [
16126            #[cfg(feature = "c")]
16127            ("C", emits::<crate::CParser>("f.c")),
16128            #[cfg(feature = "cpp")]
16129            ("C++", emits::<crate::CppParser>("f.cpp")),
16130            #[cfg(feature = "mozcpp")]
16131            ("Mozcpp", emits::<crate::MozcppParser>("f.cpp")),
16132        ];
16133
16134        for (language, emitted) in c_family {
16135            assert!(
16136                !emitted,
16137                "{language} now emits `available_expression`, so the shared \
16138                 `cpp_bool_terminal_kinds!()` entry is no longer inert there \
16139                 and its ABC conditions have moved"
16140            );
16141        }
16142    }
16143
16144    /// The absolute anchor under the comparison above: every spelling
16145    /// must produce the slot's recorded `conditions`, and must leave
16146    /// `cyclomatic` alone.
16147    #[test]
16148    fn every_construct_scores_its_recorded_values() {
16149        for_each_case(|lang, (slots, identifier, constructs, _)| {
16150            for (template, expected_conditions, expected_cyclomatic) in slots {
16151                for spelling in std::iter::once(identifier).chain(constructs.iter().copied()) {
16152                    let source = template.replace("{}", spelling);
16153                    assert_eq!(
16154                        conditions(lang, &source),
16155                        expected_conditions,
16156                        "{lang:?}: `{spelling}` conditions\n  source: {source}"
16157                    );
16158                    assert_eq!(
16159                        cyclomatic_sum(lang, &source),
16160                        expected_cyclomatic,
16161                        "{lang:?}: `{spelling}` cyclomatic_sum\n  source: {source}"
16162                    );
16163                }
16164            }
16165        });
16166    }
16167}
16168
16169/// A Perl statement modifier must score like the block form it is
16170/// shorthand for (#1464).
16171///
16172/// `return 1 if $x;` and `if ($x) { return 1; }` are the same decision
16173/// written two ways, and the modifier is the idiomatic Perl spelling —
16174/// `next unless $ok;`, `warn "..." if $debug;`. Perl's *cyclomatic*
16175/// dispatcher already counted all six modifier kinds, so this was a
16176/// straight ABC undercount against Perl's own decision count rather
16177/// than a policy disagreement between the two metrics.
16178///
16179/// Each pair is asserted equal **and** equal to a literal, so a
16180/// regression that zeroed both spellings still fails. The fixtures are
16181/// spell-anchored because the modifier is the only thing in them that
16182/// scores a condition: deleting `if $x` from the modifier fixture would
16183/// otherwise turn it into a silent copy of nothing at all
16184/// (`.claude/rules/testing.md`, "Perturb the fixture as well as the
16185/// production line").
16186#[cfg(test)]
16187#[cfg(feature = "perl")]
16188mod perl_statement_modifier_parity {
16189    use crate::test_support::{assert_perl_fixture_spells, metrics_verbatim};
16190    use crate::{LANG, MetricsOptions, Perl};
16191
16192    fn conditions(source: &str) -> u64 {
16193        metrics_verbatim(LANG::Perl, source.as_bytes(), MetricsOptions::default())
16194            .abc
16195            .conditions_sum()
16196    }
16197
16198    /// `(keyword, kind id, kind name)` — the four modifier keywords
16199    /// whose grammar slot is a `condition`. `for` / `foreach` is the
16200    /// fifth spelling and is covered by its own test below; `when` is
16201    /// the sixth and has no fixture at all, being unreachable from
16202    /// valid Perl (see `perl_walk_statement_modifier`).
16203    const CONDITION_MODIFIERS: [(&str, u16, &str); 4] = [
16204        ("if", Perl::IfSimpleStatement as u16, "if_simple_statement"),
16205        (
16206            "unless",
16207            Perl::UnlessSimpleStatement as u16,
16208            "unless_simple_statement",
16209        ),
16210        (
16211            "while",
16212            Perl::WhileSimpleStatement as u16,
16213            "while_simple_statement",
16214        ),
16215        (
16216            "until",
16217            Perl::UntilSimpleStatement as u16,
16218            "until_simple_statement",
16219        ),
16220    ];
16221
16222    #[test]
16223    fn a_modifier_scores_like_its_block_form() {
16224        assert!(
16225            LANG::Perl.is_enabled(),
16226            "compiled under `feature = \"perl\"` but `LANG::Perl` reports disabled; \
16227             this test asserted nothing"
16228        );
16229        for (keyword, kind, name) in CONDITION_MODIFIERS {
16230            let modifier = format!("sub f {{ my $x = shift; return 1 {keyword} $x; }}");
16231            let block = format!("sub f {{ my $x = shift; {keyword} ($x) {{ return 1; }} }}");
16232            assert_perl_fixture_spells(&modifier, &[(kind, 1, name)]);
16233            assert_eq!(
16234                conditions(&modifier),
16235                conditions(&block),
16236                "`{keyword}` modifier and block form disagree\n  modifier: {modifier}\n  block:    {block}"
16237            );
16238            assert_eq!(
16239                conditions(&modifier),
16240                1,
16241                "`{keyword}` modifier: the bare scalar predicate is one condition"
16242            );
16243        }
16244    }
16245
16246    /// The modifier's `condition` field goes through the shared
16247    /// condition classifier, not a flat `+1`, so a compound predicate
16248    /// keeps its sub-structure — three shapes the block form already
16249    /// scores above and below one.
16250    #[test]
16251    fn a_modifier_condition_keeps_its_substructure() {
16252        for (predicate, expected) in [("!$x", 1), ("($x)", 1), ("$x && $x", 2), ("$x > 2", 1)] {
16253            let modifier = format!("sub f {{ my $x = shift; return 1 if {predicate}; }}");
16254            let block = format!("sub f {{ my $x = shift; if ({predicate}) {{ return 1; }} }}");
16255            assert_perl_fixture_spells(
16256                &modifier,
16257                &[(Perl::IfSimpleStatement as u16, 1, "if_simple_statement")],
16258            );
16259            assert_eq!(
16260                conditions(&modifier),
16261                expected,
16262                "`if {predicate}` modifier\n  source: {modifier}"
16263            );
16264            assert_eq!(
16265                conditions(&block),
16266                expected,
16267                "`if ({predicate})` block form\n  source: {block}"
16268            );
16269        }
16270    }
16271
16272    /// The `for` / `foreach` modifier iterates a list and is not a
16273    /// boolean test — the grammar names its slot `list`, not
16274    /// `condition`. Pinned against the `if` modifier so the zero reads
16275    /// as a decision rather than as an arm nothing reaches.
16276    ///
16277    /// Measured: simply *adding* `ForSimpleStatement` to the dispatch
16278    /// arm does not fail this test, because the walker reads the slot by
16279    /// field name and `for_simple_statement` exposes no `condition` —
16280    /// the exclusion is structural, not a listed omission. What it does
16281    /// catch is a rework that reaches the slot some other way (a child
16282    /// index, the `list` field) or a grammar that renames the field.
16283    #[test]
16284    fn the_for_modifier_is_not_a_condition() {
16285        for keyword in ["for", "foreach"] {
16286            let source = format!("sub f {{ my $x = shift; print 5 {keyword} @$x; }}");
16287            assert_perl_fixture_spells(
16288                &source,
16289                &[(Perl::ForSimpleStatement as u16, 1, "for_simple_statement")],
16290            );
16291            let abc =
16292                metrics_verbatim(LANG::Perl, source.as_bytes(), MetricsOptions::default()).abc;
16293            assert_eq!(abc.conditions_sum(), 0, "`{keyword}` modifier: {source}");
16294            // The expected value *is* the default, so the zero above
16295            // proves nothing on its own — a fixture that stopped
16296            // parsing would satisfy it too. `my $x = shift` is the
16297            // file's one assignment, and `shift` and `print 5` its two
16298            // branches; all three must survive.
16299            assert_eq!(
16300                abc.assignments_sum(),
16301                1,
16302                "`{keyword}`: fixture stopped scoring"
16303            );
16304            assert_eq!(
16305                abc.branches_sum(),
16306                2,
16307                "`{keyword}`: fixture stopped scoring"
16308            );
16309        }
16310        assert_eq!(
16311            conditions("sub f { my $x = shift; return 1 if $x; }"),
16312            1,
16313            "control: the `if` modifier still counts, so the zero above is a \
16314             policy decision and not a dead arm"
16315        );
16316    }
16317}
16318
16319/// A non-numeric literal in a boolean operand slot must score like an
16320/// identifier in the same slot (#1462).
16321///
16322/// The sibling module above pins the *numeric* half, closed by #1410.
16323/// The non-numeric literals were never swept and were missing from
16324/// every set the numerics were added to — same mechanism, same silent
16325/// zero, same file. Measured before the fix, every row below scored
16326/// exactly one condition short of its identifier control, in **both**
16327/// slots: `x || "default"` — the language's commonest truthy-default
16328/// idiom — reported 1 against `x || y`'s 2.
16329///
16330/// Scope is the eleven truthy-valued sets. #1462 shipped eight of them
16331/// and called that complete; a whole-branch review of the batch found
16332/// Perl, Ruby and Elixir in neither the swept list nor the
16333/// compile-error exemption, each already carrying `False` / `Nil`, and
16334/// each reproducing the gap exactly. They were swept on the same terms
16335/// and are rows here. Tcl and iRules are truthy too and needed nothing:
16336/// their `quoted_word` / `braced_word_simple` / `number` kinds already
16337/// cover every literal an `expr {…}` operand can hold, so they have no
16338/// row rather than a vacuous one.
16339///
16340/// C#, Java, Kotlin, Rust and Go name no literal kind at all and stay
16341/// that way: a bare literal in a boolean slot is a compile error there,
16342/// so there is nothing to count. The C family is integer-truthy and
16343/// does carry the same gap for `string_literal`, but it is the one
16344/// group in that class with integration-corpus exposure, so it is
16345/// deferred rather than decided (see `cpp_bool_terminal_kinds!`).
16346///
16347/// Three things each row pins that a conditions comparison alone
16348/// cannot:
16349///
16350/// - **The `kind_id` each spelling parses to.** This is the whole bug
16351///   class (`.claude/rules/grammar-dispatch.md` §1): the grammars here
16352///   emit `string` under two ids in JavaScript, Mozjs and Tsx and under
16353///   three in Tsx counting the type keyword, and listing the wrong one
16354///   compiles, runs, and returns the unfixed number. Asserting the id
16355///   turns that measurement into a standing claim rather than a note in
16356///   an issue. It is also the fixture anchor `.claude/rules/testing.md`
16357///   asks for — editing `"s"` to `1` in a row now fails by name instead
16358///   of quietly turning that row into a copy of the numeric module.
16359/// - **`cyclomatic` does not move.** Only the operand spelling changes
16360///   between a row and its control, so a `conditions` difference is
16361///   unambiguously ABC's.
16362/// - **Both walker paths.** The sets feed two structurally independent
16363///   consumers (§11) — the operands of a `&&` / `and` chain, and the
16364///   predicate of an `if` — and every row measured short in both. A
16365///   fixture of only one leaves the other path invisible.
16366///
16367/// The JS-family rows go through `js_literals!` rather than four hand-
16368/// written arrays, because the four languages differ in exactly one
16369/// place — which `string` alias an operand slot carries — and making
16370/// that the macro's only argument states the asymmetry instead of
16371/// leaving a reader to diff four near-identical lists for it.
16372#[cfg(test)]
16373#[cfg(any(
16374    feature = "javascript",
16375    feature = "mozjs",
16376    feature = "typescript",
16377    feature = "python",
16378    feature = "lua",
16379    feature = "php",
16380    feature = "groovy",
16381    feature = "perl",
16382    feature = "ruby",
16383    feature = "elixir"
16384))]
16385mod literal_bool_operands {
16386    use crate::test_support::{assert_fixture_spells, metrics_verbatim};
16387    use crate::{LANG, MetricsOptions};
16388
16389    /// One fixture shape: a source template with a `{}` operand slot,
16390    /// and the `abc.conditions_sum` / `cyclomatic_sum` every spelling of
16391    /// that operand must produce.
16392    type Slot = (&'static str, u64, u64);
16393
16394    /// One literal operand: its spelling, and the `kind_id` the grammar
16395    /// must emit for it.
16396    type Literal = (&'static str, u16);
16397
16398    /// A language's two slots, its identifier baseline operand, the
16399    /// literal operands that must score the same, and how many of those
16400    /// there should be.
16401    ///
16402    /// The count is not bookkeeping. `for_each_case` counts *languages*,
16403    /// so trimming a row's literal list — the pre-#1462 state is the
16404    /// empty list — would leave the whole module green.
16405    type Case = ([Slot; 2], &'static str, &'static [Literal], usize);
16406
16407    /// The seven non-numeric literal kinds of a JS-family grammar, with
16408    /// the `string` alias an operand slot carries passed in: `String2`
16409    /// for JavaScript, Mozjs and Tsx, `String` for TypeScript. That one
16410    /// argument is the entire difference between the four rows, and it
16411    /// is the same split `Checker::is_string` makes (§7).
16412    macro_rules! js_literals {
16413        ($Lang:ident, $string:ident) => {
16414            &[
16415                ("\"s\"", crate::$Lang::$string as u16),
16416                ("`t`", crate::$Lang::TemplateString as u16),
16417                ("/re/", crate::$Lang::Regex as u16),
16418                ("null", crate::$Lang::Null as u16),
16419                ("undefined", crate::$Lang::Undefined as u16),
16420                ("{}", crate::$Lang::Object as u16),
16421                ("[]", crate::$Lang::Array as u16),
16422            ]
16423        };
16424    }
16425
16426    const JS_SLOTS: [Slot; 2] = [
16427        ("function f(a) {\n  return a && {};\n}\n", 2, 3),
16428        ("function f() {\n  if ({}) { return 1; }\n}\n", 1, 3),
16429    ];
16430
16431    fn conditions(lang: LANG, source: &str) -> u64 {
16432        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
16433            .abc
16434            .conditions_sum()
16435    }
16436
16437    fn cyclomatic_sum(lang: LANG, source: &str) -> u64 {
16438        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
16439            .cyclomatic
16440            .cyclomatic_sum()
16441    }
16442
16443    /// Asserts the fixture still spells the construct under test, by
16444    /// parsing it with the language's own parser. Split out of `cases`
16445    /// because only the parser types are feature-gated; the `kind_id`
16446    /// enums are compiled unconditionally.
16447    fn assert_spells(lang: LANG, source: &str, kinds: &[(u16, usize, &str)]) {
16448        match lang {
16449            #[cfg(feature = "javascript")]
16450            LANG::Javascript => {
16451                assert_fixture_spells::<crate::JavascriptParser>(source, "f.js", kinds);
16452            }
16453            #[cfg(feature = "mozjs")]
16454            LANG::Mozjs => assert_fixture_spells::<crate::MozjsParser>(source, "f.jsm", kinds),
16455            #[cfg(feature = "typescript")]
16456            LANG::Typescript => {
16457                assert_fixture_spells::<crate::TypescriptParser>(source, "f.ts", kinds);
16458            }
16459            #[cfg(feature = "typescript")]
16460            LANG::Tsx => assert_fixture_spells::<crate::TsxParser>(source, "f.tsx", kinds),
16461            #[cfg(feature = "python")]
16462            LANG::Python => assert_fixture_spells::<crate::PythonParser>(source, "f.py", kinds),
16463            #[cfg(feature = "lua")]
16464            LANG::Lua => assert_fixture_spells::<crate::LuaParser>(source, "f.lua", kinds),
16465            #[cfg(feature = "php")]
16466            LANG::Php => assert_fixture_spells::<crate::PhpParser>(source, "f.php", kinds),
16467            #[cfg(feature = "groovy")]
16468            LANG::Groovy => {
16469                assert_fixture_spells::<crate::GroovyParser>(source, "f.groovy", kinds);
16470            }
16471            #[cfg(feature = "perl")]
16472            LANG::Perl => assert_fixture_spells::<crate::PerlParser>(source, "f.pl", kinds),
16473            #[cfg(feature = "ruby")]
16474            LANG::Ruby => assert_fixture_spells::<crate::RubyParser>(source, "f.rb", kinds),
16475            #[cfg(feature = "elixir")]
16476            LANG::Elixir => assert_fixture_spells::<crate::ElixirParser>(source, "f.ex", kinds),
16477            other => panic!("{other:?} has a case row but no parser arm"),
16478        }
16479    }
16480
16481    /// `([chain_slot, condition_slot], identifier, literals)` per
16482    /// language, one spelling per literal *kind* the grammar emits.
16483    ///
16484    /// - JS family: `string`, `template_string`, `regex`, `null`,
16485    ///   `undefined`, `object`, `array`. TypeScript's `string` / `object`
16486    ///   type keywords (`a: string`) are different ids rendering to the
16487    ///   same names and are deliberately not in the sets, so a row that
16488    ///   accidentally named one would fail the id anchor here.
16489    /// - Python: `string` covers `'s'`, `"""s"""`, `f'x'` and `b'x'`,
16490    ///   while the implicit-join `'a' 'b'` is the separate
16491    ///   `concatenated_string` rule — the sibling-rule-under-a-supertype
16492    ///   shape #1379's Perl numerals were, which an alias sweep cannot
16493    ///   see. Then `none`, the four collection displays, and `ellipsis`.
16494    /// - Lua: one `string` kind for `"s"`, `'s'` and `[[s]]`, plus
16495    ///   `table_constructor`. Lua's is the strongest case of the eight:
16496    ///   everything but `false` and `nil` is truthy, which is why
16497    ///   `cond and "a" or "b"` is the language's ternary.
16498    /// - PHP: `string` (single-quoted) and `encapsed_string`
16499    ///   (interpolating) are separate rules, as are `heredoc` and
16500    ///   `nowdoc`; then `array_creation_expression`, `null`, the
16501    ///   backtick `shell_command_expression`, and `cast_expression` —
16502    ///   the second finding of #1462, PHP being the only set in the
16503    ///   Java / C# / Groovy / PHP group that named no cast kind.
16504    /// - Groovy: `string_literal` (which also covers the slashy `/re/`),
16505    ///   `null_literal`, `list_literal`, `map_literal`.
16506    /// - Perl: the four string productions the grammar keeps separate
16507    ///   (`'s'`, `q()`, `"s"`, `qq()`), the two command substitutions
16508    ///   (`qx()`, backticks), the `qw()` / `[…]` / `{…}` collection
16509    ///   literals, `qr//`, and `special_literal` (`__FILE__`).
16510    ///   `heredoc_initializer` is the twelfth kind and cannot live in
16511    ///   this table — a Perl heredoc body follows the *statement*, not
16512    ///   the operand — so it has its own test below.
16513    /// - Ruby: `string` (covering `"s"`, `'s'`, `%q()`, `%Q()`),
16514    ///   `chained_string` (the adjacent-literal join `"a" "b"`, a
16515    ///   sibling rule an alias sweep cannot see), `subshell`, the four
16516    ///   collection literals `array` / `hash` / `%w[]` / `%i[]`,
16517    ///   `regex`, the one-character `?a`, and both symbol productions.
16518    ///   `heredoc_beginning` is the twelfth and shares the Perl heredoc
16519    ///   test for the same reason.
16520    /// - Elixir: `string` (one kind for `"s"` and the `"""` heredoc),
16521    ///   `charlist`, `sigil` (one kind for `~r//`, `~s()`, `~w()`),
16522    ///   `quoted_atom` — a separate production from `atom`, so
16523    ///   `:"q a"` scored zero while `:atom` scored one — and the four
16524    ///   collection literals `list` / `tuple` / `map` / `bitstring`.
16525    ///
16526    /// Elixir is the one row whose two slots are not two independent
16527    /// consumers: the language has no bare-truthy `if` predicate slot
16528    /// to route, since an Elixir `if` is a keyword-shaped `Call` scoring
16529    /// one whatever its argument. Its second slot negates the operand
16530    /// instead, which reaches the terminal set through
16531    /// `elixir_inspect_container` rather than through the chain
16532    /// walker's own check — a different path to the same set, which is
16533    /// what the second slot exists to exercise.
16534    ///
16535    /// Shapes that measured short and are deliberately absent, none of
16536    /// them a literal: JavaScript's `this` and Groovy's
16537    /// `object_creation_expression` (recorded in #1462); Perl's
16538    /// `s///` and `tr///` (operations on `$_` evaluating to a count),
16539    /// `anonymous_function` and `array_dereference`; Ruby's `lambda`;
16540    /// and Elixir's `anonymous_function` and `&f/1` capture.
16541    fn cases(lang: LANG) -> Option<Case> {
16542        Some(match lang {
16543            LANG::Javascript => (JS_SLOTS, "b", js_literals!(Javascript, String2), 7),
16544            LANG::Mozjs => (JS_SLOTS, "b", js_literals!(Mozjs, String2), 7),
16545            LANG::Typescript => (JS_SLOTS, "b", js_literals!(Typescript, String), 7),
16546            LANG::Tsx => (JS_SLOTS, "b", js_literals!(Tsx, String2), 7),
16547            LANG::Python => (
16548                [
16549                    ("def f(a):\n    return a and {}\n", 2, 3),
16550                    ("def f():\n    if {}:\n        return 1\n", 1, 3),
16551                ],
16552                "b",
16553                &[
16554                    ("'s'", crate::Python::String as u16),
16555                    ("'a' 'b'", crate::Python::ConcatenatedString as u16),
16556                    ("None", crate::Python::None as u16),
16557                    ("[]", crate::Python::List as u16),
16558                    ("{1}", crate::Python::Set as u16),
16559                    ("()", crate::Python::Tuple as u16),
16560                    ("{}", crate::Python::Dictionary as u16),
16561                    ("...", crate::Python::Ellipsis as u16),
16562                ],
16563                8,
16564            ),
16565            LANG::Lua => (
16566                [
16567                    ("function f(a)\n  return a and {}\nend\n", 2, 3),
16568                    ("function f()\n  if {} then return 1 end\nend\n", 1, 3),
16569                ],
16570                "b",
16571                &[
16572                    ("\"s\"", crate::Lua::String as u16),
16573                    ("{}", crate::Lua::TableConstructor as u16),
16574                ],
16575                2,
16576            ),
16577            LANG::Php => (
16578                [
16579                    ("<?php\nfunction f($a) {\n  return $a && {};\n}\n", 2, 3),
16580                    ("<?php\nfunction f() {\n  if ({}) { return 1; }\n}\n", 1, 3),
16581                ],
16582                "$b",
16583                &[
16584                    ("'s'", crate::Php::String as u16),
16585                    ("\"x$b\"", crate::Php::EncapsedString as u16),
16586                    ("<<<EOT\ns\nEOT", crate::Php::Heredoc as u16),
16587                    ("<<<'EOT'\ns\nEOT", crate::Php::Nowdoc as u16),
16588                    ("[]", crate::Php::ArrayCreationExpression as u16),
16589                    ("null", crate::Php::Null as u16),
16590                    ("`ls`", crate::Php::ShellCommandExpression as u16),
16591                    ("(bool)$b", crate::Php::CastExpression as u16),
16592                ],
16593                8,
16594            ),
16595            LANG::Groovy => (
16596                [
16597                    ("def f(a) {\n  return a && {}\n}\n", 2, 3),
16598                    ("def f() {\n  if ({}) { return 1 }\n}\n", 1, 3),
16599                ],
16600                "b",
16601                &[
16602                    ("\"s\"", crate::Groovy::StringLiteral as u16),
16603                    ("null", crate::Groovy::NullLiteral as u16),
16604                    ("[]", crate::Groovy::ListLiteral as u16),
16605                    ("[:]", crate::Groovy::MapLiteral as u16),
16606                ],
16607                4,
16608            ),
16609            LANG::Perl => (
16610                [
16611                    (
16612                        "sub f {\n  my ($a, $b) = @_;\n  if ($a && {}) { print 1; }\n}\n",
16613                        2,
16614                        4,
16615                    ),
16616                    (
16617                        "sub f {\n  my ($a, $b) = @_;\n  if ({}) { print 1; }\n}\n",
16618                        1,
16619                        3,
16620                    ),
16621                ],
16622                "$b",
16623                &[
16624                    ("'s'", crate::Perl::StringSingleQuoted as u16),
16625                    ("q(s)", crate::Perl::StringQQuoted as u16),
16626                    ("\"s\"", crate::Perl::StringDoubleQuoted as u16),
16627                    ("qq(s)", crate::Perl::StringQqQuoted as u16),
16628                    ("qx(ls)", crate::Perl::CommandQxQuoted as u16),
16629                    ("`ls`", crate::Perl::BacktickQuoted as u16),
16630                    ("qw(a b)", crate::Perl::WordListQw as u16),
16631                    ("[1, 2]", crate::Perl::ArrayRef as u16),
16632                    ("{ a => 1 }", crate::Perl::HashRef as u16),
16633                    ("qr/re/", crate::Perl::RegexPatternQr as u16),
16634                    ("__FILE__", crate::Perl::SpecialLiteral as u16),
16635                ],
16636                11,
16637            ),
16638            LANG::Ruby => (
16639                [
16640                    ("def f(a, b)\n  if a && {} then 1 else 0 end\nend\n", 3, 4),
16641                    ("def f(a, b)\n  if {} then 1 else 0 end\nend\n", 2, 3),
16642                ],
16643                "b",
16644                &[
16645                    ("\"s\"", crate::Ruby::String as u16),
16646                    ("\"a\" \"b\"", crate::Ruby::ChainedString as u16),
16647                    ("`ls`", crate::Ruby::Subshell as u16),
16648                    ("[1, 2]", crate::Ruby::Array as u16),
16649                    ("{ a: 1 }", crate::Ruby::Hash as u16),
16650                    ("%w[a b]", crate::Ruby::StringArray as u16),
16651                    ("%i[a b]", crate::Ruby::SymbolArray as u16),
16652                    ("/re/", crate::Ruby::Regex as u16),
16653                    ("?a", crate::Ruby::Character as u16),
16654                    (":sym", crate::Ruby::SimpleSymbol as u16),
16655                    (":\"sym\"", crate::Ruby::DelimitedSymbol as u16),
16656                ],
16657                11,
16658            ),
16659            LANG::Elixir => (
16660                [
16661                    (
16662                        "defmodule M do\n  def f(a, b) do\n    if a && {} do\n      a\n    end\n  end\nend\n",
16663                        3,
16664                        5,
16665                    ),
16666                    (
16667                        "defmodule M do\n  def f(a, b) do\n    if a && !{} do\n      a\n    end\n  end\nend\n",
16668                        3,
16669                        5,
16670                    ),
16671                ],
16672                "b",
16673                &[
16674                    ("\"s\"", crate::Elixir::String as u16),
16675                    ("'c'", crate::Elixir::Charlist as u16),
16676                    ("~r/re/", crate::Elixir::Sigil as u16),
16677                    (":\"q a\"", crate::Elixir::QuotedAtom as u16),
16678                    ("[1, 2]", crate::Elixir::List as u16),
16679                    ("{1, 2}", crate::Elixir::Tuple as u16),
16680                    ("%{a: 1}", crate::Elixir::Map as u16),
16681                    ("<<1>>", crate::Elixir::Bitstring as u16),
16682                ],
16683                8,
16684            ),
16685            _ => return None,
16686        })
16687    }
16688
16689    /// Runs `check` once per enabled language that has a case, having
16690    /// first established that the case can still assert something. The
16691    /// four guards are the sibling module's, for the same four ways
16692    /// this table could decay into asserting nothing: no language
16693    /// enabled, an emptied literal list, a list that no longer covers
16694    /// every kind, and a template that lost its `{}` slot (which makes
16695    /// every comparison `x == x`).
16696    fn for_each_case(check: impl Fn(LANG, Case)) {
16697        let mut checked = 0;
16698        for lang in LANG::into_enum_iter() {
16699            if !lang.is_enabled() {
16700                continue;
16701            }
16702            let Some(case @ (slots, _, literals, expected_kinds)) = cases(lang) else {
16703                continue;
16704            };
16705            // The length check does not imply this one: it asserts only
16706            // that the list and its recorded count *agree*, which an
16707            // emptied row with `expected_kinds` set to 0 satisfies —
16708            // the shape a "the test failed, fix the number" edit takes.
16709            // `checked` counts languages, so that row still increments
16710            // it and the non-vacuity guard below reads satisfied.
16711            assert!(
16712                !literals.is_empty(),
16713                "{lang:?}: the literal-operand list is empty; this language asserted nothing"
16714            );
16715            assert_eq!(
16716                literals.len(),
16717                expected_kinds,
16718                "{lang:?}: the literal-operand list no longer covers one spelling \
16719                 per grammar literal kind"
16720            );
16721            for (template, _, _) in slots {
16722                assert!(
16723                    template.contains("{}"),
16724                    "{lang:?}: template lost its `{{}}` operand slot: {template}"
16725                );
16726            }
16727            check(lang, case);
16728            checked += 1;
16729        }
16730        assert!(
16731            checked > 0,
16732            "no truthy-valued language enabled; this test asserted nothing"
16733        );
16734    }
16735
16736    #[test]
16737    fn a_literal_operand_scores_like_an_identifier_operand() {
16738        for_each_case(|lang, (slots, identifier, literals, _)| {
16739            for (template, _, _) in slots {
16740                let baseline = conditions(lang, &template.replace("{}", identifier));
16741                for (literal, _) in literals {
16742                    let source = template.replace("{}", literal);
16743                    let scored = conditions(lang, &source);
16744                    assert_eq!(
16745                        scored, baseline,
16746                        "{lang:?}: `{literal}` scored {scored} unary conditions against \
16747                         `{identifier}`'s {baseline}\n  source: {source}"
16748                    );
16749                }
16750            }
16751        });
16752    }
16753
16754    /// The absolute anchor under the comparison above: every operand
16755    /// spelling must produce the slot's recorded `conditions`, and must
16756    /// leave `cyclomatic` alone.
16757    #[test]
16758    fn every_literal_operand_scores_its_recorded_values() {
16759        for_each_case(|lang, (slots, identifier, literals, _)| {
16760            for (template, expected_conditions, expected_cyclomatic) in slots {
16761                let operands = std::iter::once(identifier)
16762                    .chain(literals.iter().map(|(spelling, _)| *spelling));
16763                for operand in operands {
16764                    let source = template.replace("{}", operand);
16765                    assert_eq!(
16766                        conditions(lang, &source),
16767                        expected_conditions,
16768                        "{lang:?}: `{operand}` conditions\n  source: {source}"
16769                    );
16770                    assert_eq!(
16771                        cyclomatic_sum(lang, &source),
16772                        expected_cyclomatic,
16773                        "{lang:?}: `{operand}` cyclomatic_sum\n  source: {source}"
16774                    );
16775                }
16776            }
16777        });
16778    }
16779
16780    /// The twelfth Perl and Ruby literal kind, which the table above
16781    /// cannot hold: in both languages a heredoc body follows the
16782    /// *statement*, so the operand slot carries only the introducer and
16783    /// the rest of the literal lands on later lines. Substituting one
16784    /// into a single-line template produces an unterminated heredoc and
16785    /// an `ERROR` parse, not a measurement.
16786    ///
16787    /// That shape is also why exactly one kind is listed per language
16788    /// (§5): `heredoc_initializer` / `heredoc_beginning` is the node in
16789    /// the operand slot, while `heredoc_body_statement` /
16790    /// `heredoc_body` is a sibling the walker never reaches. Listing
16791    /// both would score one literal twice.
16792    #[cfg(any(feature = "perl", feature = "ruby"))]
16793    #[test]
16794    fn a_heredoc_operand_scores_like_an_identifier_operand() {
16795        #[cfg(feature = "perl")]
16796        {
16797            let heredoc = "sub f {\n  my ($a, $b) = @_;\n  if ($a && <<\"EOT\") { print 1; }\nhello\nEOT\n}\n";
16798            assert_spells(
16799                LANG::Perl,
16800                heredoc,
16801                &[(crate::Perl::HeredocInitializer as u16, 1, "<<\"EOT\"")],
16802            );
16803            assert_eq!(
16804                conditions(LANG::Perl, heredoc),
16805                conditions(
16806                    LANG::Perl,
16807                    "sub f {\n  my ($a, $b) = @_;\n  if ($a && $b) { print 1; }\n}\n"
16808                ),
16809                "Perl heredoc operand"
16810            );
16811        }
16812        #[cfg(feature = "ruby")]
16813        {
16814            let heredoc =
16815                "def f(a, b)\n  if a && <<~TXT then 1 else 0 end\n    hello\n  TXT\nend\n";
16816            assert_spells(
16817                LANG::Ruby,
16818                heredoc,
16819                &[(crate::Ruby::HeredocBeginning as u16, 1, "<<~TXT")],
16820            );
16821            assert_eq!(
16822                conditions(LANG::Ruby, heredoc),
16823                conditions(
16824                    LANG::Ruby,
16825                    "def f(a, b)\n  if a && b then 1 else 0 end\nend\n"
16826                ),
16827                "Ruby heredoc operand"
16828            );
16829        }
16830    }
16831
16832    /// Every spelling must parse to the `kind_id` its row names — the
16833    /// §1 alias claim, which is the one thing a conditions comparison
16834    /// cannot check: a row naming the wrong alias of a multi-id kind
16835    /// would simply keep reporting the unfixed number.
16836    #[test]
16837    fn every_literal_spelling_parses_to_the_kind_its_row_names() {
16838        for_each_case(|lang, (slots, _, literals, _)| {
16839            for (template, _, _) in slots {
16840                for (literal, kind) in literals {
16841                    let source = template.replace("{}", literal);
16842                    assert_spells(lang, &source, &[(*kind, 1, literal)]);
16843                }
16844            }
16845        });
16846    }
16847}
16848
16849/// PHP's `String3` is the hidden `_string` supertype and Groovy's
16850/// `SlashyString` the hidden `_slashy_string` one — both listed in, or
16851/// deliberately omitted from, their terminal-bool sets on the strength
16852/// of being unreachable (`.claude/rules/grammar-dispatch.md` §2). A
16853/// grammar bump that promotes either changes ABC's answer silently, so
16854/// the unreachability is pinned rather than assumed.
16855// Gated on the union of the two features its tests name, so a build
16856// enabling neither drops the module rather than leaving its imports
16857// unused (`.claude/rules/testing.md`, #1286).
16858#[cfg(all(test, any(feature = "php", feature = "groovy")))]
16859mod hidden_literal_supertypes {
16860    use crate::test_support::ast_has_kind_id;
16861    use crate::*;
16862
16863    #[cfg(feature = "php")]
16864    #[test]
16865    fn php_hidden_string_supertype_is_unreachable() {
16866        let src = "<?php\nfunction f($a) {\n  return $a && 's' && \"x$a\" && <<<EOT\ns\nEOT;\n}\n";
16867        let parser = PhpParser::new(src.as_bytes().to_vec(), std::path::Path::new("f.php"), None);
16868        assert!(
16869            ast_has_kind_id(&parser, Php::String as u16),
16870            "control: the fixture must carry the reachable `string` kind"
16871        );
16872        assert!(
16873            !ast_has_kind_id(&parser, Php::String3 as u16),
16874            "`_string` is no longer hidden; the defensive arm in \
16875             `php_bool_terminal_kinds!()` is now live and needs a fixture"
16876        );
16877    }
16878
16879    #[cfg(feature = "groovy")]
16880    #[test]
16881    fn groovy_hidden_slashy_string_is_unreachable() {
16882        let src = "def f(a) {\n  return a && /re/ && \"s\"\n}\n";
16883        let parser = GroovyParser::new(
16884            src.as_bytes().to_vec(),
16885            std::path::Path::new("f.groovy"),
16886            None,
16887        );
16888        assert!(
16889            ast_has_kind_id(&parser, Groovy::StringLiteral as u16),
16890            "control: a slashy string must still parse to `string_literal`"
16891        );
16892        assert!(
16893            !ast_has_kind_id(&parser, Groovy::SlashyString as u16),
16894            "`_slashy_string` is no longer hidden; `groovy_bool_terminal_kinds!()` \
16895             omits it on the strength of it being unreachable"
16896        );
16897    }
16898}