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/// See issue #395 for the Phase-1 cross-language policy
114/// alignment, #403 for the Phase-2 unary-conditional walker
115/// fan-out, #404 for the Phase-3 book documentation, and #557
116/// for the Kotlin / Ruby / Elixir walker wiring.
117#[derive(Debug, Clone, PartialEq)]
118#[non_exhaustive]
119pub struct Stats {
120    pub(super) assignments: f64,
121    assignments_sum: f64,
122    assignments_min: f64,
123    assignments_max: f64,
124    pub(super) branches: f64,
125    branches_sum: f64,
126    branches_min: f64,
127    branches_max: f64,
128    pub(super) conditions: f64,
129    conditions_sum: f64,
130    conditions_min: f64,
131    conditions_max: f64,
132    space_count: usize,
133    pub(super) declaration: Vec<DeclKind>,
134}
135
136#[derive(Debug, Clone, PartialEq)]
137pub(super) enum DeclKind {
138    Var,
139    Const,
140}
141
142impl Default for Stats {
143    fn default() -> Self {
144        Self {
145            assignments: 0.,
146            assignments_sum: 0.,
147            assignments_min: f64::MAX,
148            assignments_max: 0.,
149            branches: 0.,
150            branches_sum: 0.,
151            branches_min: f64::MAX,
152            branches_max: 0.,
153            conditions: 0.,
154            conditions_sum: 0.,
155            conditions_min: f64::MAX,
156            conditions_max: 0.,
157            space_count: 1,
158            declaration: Vec::new(),
159        }
160    }
161}
162
163impl fmt::Display for Stats {
164    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
165        write!(
166            f,
167            "assignments: {}, branches: {}, conditions: {}, magnitude: {}, \
168            assignments_average: {}, branches_average: {}, conditions_average: {}, \
169            assignments_min: {}, assignments_max: {}, \
170            branches_min: {}, branches_max: {}, \
171            conditions_min: {}, conditions_max: {}",
172            self.assignments_sum(),
173            self.branches_sum(),
174            self.conditions_sum(),
175            self.magnitude_sum(),
176            self.assignments_average(),
177            self.branches_average(),
178            self.conditions_average(),
179            self.assignments_min(),
180            self.assignments_max(),
181            self.branches_min(),
182            self.branches_max(),
183            self.conditions_min(),
184            self.conditions_max()
185        )
186    }
187}
188
189impl Stats {
190    /// Merges a second `Abc` metric into the first one.
191    pub fn merge(&mut self, other: &Stats) {
192        // Calculates minimum and maximum values
193        self.assignments_min = self.assignments_min.min(other.assignments_min);
194        self.assignments_max = self.assignments_max.max(other.assignments_max);
195        self.branches_min = self.branches_min.min(other.branches_min);
196        self.branches_max = self.branches_max.max(other.branches_max);
197        self.conditions_min = self.conditions_min.min(other.conditions_min);
198        self.conditions_max = self.conditions_max.max(other.conditions_max);
199
200        self.assignments_sum += other.assignments_sum;
201        self.branches_sum += other.branches_sum;
202        self.conditions_sum += other.conditions_sum;
203
204        self.space_count += other.space_count;
205    }
206
207    /// Returns the `Abc` assignments metric value.
208    #[must_use]
209    pub fn assignments(&self) -> u64 {
210        self.assignments as u64
211    }
212
213    /// Returns the `Abc` assignments sum metric value.
214    #[must_use]
215    pub fn assignments_sum(&self) -> u64 {
216        self.assignments_sum as u64
217    }
218
219    /// Returns the `Abc` assignments average value.
220    ///
221    /// This value is computed dividing the `Abc`
222    /// assignments value for the number of spaces.
223    #[must_use]
224    pub fn assignments_average(&self) -> f64 {
225        crate::metrics::average(self.assignments_sum() as f64, self.space_count)
226    }
227
228    /// Returns the `Abc` assignments minimum value.
229    ///
230    /// Collapses the `f64::MAX` sentinel that `Stats::default()` plants
231    /// into `assignments_min` to `0`, so a never-observed space
232    /// serializes to a meaningful number rather than `1.7976931e308`.
233    #[allow(clippy::float_cmp)]
234    #[must_use]
235    pub fn assignments_min(&self) -> u64 {
236        if self.assignments_min == f64::MAX {
237            0
238        } else {
239            self.assignments_min as u64
240        }
241    }
242
243    /// Returns the `Abc` assignments maximum value.
244    #[must_use]
245    pub fn assignments_max(&self) -> u64 {
246        self.assignments_max as u64
247    }
248
249    /// Returns the `Abc` branches metric value.
250    #[must_use]
251    pub fn branches(&self) -> u64 {
252        self.branches as u64
253    }
254
255    /// Returns the `Abc` branches sum metric value.
256    #[must_use]
257    pub fn branches_sum(&self) -> u64 {
258        self.branches_sum as u64
259    }
260
261    /// Returns the `Abc` branches average value.
262    ///
263    /// This value is computed dividing the `Abc`
264    /// branches value for the number of spaces.
265    #[must_use]
266    pub fn branches_average(&self) -> f64 {
267        crate::metrics::average(self.branches_sum() as f64, self.space_count)
268    }
269
270    /// Returns the `Abc` branches minimum value.
271    ///
272    /// Same `f64::MAX` sentinel collapse as `assignments_min`.
273    #[allow(clippy::float_cmp)]
274    #[must_use]
275    pub fn branches_min(&self) -> u64 {
276        if self.branches_min == f64::MAX {
277            0
278        } else {
279            self.branches_min as u64
280        }
281    }
282
283    /// Returns the `Abc` branches maximum value.
284    #[must_use]
285    pub fn branches_max(&self) -> u64 {
286        self.branches_max as u64
287    }
288
289    /// Returns the `Abc` conditions metric value.
290    #[must_use]
291    pub fn conditions(&self) -> u64 {
292        self.conditions as u64
293    }
294
295    /// Returns the `Abc` conditions sum metric value.
296    #[must_use]
297    pub fn conditions_sum(&self) -> u64 {
298        self.conditions_sum as u64
299    }
300
301    /// Returns the `Abc` conditions average value.
302    ///
303    /// This value is computed dividing the `Abc`
304    /// conditions value for the number of spaces.
305    #[must_use]
306    pub fn conditions_average(&self) -> f64 {
307        crate::metrics::average(self.conditions_sum() as f64, self.space_count)
308    }
309
310    /// Returns the `Abc` conditions minimum value.
311    ///
312    /// Same `f64::MAX` sentinel collapse as `assignments_min`.
313    #[allow(clippy::float_cmp)]
314    #[must_use]
315    pub fn conditions_min(&self) -> u64 {
316        if self.conditions_min == f64::MAX {
317            0
318        } else {
319            self.conditions_min as u64
320        }
321    }
322
323    /// Returns the `Abc` conditions maximum value.
324    #[must_use]
325    pub fn conditions_max(&self) -> u64 {
326        self.conditions_max as u64
327    }
328
329    /// Returns the `Abc` magnitude metric value.
330    #[must_use]
331    pub fn magnitude(&self) -> f64 {
332        (self.assignments.powi(2) + self.branches.powi(2) + self.conditions.powi(2)).sqrt()
333    }
334
335    /// Returns the `Abc` magnitude sum metric value.
336    #[must_use]
337    pub fn magnitude_sum(&self) -> f64 {
338        (self.assignments_sum.powi(2) + self.branches_sum.powi(2) + self.conditions_sum.powi(2))
339            .sqrt()
340    }
341
342    #[inline]
343    pub(crate) fn compute_sum(&mut self) {
344        self.assignments_sum += self.assignments;
345        self.branches_sum += self.branches;
346        self.conditions_sum += self.conditions;
347    }
348
349    #[inline]
350    pub(crate) fn compute_minmax(&mut self) {
351        self.assignments_min = self.assignments_min.min(self.assignments);
352        self.assignments_max = self.assignments_max.max(self.assignments);
353        self.branches_min = self.branches_min.min(self.branches);
354        self.branches_max = self.branches_max.max(self.branches);
355        self.conditions_min = self.conditions_min.min(self.conditions);
356        self.conditions_max = self.conditions_max.max(self.conditions);
357        self.compute_sum();
358    }
359}
360
361#[doc(hidden)]
362/// Per-language computation of the ABC metric.
363pub(crate) trait Abc
364where
365    Self: Checker,
366{
367    /// Walk `node` and update `stats` with this metric for the language
368    /// implementing the trait.
369    ///
370    /// `code` is the source bytes underlying the parsed tree. Most
371    /// languages ignore it: assignments, branches, and conditions all
372    /// surface as distinct grammar productions and a `kind_id()` match
373    /// is enough. Elixir is the exception — `case` / `cond` / `if` /
374    /// `with` / guard `when` arms surface as `Call` nodes whose keyword
375    /// target lives only in the source text. Matching the `Cyclomatic`
376    /// / `Halstead` / `Exit` / `Cognitive` pattern keeps the signature
377    /// uniform.
378    fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats);
379}
380
381// Shared Phase-2B helper (issue #403): walk every named child of an
382// expression-list-style wrapper (Go's `expression_list`, Lua's
383// `expression_list`) and route each through a language-specific
384// classifier. Used for `return value1, value2, ...` arms where the
385// values live one level below the return statement under a list
386// wrapper. The classifier receives only named children so that
387// `,` / `;` / `(` / `)` tokens never reach it.
388pub(super) fn for_each_named_child(list: &Node, conditions: &mut f64, f: fn(&Node, &mut f64)) {
389    let mut cursor = list.cursor();
390    if cursor.goto_first_child() {
391        loop {
392            let child = cursor.node();
393            if child.is_named() {
394                f(&child, conditions);
395            }
396            if !cursor.goto_next_sibling() {
397                break;
398            }
399        }
400    }
401}
402
403// Default no-op `Abc` impls. Audited in #188; the matrix below
404// records the rationale for every entry so the no-op default is a
405// deliberate choice, not scaffolding leftover.
406//
407// Real defaults (the language has no construct ABC measures, so the
408// metric is genuinely 0):
409//   - PreprocCode, CcommentCode: no executable code (comments /
410//     preprocessor lines only).
411implement_metric_trait!(Abc, PreprocCode, CcommentCode);
412
413#[cfg(test)]
414#[allow(
415    clippy::float_cmp,
416    clippy::cast_precision_loss,
417    clippy::cast_possible_truncation,
418    clippy::cast_sign_loss,
419    clippy::similar_names,
420    clippy::doc_markdown,
421    clippy::needless_raw_string_hashes,
422    clippy::too_many_lines
423)]
424mod tests {
425    use crate::tools::{check_func_space, check_metrics};
426    use crate::traits::ParserTrait;
427
428    use super::*;
429
430    // Walk the AST and return true iff any node has `kind_id == target`.
431    // Used as a drift marker for hidden-rule kind ids: a passing
432    // `!ast_has_kind_id(...)` assertion proves the kind is unreachable
433    // at the pinned grammar version, so a future grammar bump that
434    // promotes the hidden rule to a concrete emitted node will fail
435    // loudly instead of silently changing the metric (lesson 34).
436    fn ast_has_kind_id<P: ParserTrait>(parser: &P, target: u16) -> bool {
437        let mut stack = vec![parser.root()];
438        while let Some(node) = stack.pop() {
439            if node.kind_id() == target {
440                return true;
441            }
442            for i in (0..node.child_count()).rev() {
443                if let Some(c) = node.child(i) {
444                    stack.push(c);
445                }
446            }
447        }
448        false
449    }
450
451    /// Regression for #227: a `Stats::default()` that never sees an
452    /// observation must not leak the `f64::MAX` sentinel for
453    /// `assignments_min`, `branches_min`, or `conditions_min`. All
454    /// three getters collapse the sentinel to `0.0` so JSON never
455    /// emits `1.7976931e308`.
456    #[test]
457    fn abc_empty_file_min_is_zero() {
458        let stats = Stats::default();
459        assert_eq!(stats.assignments_min(), 0);
460        assert_eq!(stats.branches_min(), 0);
461        assert_eq!(stats.conditions_min(), 0);
462    }
463
464    // Regression test for the `EQ` arm guard in `JavaCode::compute`:
465    // the rewrite from `.map().unwrap_or_else()` to
466    // `is_none_or(|decl| matches!(decl, DeclKind::Var))` must preserve
467    // the three-way truth table — None → ++, Some(Var) → ++,
468    // Some(Const) → no-op.
469    #[test]
470    fn java_eq_arm_increments_when_declaration_stack_is_empty() {
471        // No surrounding `int x = ...` / `Final` token → declaration
472        // stack is empty when the `EQ` token is visited, so the None
473        // branch must increment `assignments`.
474        check_metrics::<JavaParser>(
475            "class A { void m() { int x = 0; x = 1; x = 2; x = 3; } }",
476            "foo.java",
477            |metric| {
478                // `int x = 0;` adds 1 (Some(Var) branch),
479                // each subsequent `x = N;` adds 1 (None branch).
480                assert_eq!(metric.abc.assignments_sum(), 4);
481            },
482        );
483    }
484
485    #[test]
486    fn java_eq_arm_skips_when_declaration_stack_top_is_const() {
487        // `final` pushes `DeclKind::Const` on top of the active `Var`
488        // entry, so the Some(non-Var) branch must skip the increment.
489        check_metrics::<JavaParser>(
490            "class A {
491                final int X = 1;
492                final int Y = 2;
493                void m() { final int Z = 3; }
494            }",
495            "foo.java",
496            |metric| {
497                // All three `=` tokens land under a `Const` top, so
498                // assignments should be 0 across all spaces.
499                assert_eq!(metric.abc.assignments_sum(), 0);
500            },
501        );
502    }
503
504    // Constant declarations are not counted as assignments
505    #[test]
506    fn java_constant_declarations() {
507        check_metrics::<JavaParser>(
508            "class A {
509                private final int X1 = 0, Y1 = 0;
510                public final float PI = 3.14f;
511                final static String HELLO = \"Hello,\";
512                protected String world = \" world!\";   // +1a
513                public float e = 2.718f;                // +1a
514                private int x2 = 1, y2 = 2;             // +2a
515
516                void m() {
517                    final int Z1 = 0, Z2 = 0, Z3 = 0;
518                    final float T = 0.0f;
519                    int z1 = 1, z2 = 2, z3 = 3;         // +3a
520                    float t = 60.0f;                    // +1a
521                }
522            }",
523            "foo.java",
524            |metric| {
525                // magnitude: sqrt(64 + 0 + 0) = sqrt(64)
526                // space count: 3 (1 unit, 1 class and 1 method)
527                insta::assert_json_snapshot!(
528                    metric.abc,
529                    @r#"
530                {
531                  "assignments": 8,
532                  "branches": 0,
533                  "conditions": 0,
534                  "magnitude": 8.0,
535                  "value": 0.0,
536                  "assignments_average": 2.6666666666666665,
537                  "branches_average": 0.0,
538                  "conditions_average": 0.0,
539                  "assignments_min": 0,
540                  "assignments_max": 4,
541                  "branches_min": 0,
542                  "branches_max": 0,
543                  "conditions_min": 0,
544                  "conditions_max": 0
545                }
546                "#
547                );
548            },
549        );
550    }
551
552    // "In computer science, conditionals (that is, conditional statements, conditional expressions
553    // and conditional constructs,) are programming language commands for handling decisions."
554    // Source: https://en.wikipedia.org/wiki/Conditional_(computer_programming)
555    // According to this definition, boolean expressions that are evaluated to make a decision are considered as conditions
556    // Variables, method invocations and true or false values used inside
557    // variable declarations and assignment expressions are not counted as conditions
558    #[test]
559    fn java_declarations_with_conditions() {
560        check_metrics::<JavaParser>(
561            "
562            boolean a = (1 > 2);            // +1a +1c
563            boolean b = 3 > 4;              // +1a +1c
564            boolean c = (1 > 2) && 3 > 4;   // +1a +2c
565            boolean d = b && (x > 5) || c;  // +1a +3c
566            boolean e = !d;                 // +1a +1c
567            boolean f = ((!false));         // +1a +1c
568            boolean g = !(!(true));         // +1a +1c
569            boolean h = true;               // +1a
570            boolean i = (false);            // +1a
571            boolean j = (((((true)))));     // +1a
572            boolean k = (((((m())))));      // +1a +1b
573            boolean l = (((((!m())))));     // +1a +1b +1c
574            boolean m = (!(!((m()))));      // +1a +1b +1c
575            List<String> n = null;          // +1a (< and > used for generic types are not counted as conditions)
576            ",
577            "foo.java",
578          |metric| {
579                // magnitude: sqrt(196 + 9 + 144) = sqrt(349)
580                // space count: 1 (1 unit)
581                insta::assert_json_snapshot!(
582                    metric.abc,
583                    @r#"
584                {
585                  "assignments": 14,
586                  "branches": 3,
587                  "conditions": 12,
588                  "magnitude": 18.681541692269406,
589                  "value": 18.681541692269406,
590                  "assignments_average": 14.0,
591                  "branches_average": 3.0,
592                  "conditions_average": 12.0,
593                  "assignments_min": 14,
594                  "assignments_max": 14,
595                  "branches_min": 3,
596                  "branches_max": 3,
597                  "conditions_min": 12,
598                  "conditions_max": 12
599                }
600                "#
601                );
602            },
603        );
604    }
605
606    // Conditions can be found in assignment expressions
607    #[test]
608    fn java_assignments_with_conditions() {
609        check_metrics::<JavaParser>(
610            "
611            a = 2 < 1;                  // +1a +1c
612            b = (4 >= 3) && 2 <= 1;     // +1a +2c
613            c = a || (x != 10) && b;    // +1a +3c
614            d = !false;                 // +1a +1c
615            e = (!false);               // +1a +1c
616            f = !(false);               // +1a +1c
617            g = (!(((true))));          // +1a +1c
618            h = ((true));               // +1a
619            i = !m();                   // +1a +1b +1c
620            j = !((m()));               // +1a +1b +1c
621            k = (!(m()));               // +1a +1b +1c
622            l = ((!(m())));             // +1a +1b +1c
623            m = !B.<Integer>m(2);       // +1a +1b +1c
624            n = !((B.<Integer>m(4)));   // +1a +1b +1c
625            ",
626            "foo.java",
627            |metric| {
628                // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
629                // space count: 1 (1 unit)
630                insta::assert_json_snapshot!(
631                    metric.abc,
632                    @r#"
633                {
634                  "assignments": 14,
635                  "branches": 6,
636                  "conditions": 16,
637                  "magnitude": 22.090722034374522,
638                  "value": 22.090722034374522,
639                  "assignments_average": 14.0,
640                  "branches_average": 6.0,
641                  "conditions_average": 16.0,
642                  "assignments_min": 14,
643                  "assignments_max": 14,
644                  "branches_min": 6,
645                  "branches_max": 6,
646                  "conditions_min": 16,
647                  "conditions_max": 16
648                }
649                "#
650                );
651            },
652        );
653    }
654
655    // Conditions can be found in method arguments
656    #[test]
657    fn java_methods_arguments_with_conditions() {
658        check_metrics::<JavaParser>(
659            "
660            m1(a);                                  // +1b
661            m2(a, b);                               // +1b
662            m3(true, (false), (((true))));          // +1b
663            m3(m1(false), m1(true), m1(false));     // +4b
664            m1(!a);                                 // +1b +1c
665            m2((((a))), (!b));                      // +1b +1c
666            m3(!(a), b, !!!c);                      // +1b +2c
667            m3(a, !b, m2(!a, !m2(!b, !m1(!c))));    // +4b +6c
668            ",
669            "foo.java",
670            |metric| {
671                // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
672                // space count: 1 (1 unit)
673                insta::assert_json_snapshot!(
674                    metric.abc,
675                    @r#"
676                {
677                  "assignments": 0,
678                  "branches": 14,
679                  "conditions": 10,
680                  "magnitude": 17.204650534085253,
681                  "value": 17.204650534085253,
682                  "assignments_average": 0.0,
683                  "branches_average": 14.0,
684                  "conditions_average": 10.0,
685                  "assignments_min": 0,
686                  "assignments_max": 0,
687                  "branches_min": 14,
688                  "branches_max": 14,
689                  "conditions_min": 10,
690                  "conditions_max": 10
691                }
692                "#
693                );
694            },
695        );
696    }
697
698    // "A unary conditional expression is an implicit condition that uses no relational operators."
699    // Source: Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report.
700    // https://www.softwarerenovation.com/Articles.aspx (page 5)
701    #[test]
702    fn java_if_single_conditions() {
703        check_metrics::<JavaParser>(
704            "
705            if ( a < 0 ) {}             // +1c
706            if ( ((a != 0)) ) {}        // +1c
707            if ( !(a > 0) ) {}          // +1c
708            if ( !(((a == 0))) ) {}     // +1c
709            if ( b.m1() ) {}            // +1b +1c
710            if ( !b.m1() ) {}           // +1b +1c
711            if ( !!b.m2() ) {}          // +1b +1c
712            if ( (!(b.m1())) ) {}       // +1b +1c
713            if ( (!(!b.m1())) ) {}      // +1b +1c
714            if ( ((b.m2())) ) {}        // +1b +1c
715            if ( ((b.m().m1())) ) {}    // +2b +1c
716            if ( c ) {}                 // +1c
717            if ( !c ) {}                // +1c
718            if ( !!!!!!!!!!c ) {}       // +1c
719            if ( (((c))) ) {}           // +1c
720            if ( (((!c))) ) {}          // +1c
721            if ( ((!(c))) ) {}          // +1c
722            if ( true ) {}              // +1c
723            if ( !true ) {}             // +1c
724            if ( ((false)) ) {}         // +1c
725            if ( !(!(false)) ) {}       // +1c
726            if ( !!!false ) {}          // +1c
727            ",
728            "foo.java",
729            |metric| {
730                // magnitude: sqrt(0 + 64 + 484) = sqrt(548)
731                // space count: 1 (1 unit)
732                insta::assert_json_snapshot!(
733                    metric.abc,
734                    @r#"
735                {
736                  "assignments": 0,
737                  "branches": 8,
738                  "conditions": 22,
739                  "magnitude": 23.40939982143925,
740                  "value": 23.40939982143925,
741                  "assignments_average": 0.0,
742                  "branches_average": 8.0,
743                  "conditions_average": 22.0,
744                  "assignments_min": 0,
745                  "assignments_max": 0,
746                  "branches_min": 8,
747                  "branches_max": 8,
748                  "conditions_min": 22,
749                  "conditions_max": 22
750                }
751                "#
752                );
753            },
754        );
755    }
756
757    #[test]
758    fn java_if_multiple_conditions() {
759        check_metrics::<JavaParser>(
760            "
761            if ( a || b || c || d ) {}              // +4c
762            if ( a || b && c && d ) {}              // +4c
763            if ( x < y && a == b ) {}               // +2c
764            if ( ((z < (x + y))) ) {}               // +1c
765            if ( a || ((((b))) && c) ) {}           // +3c
766            if ( a && ((((a == b))) && c) ) {}      // +3c
767            if ( a || ((((a == b))) || ((c))) ) {}  // +3c
768            if ( x < y && B.m() ) {}                // +1b +2c
769            if ( x < y && !(((B.m()))) ) {}         // +1b +2c
770            if ( !(x < y) && !B.m() ) {}            // +1b +2c
771            if ( !!!(!!!(a)) && B.m() ||            // +1b +2c
772                 !B.m() && (((x > 4))) ) {}         // +1b +2c
773            ",
774            "foo.java",
775            |metric| {
776                // magnitude: sqrt(0 + 25 + 900) = sqrt(925)
777                // space count: 1 (1 unit)
778                insta::assert_json_snapshot!(
779                    metric.abc,
780                    @r#"
781                {
782                  "assignments": 0,
783                  "branches": 5,
784                  "conditions": 30,
785                  "magnitude": 30.4138126514911,
786                  "value": 30.4138126514911,
787                  "assignments_average": 0.0,
788                  "branches_average": 5.0,
789                  "conditions_average": 30.0,
790                  "assignments_min": 0,
791                  "assignments_max": 0,
792                  "branches_min": 5,
793                  "branches_max": 5,
794                  "conditions_min": 30,
795                  "conditions_max": 30
796                }
797                "#
798                );
799            },
800        );
801    }
802
803    #[test]
804    fn java_while_and_do_while_conditions() {
805        check_metrics::<JavaParser>(
806            "
807            while ( (!(!(!(a)))) ) {}                   // +1c
808            while ( b || 1 > 2 ) {}                     // +2c
809            while ( x.m() && (((c))) ) {}               // +1b +2c
810            do {} while ( !!!(((!!!a))) );              // +1c
811            do {} while ( a || (b && c) );              // +3c
812            do {} while ( !x.m() && 1 > 2 || !true );   // +1b +3c
813            ",
814            "foo.java",
815            |metric| {
816                // magnitude: sqrt(0 + 4 + 144) = sqrt(148)
817                // space count: 1 (1 unit)
818                insta::assert_json_snapshot!(
819                    metric.abc,
820                    @r#"
821                {
822                  "assignments": 0,
823                  "branches": 2,
824                  "conditions": 12,
825                  "magnitude": 12.165525060596439,
826                  "value": 12.165525060596439,
827                  "assignments_average": 0.0,
828                  "branches_average": 2.0,
829                  "conditions_average": 12.0,
830                  "assignments_min": 0,
831                  "assignments_max": 0,
832                  "branches_min": 2,
833                  "branches_max": 2,
834                  "conditions_min": 12,
835                  "conditions_max": 12
836                }
837                "#
838                );
839            },
840        );
841    }
842
843    // GMetrics, a Groovy source code analyzer, provides the following definition of unary conditional expression:
844    // "These are cases where a single variable/field/value is treated as a boolean value.
845    // Examples include `if (x)` and `return !ready`."
846    // According to this definition, unary conditional expressions are counted also in function return values.
847    // Source: https://dx42.github.io/gmetrics/metrics/AbcMetric.html
848    // Examples: https://github.com/dx42/gmetrics/blob/master/src/test/groovy/org/gmetrics/metric/abc/AbcMetric_MethodTest.groovy
849    #[test]
850    fn java_return_with_conditions() {
851        check_metrics::<JavaParser>(
852            "class A {
853                boolean m1() {
854                    return !(z >= 0);       // +1c
855                }
856                boolean m2() {
857                    return (((!x)));        // +1c
858                }
859                boolean m3() {
860                    return x && y;          // +2c
861                }
862                boolean m4() {
863                    return y || (z < 0);    // +2c
864                }
865                boolean m5() {
866                    return x || y ?         // +3c (two unary conditions and one ?)
867                        true : false;
868                }
869            }",
870            "foo.java",
871            |metric| {
872                // magnitude: sqrt(0 + 0 + 81) = sqrt(81)
873                // space count: 7 (1 unit, 1 class and 5 methods)
874                insta::assert_json_snapshot!(
875                    metric.abc,
876                    @r#"
877                {
878                  "assignments": 0,
879                  "branches": 0,
880                  "conditions": 9,
881                  "magnitude": 9.0,
882                  "value": 0.0,
883                  "assignments_average": 0.0,
884                  "branches_average": 0.0,
885                  "conditions_average": 1.2857142857142858,
886                  "assignments_min": 0,
887                  "assignments_max": 0,
888                  "branches_min": 0,
889                  "branches_max": 0,
890                  "conditions_min": 0,
891                  "conditions_max": 3
892                }
893                "#
894                );
895            },
896        );
897    }
898
899    // Variables, method invocations, and true or false values
900    // inside return statements are not counted as conditions
901    #[test]
902    fn java_return_without_conditions() {
903        check_metrics::<JavaParser>(
904            "class A {
905                boolean m1() {
906                    return x;
907                }
908                boolean m2() {
909                    return (x);
910                }
911                boolean m3() {
912                    return y.m();   // +1b
913                }
914                boolean m4() {
915                    return false;
916                }
917                void m5() {
918                    return;
919                }
920            }",
921            "foo.java",
922            |metric| {
923                // magnitude: sqrt(0 + 1 + 0) = sqrt(1)
924                // space count: 7 (1 unit, 1 class and 5 methods)
925                insta::assert_json_snapshot!(
926                    metric.abc,
927                    @r#"
928                {
929                  "assignments": 0,
930                  "branches": 1,
931                  "conditions": 0,
932                  "magnitude": 1.0,
933                  "value": 0.0,
934                  "assignments_average": 0.0,
935                  "branches_average": 0.14285714285714285,
936                  "conditions_average": 0.0,
937                  "assignments_min": 0,
938                  "assignments_max": 0,
939                  "branches_min": 0,
940                  "branches_max": 1,
941                  "conditions_min": 0,
942                  "conditions_max": 0
943                }
944                "#
945                );
946            },
947        );
948    }
949
950    // Variables, method invocations, and true or false values
951    // in lambda expression return values are not counted as conditions
952    #[test]
953    fn java_lambda_expressions_return_with_conditions() {
954        check_metrics::<JavaParser>(
955            "
956            Predicate<Boolean> p1 = a -> a;                         // +1a
957            Predicate<Boolean> p2 = b -> true;                      // +1a
958            Predicate<Boolean> p3 = c -> m();                       // +1a
959            Predicate<Integer> p4 = d -> d > 10;                    // +1a +1c
960            Predicate<Boolean> p5 = (e) -> !e;                      // +1a +1c
961            Predicate<Boolean> p6 = (f) -> !((!f));                 // +1a +1c
962            Predicate<Boolean> p7 = (g) -> !g && true;              // +1a +2c
963            BiPredicate<Boolean, Boolean> bp1 = (h, i) -> !h && !i; // +1a +2c
964            BiPredicate<Boolean, Boolean> bp2 = (j, k) -> {
965                return j || k;                                      // +1a +2c
966            };
967            ",
968            "foo.java",
969            |metric| {
970                // magnitude: sqrt(81 + 1 + 81) = sqrt(163)
971                // space count: 1 (1 unit)
972                insta::assert_json_snapshot!(
973                    metric.abc,
974                    @r#"
975                {
976                  "assignments": 9,
977                  "branches": 1,
978                  "conditions": 9,
979                  "magnitude": 12.767145334803704,
980                  "value": 12.767145334803704,
981                  "assignments_average": 9.0,
982                  "branches_average": 1.0,
983                  "conditions_average": 9.0,
984                  "assignments_min": 9,
985                  "assignments_max": 9,
986                  "branches_min": 1,
987                  "branches_max": 1,
988                  "conditions_min": 9,
989                  "conditions_max": 9
990                }
991                "#
992                );
993            },
994        );
995    }
996
997    #[test]
998    fn java_for_with_variable_declaration() {
999        check_metrics::<JavaParser>(
1000            "
1001            for ( int i1 = 0; !(!(!(!a))); i1++ ) {}                // +2a +1c
1002            for ( int i2 = 0; !B.m(); i2++ ) {}                     // +2a +1b +1c
1003            for ( int i3 = 0; a || false; i3++ ) {}                 // +2a +2c
1004            for ( int i4 = 0; a && B.m() ? true : false; i4++ ) {}  // +2a +1b +3c
1005            for ( int i5 = 0; true; i5++ ) {}                       // +2a +1c
1006            ",
1007            "foo.java",
1008            |metric| {
1009                // magnitude: sqrt(100 + 4 + 64) = sqrt(168)
1010                // space count: 1 (1 unit)
1011                insta::assert_json_snapshot!(
1012                    metric.abc,
1013                    @r#"
1014                {
1015                  "assignments": 10,
1016                  "branches": 2,
1017                  "conditions": 8,
1018                  "magnitude": 12.96148139681572,
1019                  "value": 12.96148139681572,
1020                  "assignments_average": 10.0,
1021                  "branches_average": 2.0,
1022                  "conditions_average": 8.0,
1023                  "assignments_min": 10,
1024                  "assignments_max": 10,
1025                  "branches_min": 2,
1026                  "branches_max": 2,
1027                  "conditions_min": 8,
1028                  "conditions_max": 8
1029                }
1030                "#
1031                );
1032            },
1033        );
1034    }
1035
1036    #[test]
1037    fn java_for_without_variable_declaration() {
1038        check_metrics::<JavaParser>(
1039            "class A{
1040                void m1() {
1041                    for (i = 0; x < y; i++) {}          // +2a +1c
1042                    for (i = 0; ((x < y)); i++) {}      // +2a +1c
1043                    for (i = 0; !(!(x < y)); i++) {}    // +2a +1c
1044                    for (i = 0; true; i++) {}           // +2a +1c
1045                }
1046                void m2() {
1047                    for ( ; true; ) {}  // +1c
1048                }
1049                void m3() {
1050                    for ( ; ; ) {}      // +1c (one implicit unary condition set to true)
1051                }
1052            }",
1053            "foo.java",
1054            |metric| {
1055                // magnitude: sqrt(64 + 0 + 36) = sqrt(100)
1056                // space count: 5 (1 unit, 1 class and 3 methods)
1057                insta::assert_json_snapshot!(
1058                    metric.abc,
1059                    @r#"
1060                {
1061                  "assignments": 8,
1062                  "branches": 0,
1063                  "conditions": 6,
1064                  "magnitude": 10.0,
1065                  "value": 0.0,
1066                  "assignments_average": 1.6,
1067                  "branches_average": 0.0,
1068                  "conditions_average": 1.2,
1069                  "assignments_min": 0,
1070                  "assignments_max": 8,
1071                  "branches_min": 0,
1072                  "branches_max": 0,
1073                  "conditions_min": 0,
1074                  "conditions_max": 4
1075                }
1076                "#
1077                );
1078            },
1079        );
1080    }
1081
1082    // Variables, method invocations, and true or false values
1083    // in ternary expression return values are not counted as conditions
1084    #[test]
1085    fn java_ternary_conditions() {
1086        check_metrics::<JavaParser>(
1087            "
1088            a = true;                                   // +1a
1089            b = a ? true : false;                       // +1a +2c
1090            c = ((((a)))) ? !false : !b;                // +1a +4c
1091            d = !this.m() ? !!a : (false);              // +1a +1b +3c
1092            e = !(a) && b ? ((c)) : !d;                 // +1a +4c
1093            if ( this.m() ? a : !this.m() ) {}          // +2b +3c
1094            if ( x > 0 ? !(false) : this.m() ) {}       // +1b +3c
1095            if ( x > 0 && x != 3 ? !(a) : (!(b)) ) {}   // +5c
1096            ",
1097            "foo.java",
1098            |metric| {
1099                // magnitude: sqrt(25 + 16 + 576) = sqrt(617)
1100                //  space count: 1 (1 unit)
1101                insta::assert_json_snapshot!(
1102                    metric.abc,
1103                    @r#"
1104                {
1105                  "assignments": 5,
1106                  "branches": 4,
1107                  "conditions": 24,
1108                  "magnitude": 24.839484696748443,
1109                  "value": 24.839484696748443,
1110                  "assignments_average": 5.0,
1111                  "branches_average": 4.0,
1112                  "conditions_average": 24.0,
1113                  "assignments_min": 5,
1114                  "assignments_max": 5,
1115                  "branches_min": 4,
1116                  "branches_max": 4,
1117                  "conditions_min": 24,
1118                  "conditions_max": 24
1119                }
1120                "#
1121                );
1122            },
1123        );
1124    }
1125
1126    #[test]
1127    fn bash_assignments_only() {
1128        check_metrics::<BashParser>(
1129            "f() {
1130                 a=1
1131                 b=2
1132                 c+=3
1133             }",
1134            "foo.sh",
1135            |metric| {
1136                insta::assert_json_snapshot!(
1137                    metric.abc,
1138                    @r#"
1139                {
1140                  "assignments": 3,
1141                  "branches": 0,
1142                  "conditions": 0,
1143                  "magnitude": 3.0,
1144                  "value": 0.0,
1145                  "assignments_average": 1.5,
1146                  "branches_average": 0.0,
1147                  "conditions_average": 0.0,
1148                  "assignments_min": 0,
1149                  "assignments_max": 3,
1150                  "branches_min": 0,
1151                  "branches_max": 0,
1152                  "conditions_min": 0,
1153                  "conditions_max": 0
1154                }
1155                "#
1156                );
1157            },
1158        );
1159    }
1160
1161    #[test]
1162    fn bash_commands_only() {
1163        check_metrics::<BashParser>(
1164            "f() {
1165                 echo a
1166                 ls
1167             }",
1168            "foo.sh",
1169            |metric| {
1170                insta::assert_json_snapshot!(
1171                    metric.abc,
1172                    @r#"
1173                {
1174                  "assignments": 0,
1175                  "branches": 2,
1176                  "conditions": 0,
1177                  "magnitude": 2.0,
1178                  "value": 0.0,
1179                  "assignments_average": 0.0,
1180                  "branches_average": 1.0,
1181                  "conditions_average": 0.0,
1182                  "assignments_min": 0,
1183                  "assignments_max": 0,
1184                  "branches_min": 0,
1185                  "branches_max": 2,
1186                  "conditions_min": 0,
1187                  "conditions_max": 0
1188                }
1189                "#
1190                );
1191            },
1192        );
1193    }
1194
1195    #[test]
1196    fn bash_control_flow_counts_conditions() {
1197        // Regression for #696: Bash control-flow branches are ABC
1198        // conditions (a Bash predicate is a command, so the branch keyword
1199        // is the only condition signal). Each mirrors a cyclomatic decision.
1200        //
1201        // expected: 4 conditions — `if` (1) + `elif` (1) + `while` (1) +
1202        // the non-wildcard case arm `a)` (1). The bare-`*)` wildcard arm is
1203        // the Bash analogue of `default:` and is excluded, exactly as the
1204        // cyclomatic standard count excludes it. No comparison / test
1205        // operators appear, so every condition here is control-flow.
1206        check_metrics::<BashParser>(
1207            "f() {
1208                 if cmd; then
1209                     echo a
1210                 elif other; then
1211                     echo b
1212                 fi
1213                 while running; do
1214                     echo c
1215                 done
1216                 case \"$x\" in
1217                     a) echo d ;;
1218                     *) echo e ;;
1219                 esac
1220             }",
1221            "foo.sh",
1222            |metric| {
1223                assert_eq!(metric.abc.conditions_sum(), 4);
1224            },
1225        );
1226    }
1227
1228    #[test]
1229    fn bash_conditions_mix() {
1230        // Exercises every condition path: `==` and `!=` inside `[[ ]]`,
1231        // arithmetic `<` inside `(( ))`, and the prefix `-z` test operator
1232        // inside `[ ]`. Each `if` body's `echo` contributes a branch.
1233        //
1234        // expected: 8 conditions — each of the four `if`s contributes one
1235        // for the control-flow branch (#696) plus one for its comparison /
1236        // test operator (`==`, `!=`, `<`, `-z`). 4 branches (one `echo`
1237        // each). magnitude = sqrt(4² + 8²) = sqrt(80).
1238        check_metrics::<BashParser>(
1239            "f() {
1240                 if [[ \"$a\" == \"$b\" ]]; then
1241                     echo eq
1242                 fi
1243                 if [[ \"$x\" != \"$y\" ]]; then
1244                     echo ne
1245                 fi
1246                 if (( $a < $b )); then
1247                     echo lt
1248                 fi
1249                 if [ -z \"$x\" ]; then
1250                     echo empty
1251                 fi
1252             }",
1253            "foo.sh",
1254            |metric| {
1255                assert_eq!(metric.abc.conditions_sum(), 8);
1256                assert_eq!(metric.abc.branches_sum(), 4);
1257                insta::assert_json_snapshot!(
1258                    metric.abc,
1259                    @r#"
1260                {
1261                  "assignments": 0,
1262                  "branches": 4,
1263                  "conditions": 8,
1264                  "magnitude": 8.94427190999916,
1265                  "value": 0.0,
1266                  "assignments_average": 0.0,
1267                  "branches_average": 2.0,
1268                  "conditions_average": 4.0,
1269                  "assignments_min": 0,
1270                  "assignments_max": 0,
1271                  "branches_min": 0,
1272                  "branches_max": 4,
1273                  "conditions_min": 0,
1274                  "conditions_max": 8
1275                }
1276                "#
1277                );
1278            },
1279        );
1280    }
1281
1282    #[test]
1283    fn bash_magnitude() {
1284        // Combined assignments + branches + conditions. The single `if`
1285        // contributes two conditions (the control-flow branch, #696, plus
1286        // the `==` operator), so magnitude = sqrt(2² + 1² + 2²) = sqrt(9).
1287        check_metrics::<BashParser>(
1288            "f() {
1289                 a=1
1290                 b=2
1291                 if [[ \"$a\" == \"$b\" ]]; then
1292                     echo eq
1293                 fi
1294             }",
1295            "foo.sh",
1296            |metric| {
1297                assert_eq!(metric.abc.conditions_sum(), 2);
1298                insta::assert_json_snapshot!(
1299                    metric.abc,
1300                    @r#"
1301                {
1302                  "assignments": 2,
1303                  "branches": 1,
1304                  "conditions": 2,
1305                  "magnitude": 3.0,
1306                  "value": 0.0,
1307                  "assignments_average": 1.0,
1308                  "branches_average": 0.5,
1309                  "conditions_average": 1.0,
1310                  "assignments_min": 0,
1311                  "assignments_max": 2,
1312                  "branches_min": 0,
1313                  "branches_max": 1,
1314                  "conditions_min": 0,
1315                  "conditions_max": 2
1316                }
1317                "#
1318                );
1319            },
1320        );
1321    }
1322
1323    #[test]
1324    fn java_malformed_parenthesized_no_panic() {
1325        check_metrics::<JavaParser>("class A { void m() { if (( }) }", "foo.java", |metric| {
1326            // tree-sitter emits ERROR nodes for this malformed source, so no
1327            // IfStatement, branch, or condition is recognised — all counts are 0.
1328            // Primary goal: the unwrap-free path does not panic.
1329            assert_eq!(metric.abc.assignments(), 0);
1330            assert_eq!(metric.abc.branches(), 0);
1331            assert_eq!(metric.abc.conditions(), 0);
1332            assert_eq!(metric.abc.magnitude(), 0.0);
1333        });
1334    }
1335
1336    #[test]
1337    fn java_bool_returning_terminal_kinds_count() {
1338        // Companion to `csharp_bool_returning_terminal_kinds_count`
1339        // (issue #372 / lesson #19). Java's grammar wraps every
1340        // if/while/do condition in `parenthesized_expression`, so
1341        // the gap lived in `java_inspect_container`'s terminal-arm
1342        // recognizer: `FieldAccess` (`cfg.flag`), `CastExpression`
1343        // (`(boolean)v`), `ArrayAccess` (`flags[0]`), and
1344        // `InstanceofExpression` (`x instanceof Foo`) were never
1345        // counted. Java has no `await` or `is_pattern` analogues,
1346        // so the C# fix's five-kind set collapses to four here.
1347        //
1348        // expected: 4 conditions (one per `if`), 0 assignments,
1349        // 0 branches (no invocations).
1350        check_metrics::<JavaParser>(
1351            "class Cfg { boolean flag; }
1352            class A {
1353                void m(Object v, boolean[] flags, Cfg cfg) {
1354                    if (cfg.flag) { }
1355                    if ((boolean) v) { }
1356                    if (v instanceof Cfg) { }
1357                    if (flags[0]) { }
1358                }
1359            }",
1360            "foo.java",
1361            |metric| {
1362                assert_eq!(metric.abc.conditions_sum(), 4);
1363                assert_eq!(metric.abc.assignments_sum(), 0);
1364                assert_eq!(metric.abc.branches_sum(), 0);
1365            },
1366        );
1367    }
1368
1369    #[test]
1370    fn groovy_no_abc() {
1371        // Comment-only file has no executable code → all-zero ABC.
1372        check_metrics::<GroovyParser>(
1373            "// just a comment, no executable code",
1374            "foo.groovy",
1375            |metric| {
1376                assert_eq!(metric.abc.assignments_sum(), 0);
1377                assert_eq!(metric.abc.branches_sum(), 0);
1378                assert_eq!(metric.abc.conditions_sum(), 0);
1379            },
1380        );
1381    }
1382
1383    #[test]
1384    fn groovy_single_assignment() {
1385        // `int x = 1` is a local-variable declaration whose `=` counts
1386        // as one assignment (matches Java's semantics).
1387        check_metrics::<GroovyParser>("int x = 1", "foo.groovy", |metric| {
1388            assert_eq!(metric.abc.assignments_sum(), 1);
1389            assert_eq!(metric.abc.branches_sum(), 0);
1390            assert_eq!(metric.abc.conditions_sum(), 0);
1391        });
1392    }
1393
1394    #[test]
1395    fn groovy_assignments() {
1396        check_metrics::<GroovyParser>(
1397            "void f() {
1398                int a = 1
1399                int b = 2
1400                a = 3
1401                b = 4
1402                a += 1
1403                b -= 1
1404            }",
1405            "foo.groovy",
1406            |metric| {
1407                // Six `=` tokens total. The two `Final`-less local
1408                // var-decls (`int a = 1`, `int b = 2`) and the two
1409                // bare assignments (`a = 3`, `b = 4`) each contribute
1410                // one assignment via the `EQ` arm; the `+=` / `-=`
1411                // each contribute one via the compound-assign arm.
1412                assert_eq!(metric.abc.assignments_sum(), 6);
1413            },
1414        );
1415    }
1416
1417    #[test]
1418    fn groovy_branches() {
1419        check_metrics::<GroovyParser>(
1420            "void f() {
1421                doStuff()
1422                helper.invoke()
1423                new Worker()
1424            }",
1425            "foo.groovy",
1426            |metric| {
1427                // 2 method invocations + 1 object creation = 3 branches
1428                assert_eq!(metric.abc.branches_sum(), 3);
1429            },
1430        );
1431    }
1432
1433    #[test]
1434    fn groovy_conditions_in_if() {
1435        check_metrics::<GroovyParser>(
1436            "void f(int a) {
1437                if (a == 0) { println(a) }
1438                if (a >= 1) { println(a) }
1439                if (a != 2) { println(a) }
1440            }",
1441            "foo.groovy",
1442            |metric| {
1443                // Three relational ops = 3 conditions
1444                assert_eq!(metric.abc.conditions_sum(), 3);
1445            },
1446        );
1447    }
1448
1449    #[test]
1450    fn groovy_branches_with_juxt_call() {
1451        // Groovy's parens-less call form `println foo` must be counted
1452        // as a branch (`JuxtFunctionCall`).
1453        check_metrics::<GroovyParser>(
1454            "void f() {
1455                println 'hi'
1456                println 'bye'
1457            }",
1458            "foo.groovy",
1459            |metric| {
1460                // 2 juxt calls = 2 branches.
1461                assert_eq!(metric.abc.branches_sum(), 2);
1462            },
1463        );
1464    }
1465
1466    #[test]
1467    fn groovy_try_catch_conditions() {
1468        // Each `try` and `catch` keyword token contributes +1 to
1469        // conditions (mirrors Java).
1470        check_metrics::<GroovyParser>(
1471            "void f() {
1472                try {
1473                    risky()
1474                } catch (Exception e) {
1475                    handle(e)
1476                }
1477            }",
1478            "foo.groovy",
1479            |metric| {
1480                // try + catch = 2 conditions
1481                assert_eq!(metric.abc.conditions_sum(), 2);
1482            },
1483        );
1484    }
1485
1486    #[test]
1487    fn groovy_ternary_conditions() {
1488        check_metrics::<GroovyParser>(
1489            "void f(int x) {
1490                def y = x > 0 ? 1 : 2
1491            }",
1492            "foo.groovy",
1493            |metric| {
1494                // QMARK alone is +1 condition, plus the `>` condition = 2.
1495                assert_eq!(metric.abc.conditions_sum(), 2);
1496            },
1497        );
1498    }
1499
1500    #[test]
1501    fn groovy_constant_excluded_from_assignments() {
1502        // `final` declarations are not counted as assignments
1503        // (mirrors Java's `Final` handling).
1504        check_metrics::<GroovyParser>(
1505            "class A {
1506                final int CONST = 42
1507                int field = 0
1508            }",
1509            "foo.groovy",
1510            |metric| {
1511                // The `=` on `final int CONST = 42` is a constant
1512                // initialiser (skipped). Only `field = 0` counts.
1513                assert_eq!(metric.abc.assignments_sum(), 1);
1514            },
1515        );
1516    }
1517
1518    #[test]
1519    fn groovy_malformed_parenthesized_no_panic() {
1520        // Regression: malformed Groovy input must not panic the ABC
1521        // walker; the `spaces.rs` Unit fallback (lesson 9) covers
1522        // structural recovery. amaanq's grammar treats `def x = (((`
1523        // as a `local_variable_declaration` whose initialiser is the
1524        // first opening paren — the `=` still fires the assignment
1525        // arm.
1526        check_metrics::<GroovyParser>("def x = (((", "foo.groovy", |metric| {
1527            assert_eq!(metric.abc.assignments_sum(), 1);
1528        });
1529    }
1530
1531    #[test]
1532    fn groovy_bool_returning_terminal_kinds_count() {
1533        // Companion to `csharp_bool_returning_terminal_kinds_count`
1534        // (issue #372 / lesson #19). The dekobon Groovy grammar
1535        // shares Java's wrapping conventions for `FieldAccess` and
1536        // `InstanceofExpression`, but it splits casts into two
1537        // distinct kinds — `cast_expression` for the Groovy-idiomatic
1538        // `v as Boolean` and `parenthesized_type_cast` for the
1539        // Java-style `(boolean) v`. The grammar has no `await` or
1540        // `array_access` analogues, so the C# fix's five-kind set
1541        // collapses to four here (with the cast slot doubled).
1542        //
1543        // expected: 4 conditions (one per `if`), 0 assignments,
1544        // 0 branches (no invocations).
1545        check_metrics::<GroovyParser>(
1546            "class Cfg { boolean flag }
1547            class A {
1548                void m(Object v, Cfg cfg) {
1549                    if (cfg.flag) { }
1550                    if ((boolean) v) { }
1551                    if (v as Boolean) { }
1552                    if (v instanceof Cfg) { }
1553                }
1554            }",
1555            "foo.groovy",
1556            |metric| {
1557                assert_eq!(metric.abc.conditions_sum(), 4);
1558                assert_eq!(metric.abc.assignments_sum(), 0);
1559                assert_eq!(metric.abc.branches_sum(), 0);
1560            },
1561        );
1562    }
1563
1564    #[test]
1565    fn groovy_if_multiple_conditions() {
1566        // Mirrors `java_if_multiple_conditions`: `&&` / `||` chains
1567        // and parenthesised unary forms each contribute one
1568        // condition per primitive comparison; the inspect-container
1569        // pass picks up the unary `!a` / `!b` arguments inside the
1570        // `BinaryExpression` and counts them too.
1571        check_metrics::<GroovyParser>(
1572            "void f(boolean a, boolean b, boolean c) {
1573                if (a || b || c) { println(a) }
1574                if (a && b && c) { println(a) }
1575                if (!a && !b) { println(a) }
1576            }",
1577            "foo.groovy",
1578            |metric| {
1579                // Conditions counted via the AMPAMP/PIPEPIPE arms
1580                // (one count per identifier in the chain — three
1581                // for `||`, three for `&&`, two for the unary chain)
1582                // = 8.
1583                assert_eq!(metric.abc.conditions_sum(), 8);
1584                // Three `println a` juxt calls — each is a branch.
1585                assert_eq!(metric.abc.branches_sum(), 3);
1586            },
1587        );
1588    }
1589
1590    #[test]
1591    fn groovy_while_and_do_while_conditions() {
1592        // Covers the WhileStatement and DoStatement arms in
1593        // `impl Abc for GroovyCode`. Each `while` / `do-while` has
1594        // its condition inspected through `groovy_inspect_container`.
1595        check_metrics::<GroovyParser>(
1596            "void f(boolean a, boolean b) {
1597                while (a) {
1598                    a = false
1599                }
1600                do {
1601                    b = !b
1602                } while (b)
1603            }",
1604            "foo.groovy",
1605            |metric| {
1606                // `while(a)` + `while(b)` each contribute one condition;
1607                // the unary `!b` on the do body's right-hand side adds
1608                // one more via the assignment-arm inspection = 3.
1609                assert_eq!(metric.abc.conditions_sum(), 3);
1610                // Two assignments to existing variables (`a = false`,
1611                // `b = !b`).
1612                assert_eq!(metric.abc.assignments_sum(), 2);
1613            },
1614        );
1615    }
1616
1617    #[test]
1618    fn groovy_if_while_boolean_literal_condition() {
1619        // Regression for the Groovy half of #371-class bugs: the
1620        // dekobon tree-sitter-groovy grammar wraps a bare
1621        // `true` / `false` literal used as the condition of
1622        // `if` / `while` / `do` / `?:` in a `boolean_literal` node
1623        // (`Groovy::BooleanLiteral`, kind_id 270), not the leaf
1624        // `True` / `False` keyword tokens. `groovy_count_condition`
1625        // must therefore match `BooleanLiteral` (the wrapper).
1626        // Without that, every literal-condition statement silently
1627        // scored 0 conditions. Mirror of
1628        // `csharp_if_while_boolean_literal_condition`.
1629        check_metrics::<GroovyParser>(
1630            "void m() {
1631                if (true) { println 'a' }
1632                if (false) { println 'b' }
1633                while (true) { break }
1634                int t = true ? 1 : 0
1635            }",
1636            "foo.groovy",
1637            |metric| {
1638                // Four literal-condition statements contribute 4
1639                // `BooleanLiteral` conditions (if / if / while /
1640                // ternary), plus the ternary's `?` token adds one
1641                // more via `groovy_count_token_condition` → 5
1642                // total. The `println` calls contribute 2 branches
1643                // (the `while` body's `break` is not a branch).
1644                // The `int t = …` initializer contributes 1
1645                // assignment.
1646                assert_eq!(metric.abc.conditions_sum(), 5);
1647                assert_eq!(metric.abc.branches_sum(), 2);
1648                assert_eq!(metric.abc.assignments_sum(), 1);
1649            },
1650        );
1651    }
1652
1653    #[test]
1654    fn groovy_return_unary_boolean_literal() {
1655        // Companion to `groovy_if_while_boolean_literal_condition`:
1656        // a `!true` / `!false` operand inside a `return` statement
1657        // routes through `groovy_inspect_container` (via
1658        // `groovy_inspect_child(node, 1)` on the ReturnStatement).
1659        // The `!` operator establishes boolean context, then the
1660        // innermost-operand check matches `BooleanLiteral` — that
1661        // helper's `BooleanLiteral` arm must be present or the
1662        // count silently drops. Mutation-verified: removing
1663        // `BooleanLiteral` from `groovy_inspect_container` leaves
1664        // every other Groovy test passing.
1665        check_metrics::<GroovyParser>(
1666            "boolean f() {
1667                return !true
1668            }
1669            boolean g() {
1670                return !false
1671            }",
1672            "foo.groovy",
1673            |metric| {
1674                // Each `return !X` walks into
1675                // `groovy_inspect_container` with a UnaryExpression
1676                // wrapping a `BANG` + BooleanLiteral. The `!` arm
1677                // seeds `has_boolean_content = true` (ReturnStatement
1678                // is not a known-boolean parent), then the
1679                // BooleanLiteral operand contributes one condition.
1680                // Two `return !X` → 2 conditions, no branches, no
1681                // assignments.
1682                assert_eq!(metric.abc.conditions_sum(), 2);
1683                assert_eq!(metric.abc.branches_sum(), 0);
1684                assert_eq!(metric.abc.assignments_sum(), 0);
1685            },
1686        );
1687    }
1688
1689    #[test]
1690    fn groovy_short_circuit_with_boolean_literal_operand() {
1691        // Companion to `groovy_if_while_boolean_literal_condition`:
1692        // a bare `true` / `false` operand of `&&` / `||` lands in
1693        // `groovy_count_unary_conditions`, which iterates the
1694        // parent BinaryExpression's children. That helper must
1695        // match the `BooleanLiteral` wrapper just like
1696        // `groovy_count_condition` does — otherwise the operand
1697        // silently scores zero. Mutation-verified: removing
1698        // `BooleanLiteral` from the `groovy_count_unary_conditions`
1699        // arm leaves every other Groovy test passing.
1700        check_metrics::<GroovyParser>(
1701            "void m(boolean x) {
1702                if (x && true) { println 'a' }
1703                if (false || x) { println 'b' }
1704            }",
1705            "foo.groovy",
1706            |metric| {
1707                // `&&` and `||` themselves are NOT in
1708                // `groovy_count_token_condition`'s match list —
1709                // they route through
1710                // `groovy_walk_for_conditions::AMPAMP|PIPEPIPE`,
1711                // which calls `groovy_count_unary_conditions` on
1712                // the parent BinaryExpression. Each invocation
1713                // counts every child that matches the terminal-
1714                // operand kinds and whose parent is a
1715                // BinaryExpression. For `x && true`: Identifier x
1716                // (+1) + BooleanLiteral true (+1) = 2. For
1717                // `false || x`: BooleanLiteral false (+1) +
1718                // Identifier x (+1) = 2. Total 4.
1719                assert_eq!(metric.abc.conditions_sum(), 4);
1720                assert_eq!(metric.abc.branches_sum(), 2);
1721                assert_eq!(metric.abc.assignments_sum(), 0);
1722            },
1723        );
1724    }
1725
1726    #[test]
1727    fn groovy_methods_arguments_with_conditions() {
1728        // Mirror of `java_methods_arguments_with_conditions`: a
1729        // unary `!x` inside an argument list must count both the
1730        // method invocation as a branch AND the unary as a
1731        // condition. The `ArgumentList | ArgumentList2` arm in
1732        // `impl Abc for GroovyCode` is what exercises this.
1733        check_metrics::<GroovyParser>(
1734            "void f(boolean a, boolean b, boolean c) {
1735                m1(a)
1736                m1(!a)
1737                m2(!a, !b)
1738            }",
1739            "foo.groovy",
1740            |metric| {
1741                // 3 method invocations (m1, m1, m2) — each fires the
1742                // branches arm.
1743                assert_eq!(metric.abc.branches_sum(), 3);
1744                // Three `!` unaries — `m1(!a)` and the two args of
1745                // `m2(!a, !b)` — each contribute one condition via
1746                // the ArgumentList inspection.
1747                assert_eq!(metric.abc.conditions_sum(), 3);
1748            },
1749        );
1750    }
1751
1752    #[test]
1753    fn groovy_return_with_conditions() {
1754        // Mirror of `java_return_with_conditions`: a parenthesised
1755        // or unary expression inside `return` flows through the
1756        // `ReturnStatement` arm to `groovy_inspect_container`.
1757        check_metrics::<GroovyParser>(
1758            "boolean f(boolean a) {
1759                return (a)
1760            }
1761            boolean g(boolean a) {
1762                return !a
1763            }",
1764            "foo.groovy",
1765            |metric| {
1766                // Only one of the two return forms surfaces a
1767                // condition: `return !a` hits the UnaryExpression
1768                // path and adds one; `return (a)` reaches
1769                // `groovy_inspect_container` but the inner
1770                // identifier `a` is not in a boolean-context-firing
1771                // parent, so no condition is added.
1772                assert_eq!(metric.abc.conditions_sum(), 1);
1773            },
1774        );
1775    }
1776
1777    #[test]
1778    fn groovy_for_with_variable_declaration() {
1779        // Classical `for (int i = 0; cond; i++)` form. The init
1780        // slot's `int i = 0` is suppressed from assignments by the
1781        // `LocalVariableDeclaration` push/pop dance; the `i++` in
1782        // the update slot contributes one assignment via the
1783        // `PLUSPLUS` arm. The condition `i < 10` flows through the
1784        // `ForStatement` arm.
1785        check_metrics::<GroovyParser>(
1786            "void f() {
1787                for (int i = 0; i < 10; i++) {
1788                    println(i)
1789                }
1790            }",
1791            "foo.groovy",
1792            |metric| {
1793                // `int i = 0` fires the EQ arm + `i++` fires the
1794                // PLUSPLUS arm = 2 assignments.
1795                assert_eq!(metric.abc.assignments_sum(), 2);
1796                // `i < 10` is one condition (the LT arm).
1797                assert_eq!(metric.abc.conditions_sum(), 1);
1798            },
1799        );
1800    }
1801
1802    #[test]
1803    fn groovy_eq_arm_increments_when_no_declaration() {
1804        // Bare reassignment of an already-declared variable: the
1805        // `EQ` arm fires when the declaration stack is empty
1806        // (`stats.declaration.last().is_none()`), so the `=` counts
1807        // as one assignment. Mirrors `java_eq_arm_increments_when_
1808        // declaration_stack_is_empty`.
1809        check_metrics::<GroovyParser>(
1810            "void f(int x) {
1811                x = 42
1812            }",
1813            "foo.groovy",
1814            |metric| {
1815                assert_eq!(metric.abc.assignments_sum(), 1);
1816                assert_eq!(metric.abc.branches_sum(), 0);
1817                assert_eq!(metric.abc.conditions_sum(), 0);
1818            },
1819        );
1820    }
1821
1822    #[test]
1823    fn csharp_constant_declarations() {
1824        check_metrics::<CsharpParser>(
1825            "class A {
1826                private const int X1 = 0, Y1 = 0;
1827                public const float PI = 3.14f;
1828                const string HELLO = \"Hello,\";
1829                protected string world = \" world!\";
1830                public float e = 2.718f;
1831                private int x2 = 1, y2 = 2;
1832                void M() {
1833                    const int Z1 = 0, Z2 = 0, Z3 = 0;
1834                    const float T = 0.0f;
1835                    int z1 = 1, z2 = 2, z3 = 3;
1836                }
1837            }",
1838            "foo.cs",
1839            |metric| insta::assert_json_snapshot!(metric.abc),
1840        );
1841    }
1842
1843    #[test]
1844    fn csharp_declarations_with_conditions() {
1845        check_metrics::<CsharpParser>(
1846            "class A {
1847                bool a = (1 == 2);
1848                bool b = (1 < 2);
1849                bool c = !true;
1850                bool d = !false;
1851            }",
1852            "foo.cs",
1853            |metric| insta::assert_json_snapshot!(metric.abc),
1854        );
1855    }
1856
1857    #[test]
1858    fn csharp_assignments_with_conditions() {
1859        check_metrics::<CsharpParser>(
1860            "class A {
1861                void M() {
1862                    int a = 0;
1863                    a += 1;
1864                    a -= 2;
1865                    a *= 3;
1866                    a /= 4;
1867                    a %= 5;
1868                    a++;
1869                    a--;
1870                }
1871            }",
1872            "foo.cs",
1873            |metric| insta::assert_json_snapshot!(metric.abc),
1874        );
1875    }
1876
1877    #[test]
1878    fn csharp_methods_arguments_with_conditions() {
1879        check_metrics::<CsharpParser>(
1880            "class A {
1881                void M(int x, int y) {
1882                    F(x == y, x < y, !x.Equals(y));
1883                }
1884                void F(bool a, bool b, bool c) {}
1885            }",
1886            "foo.cs",
1887            |metric| insta::assert_json_snapshot!(metric.abc),
1888        );
1889    }
1890
1891    #[test]
1892    fn csharp_if_single_conditions() {
1893        check_metrics::<CsharpParser>(
1894            "class A {
1895                void M(int x) {
1896                    if (x > 0) { System.Console.WriteLine(\"a\"); }
1897                    if (x < 0) { System.Console.WriteLine(\"b\"); }
1898                    if (x == 0) { System.Console.WriteLine(\"c\"); }
1899                }
1900            }",
1901            "foo.cs",
1902            |metric| insta::assert_json_snapshot!(metric.abc),
1903        );
1904    }
1905
1906    #[test]
1907    fn csharp_if_multiple_conditions() {
1908        check_metrics::<CsharpParser>(
1909            "class A {
1910                void M(int x, int y) {
1911                    if (x > 0 && y > 0) { System.Console.WriteLine(\"a\"); }
1912                    if (x < 0 || y < 0) { System.Console.WriteLine(\"b\"); }
1913                }
1914            }",
1915            "foo.cs",
1916            |metric| insta::assert_json_snapshot!(metric.abc),
1917        );
1918    }
1919
1920    #[test]
1921    fn csharp_while_and_do_while_conditions() {
1922        check_metrics::<CsharpParser>(
1923            "class A {
1924                void M(int x) {
1925                    while (x > 0) { x--; }
1926                    do { x++; } while (x < 10);
1927                }
1928            }",
1929            "foo.cs",
1930            |metric| insta::assert_json_snapshot!(metric.abc),
1931        );
1932    }
1933
1934    #[test]
1935    fn csharp_return_with_conditions() {
1936        check_metrics::<CsharpParser>(
1937            "class A {
1938                bool M(int x) {
1939                    return (x > 0);
1940                }
1941                bool N(int x) {
1942                    return !(x < 0);
1943                }
1944            }",
1945            "foo.cs",
1946            |metric| insta::assert_json_snapshot!(metric.abc),
1947        );
1948    }
1949
1950    // C# `switch` *expression* arms scored zero ABC conditions before
1951    // #456 — they carry no `case` / `default` token, so the token-driven
1952    // `csharp_count_token_condition` never saw them, even though C#
1953    // cyclomatic counts each non-discard arm. Revert-verified: adding the
1954    // gated `SwitchExpressionArm` arm is what lifts this from 0 to 2. The
1955    // bare `_ =>` discard arm is excluded (the `default:` analogue),
1956    // mirroring the cyclomatic gate (lesson 11).
1957    #[test]
1958    fn csharp_switch_expression_arm_counts_condition() {
1959        check_metrics::<CsharpParser>(
1960            "class A {
1961                int M(int x) {
1962                    return x switch { 1 => 10, 2 => 20, _ => 0 };
1963                }
1964            }",
1965            "foo.cs",
1966            |metric| {
1967                // arm `1 =>` (+1) + arm `2 =>` (+1) + `_ =>` discard (+0).
1968                assert_eq!(metric.abc.conditions_sum(), 2);
1969            },
1970        );
1971    }
1972
1973    // Cross-language parity (lesson 11): a C# `switch` expression and the
1974    // equivalent Java arrow-`switch` must report the same ABC condition
1975    // count on equivalent code. Both have two concrete case arms and no
1976    // fallback arm, so both must count exactly 2. `check_metrics` takes a
1977    // non-capturing `fn` pointer, so the shared expected value (2) is
1978    // asserted in each callback rather than compared across closures; the
1979    // matching constant is what enforces parity. This guards against the
1980    // C# fix drifting away from the Java arrow-case treatment.
1981    #[test]
1982    fn csharp_java_switch_arm_abc_parity() {
1983        // C# switch expression: two arms, no fallback → 2 conditions.
1984        check_metrics::<CsharpParser>(
1985            "class A {
1986                int M(int x) {
1987                    return x switch { 1 => 10, 2 => 20 };
1988                }
1989            }",
1990            "foo.cs",
1991            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
1992        );
1993
1994        // Equivalent Java arrow-`switch`: two case arms, no default → 2.
1995        check_metrics::<JavaParser>(
1996            "class A {
1997                int m(int x) {
1998                    return switch (x) { case 1 -> 10; case 2 -> 20; };
1999                }
2000            }",
2001            "foo.java",
2002            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2003        );
2004    }
2005
2006    // Issue #469: the `default` arm of a C-family `switch` is the
2007    // unconditional fallthrough and must NOT count as an ABC condition,
2008    // mirroring cyclomatic — which counts only the `Case` arms, never
2009    // the `Default` token.
2010    //
2011    // expected: each fixture is a single function whose switch has two
2012    // concrete `case` arms plus one `default`. ABC must count exactly
2013    // the two case arms (conditions = 2), matching cyclomatic's two
2014    // case-arm decisions. Pre-fix, every language below scored 3 (the
2015    // `Default` token leaked into the condition tally) — revert-verified
2016    // against the pre-#469 condition arms. We anchor on the integer
2017    // `conditions_sum()` headline (the value the public JSON serializes;
2018    // float magnitude is bit-brittle and excluded by the snapshot
2019    // policy). The cyclomatic side is pinned separately in
2020    // `java_csharp_cpp_switch_default_cyclomatic_parity` below, where
2021    // the per-space `cyclomatic()` decision count is isolated.
2022    #[test]
2023    fn java_switch_default_not_a_condition() {
2024        // Classic statement `default:`.
2025        check_metrics::<JavaParser>(
2026            "class A {
2027                int m(int x) {
2028                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2029                }
2030            }",
2031            "foo.java",
2032            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2033        );
2034        // Arrow `default ->` — shares the same `Default` token.
2035        check_metrics::<JavaParser>(
2036            "class A {
2037                int m(int x) {
2038                    return switch (x) { case 1 -> 1; case 2 -> 2; default -> 0; };
2039                }
2040            }",
2041            "foo.java",
2042            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2043        );
2044    }
2045
2046    // Over-exclusion guard (issue #469): a statement `switch` with two
2047    // `case` arms and NO `default` must still count both cases. This
2048    // pins that the fix excludes only the `Default` token, never a
2049    // `Case` arm — the count is identical before and after #469 (two
2050    // cases → 2), so it would catch a fix that over-eagerly dropped a
2051    // real case (e.g. treating the trailing case as a fallthrough).
2052    // expected: case 1 (+1) + case 2 (+1) = 2.
2053    #[test]
2054    fn java_switch_without_default_counts_all_cases() {
2055        check_metrics::<JavaParser>(
2056            "class A {
2057                int m(int x) {
2058                    switch (x) { case 1: return 1; case 2: return 2; }
2059                    return -1;
2060                }
2061            }",
2062            "foo.java",
2063            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2064        );
2065    }
2066
2067    #[test]
2068    fn csharp_switch_default_not_a_condition() {
2069        check_metrics::<CsharpParser>(
2070            "class A {
2071                int M(int x) {
2072                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2073                }
2074            }",
2075            "foo.cs",
2076            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2077        );
2078    }
2079
2080    #[test]
2081    fn cpp_switch_default_not_a_condition() {
2082        // C++ (and plain C, which shares this grammar) already excluded
2083        // `default`; this pins the cross-language parity invariant.
2084        check_metrics::<CppParser>(
2085            "void f(int x) {
2086                 switch (x) { case 1: return; case 2: return; default: return; }
2087             }",
2088            "foo.cpp",
2089            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2090        );
2091    }
2092
2093    #[test]
2094    fn objc_abc() {
2095        // ObjC ABC reuses the C/C++ walker with two additions: a message
2096        // send `[obj msg]` is a call (B), and `@try` / `@catch` count as
2097        // conditions (C) like C++ try/catch.
2098        //   A: `int total = 0`, `int i = 0`, `i++`, `total = total + …`,
2099        //      `total = -1` = 5.
2100        //   B: `[self valueAt:i]`, `[self risky]` = 2 message sends.
2101        //   C: `i < n`, `@try`, `@catch`, `total >= 0` = 4.
2102        check_metrics::<ObjcParser>(
2103            "@implementation Foo\n\
2104             - (int)bar:(int)n {\n\
2105                 int total = 0;\n\
2106                 for (int i = 0; i < n; i++) {\n\
2107                     total = total + [self valueAt:i];\n\
2108                 }\n\
2109                 @try {\n\
2110                     [self risky];\n\
2111                 } @catch (NSException *e) {\n\
2112                     total = -1;\n\
2113                 }\n\
2114                 if (total >= 0) {\n\
2115                     return total;\n\
2116                 }\n\
2117                 return 0;\n\
2118             }\n\
2119             @end\n",
2120            "foo.m",
2121            |metric| {
2122                assert_eq!(metric.abc.assignments_sum(), 5);
2123                assert_eq!(metric.abc.branches_sum(), 2);
2124                assert_eq!(metric.abc.conditions_sum(), 4);
2125            },
2126        );
2127    }
2128
2129    #[test]
2130    fn objc_abc_conditions() {
2131        // Exercises the condition-slot arms shared with C/C++: a `while`
2132        // head, a `&&` chain, a `do … while` trailing condition, and a
2133        // `return <comparison>`. ObjC routes these through the same
2134        // grammar-agnostic `cpp_inspect_*` helpers.
2135        //   A: `p++`, `p--` = 2.   B: no calls = 0.
2136        //   C: `p != 0`, `*p > 0`, `*p < 9`, `*p == 0` = 4.
2137        check_metrics::<ObjcParser>(
2138            "@implementation Foo\n\
2139             - (int)g:(int *)p {\n\
2140                 while (p != 0 && *p > 0) {\n\
2141                     p++;\n\
2142                 }\n\
2143                 do {\n\
2144                     p--;\n\
2145                 } while (*p < 9);\n\
2146                 return *p == 0;\n\
2147             }\n\
2148             @end\n",
2149            "foo.m",
2150            |metric| {
2151                assert_eq!(metric.abc.assignments_sum(), 2);
2152                assert_eq!(metric.abc.branches_sum(), 0);
2153                assert_eq!(metric.abc.conditions_sum(), 4);
2154            },
2155        );
2156    }
2157
2158    #[test]
2159    fn objc_abc_message_send_unary_condition() {
2160        // A negated boolean passed as a message-send argument is a unary
2161        // condition (Fitzpatrick Rule 9), the same as in a C-call argument.
2162        // Message args are direct children of `message_expression` (no
2163        // `argument_list`), so they are inspected in the `MessageExpression`
2164        // arm. Here: `[self use:!a]` (1 call + 1 unary condition) +
2165        // `cFunc(!a)` (1 call + 1 unary condition) → B=2, C=2.
2166        check_metrics::<ObjcParser>(
2167            "@implementation Foo\n\
2168             - (void)bar:(int)a {\n\
2169                 [self use:!a];\n\
2170                 cFunc(!a);\n\
2171             }\n\
2172             @end\n",
2173            "foo.m",
2174            |metric| {
2175                assert_eq!(metric.abc.branches_sum(), 2);
2176                assert_eq!(metric.abc.conditions_sum(), 2);
2177            },
2178        );
2179    }
2180
2181    #[test]
2182    fn groovy_switch_default_not_a_condition() {
2183        check_metrics::<GroovyParser>(
2184            "class A {
2185                int m(int x) {
2186                    switch (x) { case 1: return 1; case 2: return 2; default: return 0 }
2187                }
2188            }",
2189            "foo.groovy",
2190            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2191        );
2192    }
2193
2194    #[test]
2195    fn js_switch_default_not_a_condition() {
2196        check_metrics::<JavascriptParser>(
2197            "function f(x) {
2198                 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2199             }",
2200            "foo.js",
2201            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2202        );
2203    }
2204
2205    #[test]
2206    fn ts_switch_default_not_a_condition() {
2207        check_metrics::<TypescriptParser>(
2208            "function f(x: number): number {
2209                 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2210             }",
2211            "foo.ts",
2212            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2213        );
2214    }
2215
2216    // Cross-language parity (lesson 11): the equivalent statement-`switch`
2217    // with a `default` arm reports the same ABC condition count across
2218    // Java / C# / C++. All three have two concrete case arms plus a
2219    // fallthrough `default`, so all three must count exactly 2 conditions
2220    // (the `default` excluded). `check_metrics` takes a non-capturing
2221    // `fn` pointer, so the shared expected value is asserted in each
2222    // callback; the matching constant is what enforces parity.
2223    #[test]
2224    fn java_csharp_cpp_switch_default_abc_parity() {
2225        check_metrics::<JavaParser>(
2226            "class A {
2227                int m(int x) {
2228                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2229                }
2230            }",
2231            "foo.java",
2232            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2233        );
2234        check_metrics::<CsharpParser>(
2235            "class A {
2236                int M(int x) {
2237                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2238                }
2239            }",
2240            "foo.cs",
2241            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2242        );
2243        check_metrics::<CppParser>(
2244            "void f(int x) {
2245                 switch (x) { case 1: return; case 2: return; default: return; }
2246             }",
2247            "foo.cpp",
2248            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2249        );
2250    }
2251
2252    // Pins the ABC-vs-cyclomatic agreement the fix is about (lesson 11):
2253    // on the method's own function space, the cyclomatic decision count
2254    // (`cyclomatic()` minus the per-space base of 1) must equal the ABC
2255    // `conditions()` for the same switch. Both must be 2 — the two case
2256    // arms — with the `default` excluded from each. Revert-verified: pre-
2257    // #469 ABC `conditions()` was 3 here while cyclomatic stayed at 2.
2258    #[test]
2259    fn java_csharp_cpp_switch_default_cyclomatic_parity() {
2260        // Recurse to the deepest function space (the method holding the
2261        // switch) and assert per-space cyclomatic decisions == ABC
2262        // conditions.
2263        fn assert_deepest(space: &crate::FuncSpace) {
2264            if let Some(child) = space.spaces.last() {
2265                assert_deepest(child);
2266                return;
2267            }
2268            // Per-space cyclomatic base is 1; the two case arms add 2.
2269            let decisions = space.metrics.cyclomatic.cyclomatic() - 1;
2270            assert_eq!(decisions, 2);
2271            assert_eq!(space.metrics.abc.conditions(), decisions);
2272        }
2273        check_func_space::<JavaParser, _>(
2274            "class A {
2275                int m(int x) {
2276                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2277                }
2278            }",
2279            "foo.java",
2280            |space| assert_deepest(&space),
2281        );
2282        check_func_space::<CsharpParser, _>(
2283            "class A {
2284                int M(int x) {
2285                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2286                }
2287            }",
2288            "foo.cs",
2289            |space| assert_deepest(&space),
2290        );
2291        check_func_space::<CppParser, _>(
2292            "void f(int x) {
2293                 switch (x) { case 1: return; case 2: return; default: return; }
2294             }",
2295            "foo.cpp",
2296            |space| assert_deepest(&space),
2297        );
2298    }
2299
2300    // Issue #473: PHP `switch` `default:` (`DefaultStatement`) is the
2301    // unconditional fallthrough, not a condition. ABC `conditions()` must
2302    // equal the cyclomatic decision count (`cyclomatic() - 1`) on the
2303    // function's own space — both 2 for the two `case` arms, with the
2304    // `default` excluded. Revert-verified: re-adding `DefaultStatement` to
2305    // the PHP ABC condition arm makes `conditions()` 3 here while cyclomatic
2306    // stays at 2, failing the invariant.
2307    #[test]
2308    fn php_switch_default_not_a_condition() {
2309        check_func_space::<PhpParser, _>(
2310            "<?php
2311            function f($x) {
2312                switch ($x) {
2313                    case 1: return 1;
2314                    case 2: return 2;
2315                    default: return 0;
2316                }
2317            }",
2318            "foo.php",
2319            |space| {
2320                fn assert_deepest(space: &crate::FuncSpace) {
2321                    if let Some(child) = space.spaces.last() {
2322                        assert_deepest(child);
2323                        return;
2324                    }
2325                    let decisions = space.metrics.cyclomatic.cyclomatic() - 1;
2326                    assert_eq!(decisions, 2);
2327                    assert_eq!(space.metrics.abc.conditions(), decisions);
2328                }
2329                assert_deepest(&space);
2330            },
2331        );
2332    }
2333
2334    // Issue #473: PHP `match` `default =>` (`MatchDefaultExpression`) is the
2335    // unconditional fallthrough, mirroring the switch `default:` case above.
2336    // ABC `conditions()` must equal the cyclomatic decision count
2337    // (`cyclomatic() - 1`) — both 2 for the two non-default match arms.
2338    // Revert-verified: re-adding `MatchDefaultExpression` to the PHP ABC
2339    // condition arm makes `conditions()` 3 here while cyclomatic stays at 2.
2340    #[test]
2341    fn php_match_default_not_a_condition() {
2342        check_func_space::<PhpParser, _>(
2343            "<?php
2344            function g($x) {
2345                return match ($x) {
2346                    1 => \"a\",
2347                    2 => \"b\",
2348                    default => \"z\",
2349                };
2350            }",
2351            "foo.php",
2352            |space| {
2353                fn assert_deepest(space: &crate::FuncSpace) {
2354                    if let Some(child) = space.spaces.last() {
2355                        assert_deepest(child);
2356                        return;
2357                    }
2358                    let decisions = space.metrics.cyclomatic.cyclomatic() - 1;
2359                    assert_eq!(decisions, 2);
2360                    assert_eq!(space.metrics.abc.conditions(), decisions);
2361                }
2362                assert_deepest(&space);
2363            },
2364        );
2365    }
2366
2367    #[test]
2368    fn csharp_if_bare_identifier_condition() {
2369        check_metrics::<CsharpParser>(
2370            "class A {
2371                void M(bool x) {
2372                    if (x) { System.Console.WriteLine(\"a\"); }
2373                }
2374            }",
2375            "foo.cs",
2376            |metric| {
2377                // `if (x)` contributes 1 condition (bare identifier).
2378                // `System.Console.WriteLine(...)` is the only call → 1 branch.
2379                // `*_sum()` is what the public JSON serializes as the
2380                // headline value (see `crate::wire::Abc`).
2381                assert_eq!(metric.abc.conditions_sum(), 1);
2382                assert_eq!(metric.abc.branches_sum(), 1);
2383                assert_eq!(metric.abc.assignments_sum(), 0);
2384            },
2385        );
2386    }
2387
2388    #[test]
2389    fn csharp_while_bare_identifier_condition() {
2390        check_metrics::<CsharpParser>(
2391            "class A {
2392                void M(bool x) {
2393                    while (x) { x = false; }
2394                }
2395            }",
2396            "foo.cs",
2397            |metric| {
2398                // `while (x)` contributes 1 condition; `x = false` is 1 assignment.
2399                assert_eq!(metric.abc.conditions_sum(), 1);
2400                assert_eq!(metric.abc.assignments_sum(), 1);
2401                assert_eq!(metric.abc.branches_sum(), 0);
2402            },
2403        );
2404    }
2405
2406    #[test]
2407    fn csharp_do_while_bare_identifier_condition() {
2408        check_metrics::<CsharpParser>(
2409            "class A {
2410                void M(bool x) {
2411                    do { x = true; } while (x);
2412                }
2413            }",
2414            "foo.cs",
2415            |metric| {
2416                // `do { ... } while (x)` contributes 1 condition;
2417                // `x = true` is 1 assignment.
2418                assert_eq!(metric.abc.conditions_sum(), 1);
2419                assert_eq!(metric.abc.assignments_sum(), 1);
2420                assert_eq!(metric.abc.branches_sum(), 0);
2421            },
2422        );
2423    }
2424
2425    #[test]
2426    fn csharp_if_unary_not_condition() {
2427        // Two cases share one test:
2428        //
2429        //   if (!x) { … }  — IfStatement is a known-boolean parent, so
2430        //   the unary `!` arm in `csharp_inspect_container` is *one of
2431        //   two* ways `has_boolean_content` gets set to true (the parent
2432        //   seed sets it before the `!` does). A regression that broke
2433        //   only the `is_not` branch wouldn't show up here.
2434        //
2435        //   return !x;  — ReturnStatement is *not* in the boolean-context
2436        //   seed list (BinaryExpression | IfStatement | WhileStatement |
2437        //   DoStatement | ForStatement | ConditionalExpression). So the
2438        //   `!` wrapper is the *only* path that sets
2439        //   `has_boolean_content = true`. Asserting the `return !x;`
2440        //   case isolates the unary-unwrap logic from the parent-seed
2441        //   path.
2442        check_metrics::<CsharpParser>(
2443            "class A {
2444                void M(bool x) {
2445                    if (!x) { System.Console.WriteLine(\"a\"); }
2446                }
2447                bool N(bool x) {
2448                    return !x;
2449                }
2450            }",
2451            "foo.cs",
2452            |metric| {
2453                // `if (!x)` contributes 1 condition (PrefixUnaryExpression
2454                // path with parent IfStatement seeding has_boolean_content).
2455                // `return !x;` contributes 1 condition (parent doesn't seed
2456                // — the unary `!` is the only path that sets the flag).
2457                // → 2 conditions total. 1 branch from WriteLine().
2458                assert_eq!(metric.abc.conditions_sum(), 2);
2459                assert_eq!(metric.abc.branches_sum(), 1);
2460                assert_eq!(metric.abc.assignments_sum(), 0);
2461            },
2462        );
2463    }
2464
2465    #[test]
2466    fn csharp_if_double_parenthesized_condition() {
2467        // Audit-tests follow-up: with only the
2468        // `csharp_prefix_unary_expr_kinds!()` arm covered by
2469        // `csharp_if_unary_not_condition`, the
2470        // `csharp_paren_expr_kinds!()` delegation arm in
2471        // `csharp_count_condition` was a pure dead-code candidate —
2472        // disabling it caused zero existing tests to fail (verified
2473        // 2026-05-26).
2474        //
2475        // `if ((x))` puts a `ParenthesizedExpression` at child(2) of
2476        // the IfStatement (child(1) is the literal `(`, child(2) is
2477        // the inner parenthesised expression, child(3) is the literal
2478        // `)`). `csharp_count_condition` must route that case to
2479        // `csharp_inspect_container`, which then sees parent =
2480        // IfStatement, seeds `has_boolean_content = true`, walks to
2481        // the inner Identifier, and counts it. A regression that
2482        // removed the paren arm would silently score 0.
2483        check_metrics::<CsharpParser>(
2484            "class A {
2485                void M(bool x) {
2486                    if ((x)) { System.Console.WriteLine(\"a\"); }
2487                }
2488            }",
2489            "foo.cs",
2490            |metric| {
2491                assert_eq!(metric.abc.conditions_sum(), 1);
2492                assert_eq!(metric.abc.branches_sum(), 1);
2493                assert_eq!(metric.abc.assignments_sum(), 0);
2494            },
2495        );
2496    }
2497
2498    #[test]
2499    fn csharp_bool_returning_terminal_kinds_count() {
2500        // Regression for issue #372 (lesson #19): before the fix,
2501        // `csharp_count_condition` / `csharp_inspect_container` only
2502        // recognised invocation / identifier / boolean literal as
2503        // terminal-bool operands, so the five idiomatic boolean
2504        // expressions in the `if (...)` slots below silently scored
2505        // zero conditions:
2506        //
2507        //   - `cfg.flag`        — MemberAccessExpression
2508        //   - `await c.Check()` — AwaitExpression
2509        //   - `(bool)v`         — CastExpression
2510        //   - `v is not null`   — IsPatternExpression
2511        //   - `flags[0]`        — ElementAccessExpression
2512        //
2513        // expected: 5 conditions (one per `if`), 0 assignments,
2514        // 1 branch (the single `c.Check()` invocation; the other
2515        // `if`-condition expressions are not invocations).
2516        check_metrics::<CsharpParser>(
2517            "using System.Threading.Tasks;
2518            class A {
2519                async Task M(object v, bool[] flags, Cfg cfg, C c) {
2520                    if (cfg.flag) { }
2521                    if (await c.Check()) { }
2522                    if ((bool)v) { }
2523                    if (v is not null) { }
2524                    if (flags[0]) { }
2525                }
2526            }
2527            class Cfg { public bool flag; }
2528            class C { public Task<bool> Check() => null; }",
2529            "foo.cs",
2530            |metric| {
2531                assert_eq!(metric.abc.conditions_sum(), 5);
2532                assert_eq!(metric.abc.assignments_sum(), 0);
2533                assert_eq!(metric.abc.branches_sum(), 1);
2534            },
2535        );
2536    }
2537
2538    #[test]
2539    fn csharp_if_method_call_condition() {
2540        check_metrics::<CsharpParser>(
2541            "class A {
2542                void M(string s) {
2543                    if (s.StartsWith(\"x\")) { System.Console.WriteLine(\"a\"); }
2544                }
2545            }",
2546            "foo.cs",
2547            |metric| {
2548                // `if (s.StartsWith("x"))` contributes 1 condition
2549                // (InvocationExpression) plus 1 branch for the call itself,
2550                // plus 1 branch for WriteLine.
2551                assert_eq!(metric.abc.conditions_sum(), 1);
2552                assert_eq!(metric.abc.branches_sum(), 2);
2553                assert_eq!(metric.abc.assignments_sum(), 0);
2554            },
2555        );
2556    }
2557
2558    #[test]
2559    fn csharp_if_while_boolean_literal_condition() {
2560        // Regression for #371: the tree-sitter-c-sharp grammar wraps a
2561        // bare `true` / `false` literal used as the condition of
2562        // `if` / `while` / `do` / `?:` in a `boolean_literal` node,
2563        // not the leaf `true` / `false` tokens. `csharp_count_condition`
2564        // must therefore match `BooleanLiteral` (the wrapper),
2565        // mirroring the existing `csharp_walk_for_statement` arm.
2566        // Without that, every literal-condition statement scored 0
2567        // conditions. The sibling `csharp_count_unary_conditions`
2568        // arm is covered separately by
2569        // `csharp_short_circuit_with_boolean_literal_operand` and
2570        // `csharp_inspect_container` is covered by
2571        // `csharp_declarations_with_conditions` (`!true` / `!false`).
2572        check_metrics::<CsharpParser>(
2573            "class A {
2574                void M() {
2575                    if (true) { System.Console.WriteLine(\"a\"); }
2576                    if (false) { System.Console.WriteLine(\"b\"); }
2577                    while (true) { break; }
2578                    do { break; } while (false);
2579                    int t = true ? 1 : 0;
2580                }
2581            }",
2582            "foo.cs",
2583            |metric| {
2584                // Five literal-condition statements contribute 5
2585                // `BooleanLiteral` conditions (one per if/if/while/
2586                // do-while/ternary), plus the ternary's `?` token
2587                // adds one more via `csharp_count_token_condition`
2588                // → 6 total. The two `System.Console.WriteLine`
2589                // calls contribute 2 branches; the `int t = …`
2590                // initializer contributes 1 assignment.
2591                assert_eq!(metric.abc.conditions_sum(), 6);
2592                assert_eq!(metric.abc.branches_sum(), 2);
2593                assert_eq!(metric.abc.assignments_sum(), 1);
2594            },
2595        );
2596    }
2597
2598    #[test]
2599    fn csharp_short_circuit_with_boolean_literal_operand() {
2600        // Regression for #371 (companion to
2601        // `csharp_if_while_boolean_literal_condition`): a bare
2602        // `true` / `false` operand of `&&` / `||` lands in
2603        // `csharp_count_unary_conditions`, which iterates the parent
2604        // BinaryExpression's children. That helper must match the
2605        // `BooleanLiteral` wrapper just like `csharp_count_condition`
2606        // does — otherwise the operand silently scores zero. Mutation-
2607        // verified: removing `BooleanLiteral` from the
2608        // `csharp_count_unary_conditions` arm leaves every other test
2609        // in the suite passing, so this is the only test guarding
2610        // that helper's literal-operand path.
2611        check_metrics::<CsharpParser>(
2612            "class A {
2613                void M(bool x) {
2614                    if (x && true) { System.Console.WriteLine(\"a\"); }
2615                    if (false || x) { System.Console.WriteLine(\"b\"); }
2616                }
2617            }",
2618            "foo.cs",
2619            |metric| {
2620                // `&&` and `||` themselves are NOT in
2621                // `csharp_count_token_condition`'s match list — they
2622                // route through `csharp_walk_for_conditions::AMPAMP|
2623                // PIPEPIPE`, which calls
2624                // `csharp_count_unary_conditions` on the parent
2625                // BinaryExpression. Each invocation counts every
2626                // child that matches the terminal-operand kinds and
2627                // whose parent is a BinaryExpression. For
2628                // `x && true`: 1 (Identifier x) + 1 (BooleanLiteral
2629                // true) = 2. For `false || x`: 1 (BooleanLiteral
2630                // false) + 1 (Identifier x) = 2. Total 4. Without
2631                // the BooleanLiteral arm only the two Identifier
2632                // counts would land, giving 2.
2633                assert_eq!(metric.abc.conditions_sum(), 4);
2634                assert_eq!(metric.abc.branches_sum(), 2);
2635                assert_eq!(metric.abc.assignments_sum(), 0);
2636            },
2637        );
2638    }
2639
2640    #[test]
2641    fn csharp_return_without_conditions() {
2642        check_metrics::<CsharpParser>(
2643            "class A {
2644                int M() { return 42; }
2645                string N() { return \"hi\"; }
2646            }",
2647            "foo.cs",
2648            |metric| insta::assert_json_snapshot!(metric.abc),
2649        );
2650    }
2651
2652    #[test]
2653    fn csharp_lambda_expressions_return_with_conditions() {
2654        check_metrics::<CsharpParser>(
2655            "class A {
2656                public void M() {
2657                    System.Func<int, bool> f = x => (x > 0);
2658                    System.Func<int, bool> g = x => !(x < 0);
2659                }
2660            }",
2661            "foo.cs",
2662            |metric| insta::assert_json_snapshot!(metric.abc),
2663        );
2664    }
2665
2666    #[test]
2667    fn csharp_for_with_variable_declaration() {
2668        check_metrics::<CsharpParser>(
2669            "class A {
2670                void M() {
2671                    for (int i = 0; i < 10; i++) {
2672                        System.Console.WriteLine(i);
2673                    }
2674                }
2675            }",
2676            "foo.cs",
2677            |metric| insta::assert_json_snapshot!(metric.abc),
2678        );
2679    }
2680
2681    #[test]
2682    fn csharp_for_without_variable_declaration() {
2683        check_metrics::<CsharpParser>(
2684            "class A {
2685                void M() {
2686                    int i;
2687                    for (i = 0; i < 10; i++) {
2688                        System.Console.WriteLine(i);
2689                    }
2690                }
2691            }",
2692            "foo.cs",
2693            |metric| insta::assert_json_snapshot!(metric.abc),
2694        );
2695    }
2696
2697    #[test]
2698    fn csharp_for_identifier_condition() {
2699        check_metrics::<CsharpParser>(
2700            "class A {
2701                void M(bool ready) {
2702                    for (; ready ;) { }
2703                }
2704            }",
2705            "foo.cs",
2706            |metric| {
2707                // expected: assignments=0 (no `=` / `++` / `--`),
2708                // branches=0 (no invocation / object creation),
2709                // conditions=1 (bare-identifier for-loop condition).
2710                // Averages divide by 3 spaces (top-level + class + method).
2711                insta::assert_json_snapshot!(
2712                    metric.abc,
2713                    @r#"
2714                {
2715                  "assignments": 0,
2716                  "branches": 0,
2717                  "conditions": 1,
2718                  "magnitude": 1.0,
2719                  "value": 0.0,
2720                  "assignments_average": 0.0,
2721                  "branches_average": 0.0,
2722                  "conditions_average": 0.3333333333333333,
2723                  "assignments_min": 0,
2724                  "assignments_max": 0,
2725                  "branches_min": 0,
2726                  "branches_max": 0,
2727                  "conditions_min": 0,
2728                  "conditions_max": 1
2729                }
2730                "#
2731                );
2732            },
2733        );
2734    }
2735
2736    #[test]
2737    fn csharp_for_invocation_condition() {
2738        check_metrics::<CsharpParser>(
2739            "class A {
2740                bool Ok() { return true; }
2741                void M() {
2742                    for (; Ok() ;) { }
2743                }
2744            }",
2745            "foo.cs",
2746            |metric| {
2747                // expected: assignments=0, branches=1 (the `Ok()` call),
2748                // conditions=1 (invocation as for-loop condition).
2749                // Averages divide by 4 spaces (top-level + class + two
2750                // methods).
2751                insta::assert_json_snapshot!(
2752                    metric.abc,
2753                    @r#"
2754                {
2755                  "assignments": 0,
2756                  "branches": 1,
2757                  "conditions": 1,
2758                  "magnitude": 1.4142135623730951,
2759                  "value": 0.0,
2760                  "assignments_average": 0.0,
2761                  "branches_average": 0.25,
2762                  "conditions_average": 0.25,
2763                  "assignments_min": 0,
2764                  "assignments_max": 0,
2765                  "branches_min": 0,
2766                  "branches_max": 1,
2767                  "conditions_min": 0,
2768                  "conditions_max": 1
2769                }
2770                "#
2771                );
2772            },
2773        );
2774    }
2775
2776    // Regression coverage for #279: the C# grammar wraps a literal
2777    // `true` / `false` for-loop condition in a `boolean_literal` node.
2778    // The `BooleanLiteral` arm in the `ForStatement` dispatch must
2779    // attribute one condition; without it, `for (; true ;)` would
2780    // contribute 0 (the bug fixed by this commit also affected this
2781    // shape).
2782    #[test]
2783    fn csharp_for_boolean_literal_condition() {
2784        check_metrics::<CsharpParser>(
2785            "class A {
2786                void M() {
2787                    for (; true ;) { }
2788                }
2789            }",
2790            "foo.cs",
2791            |metric| {
2792                // expected: assignments=0, branches=0,
2793                // conditions=1 (the `true` literal as condition).
2794                assert_eq!(metric.abc.conditions_sum(), 1);
2795                assert_eq!(metric.abc.assignments_sum(), 0);
2796                assert_eq!(metric.abc.branches_sum(), 0);
2797            },
2798        );
2799    }
2800
2801    // Regression coverage for #279: an empty for-loop condition such as
2802    // `for (; ;) {}` must contribute 0 to conditions — there is no
2803    // condition node to count.
2804    #[test]
2805    fn csharp_for_empty_condition() {
2806        check_metrics::<CsharpParser>(
2807            "class A {
2808                void M() {
2809                    for (; ;) { }
2810                }
2811            }",
2812            "foo.cs",
2813            |metric| {
2814                // expected: assignments=0, branches=0, conditions=0
2815                // (no condition expression in `for (; ;)`).
2816                insta::assert_json_snapshot!(
2817                    metric.abc,
2818                    @r#"
2819                {
2820                  "assignments": 0,
2821                  "branches": 0,
2822                  "conditions": 0,
2823                  "magnitude": 0.0,
2824                  "value": 0.0,
2825                  "assignments_average": 0.0,
2826                  "branches_average": 0.0,
2827                  "conditions_average": 0.0,
2828                  "assignments_min": 0,
2829                  "assignments_max": 0,
2830                  "branches_min": 0,
2831                  "branches_max": 0,
2832                  "conditions_min": 0,
2833                  "conditions_max": 0
2834                }
2835                "#
2836                );
2837            },
2838        );
2839    }
2840
2841    #[test]
2842    fn csharp_ternary_conditions() {
2843        check_metrics::<CsharpParser>(
2844            "class A {
2845                int Sign(int x) {
2846                    return (x > 0) ? 1 : (x < 0 ? -1 : 0);
2847                }
2848            }",
2849            "foo.cs",
2850            |metric| insta::assert_json_snapshot!(metric.abc),
2851        );
2852    }
2853
2854    #[test]
2855    fn csharp_malformed_parenthesized_no_panic() {
2856        check_metrics::<CsharpParser>("class A { void M() { if (( }) }", "foo.cs", |metric| {
2857            // Don't panic on malformed source.
2858            assert_eq!(metric.abc.assignments(), 0);
2859            assert_eq!(metric.abc.branches(), 0);
2860        });
2861    }
2862
2863    #[test]
2864    fn csharp_function_pointer_type_no_double_count() {
2865        // EC1 extension — `<` and `>` are also parameter-list delimiters
2866        // for unsafe function-pointer types. `FunctionPointerType` must
2867        // be in the LT/GT exclusion list, otherwise these brackets
2868        // accumulate spurious `conditions` counts.
2869        check_metrics::<CsharpParser>(
2870            "unsafe class A {
2871                public delegate*<int, int, int> Adder;
2872                public delegate*<string, void> Logger;
2873            }",
2874            "foo.cs",
2875            |metric| {
2876                assert_eq!(
2877                    metric.abc.conditions(),
2878                    0,
2879                    "function-pointer-type angle brackets must not count"
2880                );
2881            },
2882        );
2883    }
2884
2885    #[test]
2886    fn csharp_generic_type_args_no_double_count() {
2887        // EC1 — `<` and `>` inside TypeArgumentList must not count as
2888        // boolean conditions.
2889        check_metrics::<CsharpParser>(
2890            "class A {
2891                void M(System.Collections.Generic.Dictionary<string, System.Collections.Generic.List<int>> d) {
2892                    System.Console.WriteLine(d);
2893                }
2894            }",
2895            "foo.cs",
2896            |metric| insta::assert_json_snapshot!(metric.abc),
2897        );
2898    }
2899
2900    #[test]
2901    fn csharp_aliased_invocation_expression_branches() {
2902        // Regression for issue #94 (lesson #2): the C# grammar emits three
2903        // aliased `kind_id`s for `invocation_expression`. Code that matches
2904        // only the unsuffixed `Csharp::InvocationExpression` undercounts ABC
2905        // branches whenever the AST emits an aliased variant. The three
2906        // method calls live in `M`, so the per-method maximum (visible at
2907        // the unit-space aggregate as `branches_max`) must be 3.
2908        check_metrics::<CsharpParser>(
2909            "class A {
2910                void M() {
2911                    System.Console.WriteLine(1);
2912                    System.Console.WriteLine(2);
2913                    System.Console.WriteLine(3);
2914                }
2915            }",
2916            "foo.cs",
2917            |metric| {
2918                assert_eq!(metric.abc.branches_max(), 3);
2919                assert_eq!(metric.abc.conditions_max(), 0);
2920            },
2921        );
2922    }
2923
2924    #[test]
2925    fn php_zero_abc() {
2926        check_metrics::<PhpParser>("<?php\n", "foo.php", |metric| {
2927            assert_eq!(metric.abc.assignments_sum(), 0);
2928            assert_eq!(metric.abc.branches_sum(), 0);
2929            assert_eq!(metric.abc.conditions_sum(), 0);
2930            insta::assert_json_snapshot!(metric.abc);
2931        });
2932    }
2933
2934    #[test]
2935    fn php_simple_assignment() {
2936        check_metrics::<PhpParser>(
2937            "<?php
2938function f(): void {
2939    $a = 1;
2940    $b = 2;
2941}",
2942            "foo.php",
2943            |metric| insta::assert_json_snapshot!(metric.abc),
2944        );
2945    }
2946
2947    #[test]
2948    fn php_augmented_assignment() {
2949        check_metrics::<PhpParser>(
2950            "<?php
2951function f(int $x): int {
2952    $a = 0;
2953    $a += $x;
2954    $a -= 1;
2955    $a *= 2;
2956    return $a;
2957}",
2958            "foo.php",
2959            |metric| insta::assert_json_snapshot!(metric.abc),
2960        );
2961    }
2962
2963    #[test]
2964    fn php_const_excluded() {
2965        // Constant declarations and enum cases are NOT counted as
2966        // assignments — they declare immutable values.
2967        check_metrics::<PhpParser>(
2968            "<?php
2969class A {
2970    const PI = 3.14;
2971    const E = 2.71;
2972}
2973enum Color {
2974    case Red;
2975    case Green;
2976}",
2977            "foo.php",
2978            |metric| insta::assert_json_snapshot!(metric.abc),
2979        );
2980    }
2981
2982    #[test]
2983    fn php_function_call() {
2984        check_metrics::<PhpParser>(
2985            "<?php
2986function f(): void {
2987    foo();
2988    bar(1, 2);
2989}",
2990            "foo.php",
2991            |metric| insta::assert_json_snapshot!(metric.abc),
2992        );
2993    }
2994
2995    #[test]
2996    fn php_method_call() {
2997        check_metrics::<PhpParser>(
2998            "<?php
2999function f($obj): void {
3000    $obj->m1();
3001    $obj->m2(1);
3002}",
3003            "foo.php",
3004            |metric| insta::assert_json_snapshot!(metric.abc),
3005        );
3006    }
3007
3008    #[test]
3009    fn php_static_call() {
3010        check_metrics::<PhpParser>(
3011            "<?php
3012function f(): void {
3013    Foo::bar();
3014    Foo::baz(1);
3015}",
3016            "foo.php",
3017            |metric| insta::assert_json_snapshot!(metric.abc),
3018        );
3019    }
3020
3021    #[test]
3022    fn php_nullsafe_call() {
3023        check_metrics::<PhpParser>(
3024            "<?php
3025function f($obj): void {
3026    $obj?->m1();
3027    $obj?->m2(1);
3028}",
3029            "foo.php",
3030            |metric| insta::assert_json_snapshot!(metric.abc),
3031        );
3032    }
3033
3034    #[test]
3035    fn php_object_creation() {
3036        check_metrics::<PhpParser>(
3037            "<?php
3038function f(): void {
3039    new Foo();
3040    new Bar(1);
3041}",
3042            "foo.php",
3043            |metric| insta::assert_json_snapshot!(metric.abc),
3044        );
3045    }
3046
3047    #[test]
3048    fn php_comparison_eq() {
3049        check_metrics::<PhpParser>(
3050            "<?php
3051function f(int $a, int $b): bool {
3052    return $a == $b || $a != $b;
3053}",
3054            "foo.php",
3055            |metric| insta::assert_json_snapshot!(metric.abc),
3056        );
3057    }
3058
3059    #[test]
3060    fn php_comparison_strict() {
3061        check_metrics::<PhpParser>(
3062            "<?php
3063function f(int $a, int $b): bool {
3064    return $a === $b || $a !== $b;
3065}",
3066            "foo.php",
3067            |metric| insta::assert_json_snapshot!(metric.abc),
3068        );
3069    }
3070
3071    #[test]
3072    fn php_spaceship() {
3073        check_metrics::<PhpParser>(
3074            "<?php
3075function f(int $a, int $b): int {
3076    return $a <=> $b;
3077}",
3078            "foo.php",
3079            |metric| insta::assert_json_snapshot!(metric.abc),
3080        );
3081    }
3082
3083    #[test]
3084    fn php_instanceof() {
3085        check_metrics::<PhpParser>(
3086            "<?php
3087function f($x): bool {
3088    return $x instanceof Foo;
3089}",
3090            "foo.php",
3091            |metric| insta::assert_json_snapshot!(metric.abc),
3092        );
3093    }
3094
3095    #[test]
3096    fn php_complex_function() {
3097        // One snippet exercising A, B, C buckets together.
3098        check_metrics::<PhpParser>(
3099            "<?php
3100function f(int $a, int $b): int {
3101    $sum = $a + $b;
3102    $prod = $a * $b;
3103    if ($sum > 0 && $prod === 0) {
3104        return foo($sum);
3105    }
3106    return bar()->double();
3107}",
3108            "foo.php",
3109            |metric| insta::assert_json_snapshot!(metric.abc),
3110        );
3111    }
3112
3113    #[test]
3114    fn php_if_boolean_literal_condition() {
3115        check_metrics::<PhpParser>(
3116            "<?php\n\
3117             function f() {\n\
3118             \x20   if (true) {}                 // +1c\n\
3119             \x20   if (!false) {}               // +1c\n\
3120             \x20   while (true) {}              // +1c\n\
3121             \x20   do {} while (false);         // +1c\n\
3122             }\n",
3123            "foo.php",
3124            |metric| {
3125                assert_eq!(metric.abc.conditions_sum(), 4);
3126                insta::assert_json_snapshot!(metric.abc);
3127            },
3128        );
3129    }
3130
3131    #[test]
3132    fn php_methods_arguments_with_conditions() {
3133        check_metrics::<PhpParser>(
3134            "<?php\n\
3135             function f($a, $b) {\n\
3136             \x20   m($a, $b);                   // +1b\n\
3137             \x20   m(!$a, !$b);                 // +1b +2c\n\
3138             }\n",
3139            "foo.php",
3140            |metric| {
3141                assert_eq!(metric.abc.branches_sum(), 2);
3142                assert_eq!(metric.abc.conditions_sum(), 2);
3143                insta::assert_json_snapshot!(metric.abc);
3144            },
3145        );
3146    }
3147
3148    #[test]
3149    fn php_return_with_conditions() {
3150        check_metrics::<PhpParser>(
3151            "<?php\n\
3152             function m1($z) { return !($z >= 0); }\n\
3153             function m2($x) { return (((!$x))); }\n\
3154             function m3($x, $y) { return $x && $y; }\n",
3155            "foo.php",
3156            |metric| {
3157                // m1: `>=` (1). m2: walker unwraps to $x (1).
3158                // m3: `&&` walker counts both (2). Sum: 4.
3159                assert_eq!(metric.abc.conditions_sum(), 4);
3160                insta::assert_json_snapshot!(metric.abc);
3161            },
3162        );
3163    }
3164
3165    #[test]
3166    fn php_name2_hidden_rule_drift_marker() {
3167        // Drift marker (findings.md round-2 #3): `Php::Name2` maps
3168        // to the hidden grammar rule `_name`. At the pinned
3169        // tree-sitter-php version it is never emitted as a concrete
3170        // node — the visible `Name` (= 1) carries every name.
3171        // We list `Name2` defensively in `php_bool_terminal_kinds!()`
3172        // (lesson 34); if a future grammar bump promotes `_name`
3173        // to a visible rule, this assertion fails loudly.
3174        let src = "<?php\nfunction f($x) { if ($x) { foo($x); } }\n";
3175        let parser = PhpParser::new(
3176            src.as_bytes().to_vec(),
3177            &std::path::PathBuf::from("foo.php"),
3178            None,
3179        );
3180        assert!(!ast_has_kind_id(&parser, Php::Name2 as u16));
3181    }
3182
3183    #[test]
3184    fn php_scoped_property_access_condition_counts() {
3185        // Regression for findings.md round-2 #1 (PHP):
3186        // `if (Config::$enabled) {}` parses with
3187        // `scoped_property_access_expression` as the condition
3188        // node (kind_id 333 at the pinned grammar version — the
3189        // `*2` alias). Pre-fix, neither `ScopedPropertyAccessExpression`
3190        // nor its alias was in `php_bool_terminal_kinds!()`. The
3191        // walker reached the access node, found it non-terminal,
3192        // and broke. Mirrors C#'s `MemberAccessExpression` rule
3193        // (lesson 19, #372).
3194        check_metrics::<PhpParser>(
3195            "<?php\n\
3196             class Config { public static $enabled = true; }\n\
3197             function f() { if (Config::$enabled) { } }\n",
3198            "foo.php",
3199            |metric| {
3200                assert_eq!(metric.abc.conditions_sum(), 1);
3201                insta::assert_json_snapshot!(metric.abc);
3202            },
3203        );
3204    }
3205
3206    #[test]
3207    fn php_named_argument_unary_conditional_counts() {
3208        // Regression for the code-review finding: PHP 8 named-argument
3209        // syntax `m(name: !$a)` parses as `argument(name, ':',
3210        // unary_op_expression)`. Pre-fix, the count walker took
3211        // `argument.child(0)` (the name) and missed the value at the
3212        // last child. Now it picks the last named child as the value.
3213        check_metrics::<PhpParser>(
3214            "<?php\nfunction f($a) { m(name: !$a); }\n",
3215            "foo.php",
3216            |metric| {
3217                // 1 call (branch) + 1 unary-conditional named argument.
3218                assert_eq!(metric.abc.branches_sum(), 1);
3219                assert_eq!(metric.abc.conditions_sum(), 1);
3220                insta::assert_json_snapshot!(metric.abc);
3221            },
3222        );
3223    }
3224
3225    #[test]
3226    fn php_low_precedence_keyword_logical_ops_trigger_walker() {
3227        // Regression: pre-fix, `$a or $b` reported 0 conditions
3228        // because the dispatcher only handled `AMPAMP|PIPEPIPE`,
3229        // skipping the PHP-specific `and` / `or` / `xor` keyword
3230        // forms even though they parse under the same
3231        // `binary_expression` shape.
3232        check_metrics::<PhpParser>(
3233            "<?php\n\
3234             function f($a, $b) {\n\
3235             \x20   return $a or $b;\n\
3236             }\n",
3237            "foo.php",
3238            |metric| {
3239                assert_eq!(metric.abc.conditions_sum(), 2);
3240                insta::assert_json_snapshot!(metric.abc);
3241            },
3242        );
3243    }
3244
3245    #[test]
3246    fn php_if_multiple_conditions() {
3247        check_metrics::<PhpParser>(
3248            "<?php\n\
3249             function f($a, $b, $c, $d) {\n\
3250             \x20   if ($a || $b || $c || $d) {}     // +4c\n\
3251             \x20   if ($a && $b && $c) {}           // +3c\n\
3252             \x20   if (!$a && !$b) {}               // +2c\n\
3253             }\n",
3254            "foo.php",
3255            |metric| {
3256                assert_eq!(metric.abc.conditions_sum(), 9);
3257                insta::assert_json_snapshot!(metric.abc);
3258            },
3259        );
3260    }
3261
3262    #[test]
3263    fn php_while_and_do_while_conditions() {
3264        check_metrics::<PhpParser>(
3265            "<?php\n\
3266             function f($a, $b) {\n\
3267             \x20   while ($a || $b) {}              // +2c\n\
3268             \x20   do {} while ($a && !$b);         // +2c\n\
3269             }\n",
3270            "foo.php",
3271            |metric| {
3272                assert_eq!(metric.abc.conditions_sum(), 4);
3273                insta::assert_json_snapshot!(metric.abc);
3274            },
3275        );
3276    }
3277
3278    #[test]
3279    fn php_short_circuit_with_boolean_literal_operand() {
3280        check_metrics::<PhpParser>(
3281            "<?php\nfunction f($a) { return $a && true; }\n",
3282            "foo.php",
3283            |metric| {
3284                assert_eq!(metric.abc.conditions_sum(), 2);
3285                insta::assert_json_snapshot!(metric.abc);
3286            },
3287        );
3288    }
3289
3290    // --- Kotlin ABC tests -------------------------------------------------
3291
3292    #[test]
3293    fn kotlin_empty_class() {
3294        check_metrics::<KotlinParser>("class C {}", "foo.kt", |metric| {
3295            assert_eq!(metric.abc.assignments_sum(), 0);
3296            assert_eq!(metric.abc.branches_sum(), 0);
3297            assert_eq!(metric.abc.conditions_sum(), 0);
3298            insta::assert_json_snapshot!(metric.abc);
3299        });
3300    }
3301
3302    #[test]
3303    fn kotlin_val_declarations_are_not_assignments() {
3304        // `val` introduces an immutable binding — the `=` initialising it
3305        // is not an assignment in the ABC sense.
3306        check_metrics::<KotlinParser>(
3307            "class C {
3308                val a: Int = 1
3309                val b: Int = 2
3310                val c: Int = 3
3311            }",
3312            "foo.kt",
3313            |metric| {
3314                assert_eq!(metric.abc.assignments_sum(), 0);
3315                assert_eq!(metric.abc.branches_sum(), 0);
3316                insta::assert_json_snapshot!(metric.abc);
3317            },
3318        );
3319    }
3320
3321    #[test]
3322    fn kotlin_var_declarations_count_assignment() {
3323        // `var` initialisers count as assignments (mutable binding).
3324        check_metrics::<KotlinParser>(
3325            "class C {
3326                var a: Int = 1
3327                var b: Int = 2
3328            }",
3329            "foo.kt",
3330            |metric| {
3331                assert_eq!(metric.abc.assignments_sum(), 2);
3332                insta::assert_json_snapshot!(metric.abc);
3333            },
3334        );
3335    }
3336
3337    #[test]
3338    fn kotlin_val_then_assignments_count() {
3339        // Regression for #455: a `val` initialiser must not suppress the
3340        // standalone `=` assignments that follow it. tree-sitter-kotlin
3341        // emits no `SEMI` token (even for explicit semicolons), so the
3342        // pre-#455 `SEMI`-cleared declaration stack never cleared and the
3343        // immutable-`val` sentinel leaked, reporting A=0 here.
3344        check_metrics::<KotlinParser>(
3345            "fun f() {
3346                val cfg = 0
3347                a = 1
3348                b = 2
3349            }",
3350            "foo.kt",
3351            |metric| {
3352                // val initialiser suppressed; `a = 1` and `b = 2` count.
3353                assert_eq!(metric.abc.assignments_sum(), 2);
3354                insta::assert_json_snapshot!(metric.abc);
3355            },
3356        );
3357    }
3358
3359    #[test]
3360    fn kotlin_var_then_assignments_count() {
3361        // Companion to the #455 regression: a `var` declaration leaves a
3362        // mutable-binding sentinel that *permits* the `=` — this path
3363        // accidentally masked the leak (its `Var` sentinel never suppressed
3364        // anything), so it must keep counting both the initialiser and the
3365        // following standalone assignments.
3366        check_metrics::<KotlinParser>(
3367            "fun f() {
3368                var cfg = 0
3369                a = 1
3370                b = 2
3371            }",
3372            "foo.kt",
3373            |metric| {
3374                // var initialiser (+1) plus `a = 1` and `b = 2` (+2).
3375                assert_eq!(metric.abc.assignments_sum(), 3);
3376                insta::assert_json_snapshot!(metric.abc);
3377            },
3378        );
3379    }
3380
3381    #[test]
3382    fn kotlin_augmented_assignments_count() {
3383        // Augmented operators (+=, -=, etc.) and ++/-- always count.
3384        check_metrics::<KotlinParser>(
3385            "fun m() {
3386                var x = 0
3387                x += 1
3388                x -= 2
3389                x *= 3
3390                x++
3391                --x
3392            }",
3393            "foo.kt",
3394            |metric| {
3395                // var declaration (var x = 0): +1
3396                // x += 1, x -= 2, x *= 3, x++, --x: +5
3397                assert_eq!(metric.abc.assignments_sum(), 6);
3398                insta::assert_json_snapshot!(metric.abc);
3399            },
3400        );
3401    }
3402
3403    #[test]
3404    fn kotlin_branches_call_expression() {
3405        check_metrics::<KotlinParser>(
3406            "fun m() {
3407                println(\"a\")
3408                println(\"b\")
3409                println(\"c\")
3410            }",
3411            "foo.kt",
3412            |metric| {
3413                assert_eq!(metric.abc.branches_sum(), 3);
3414                insta::assert_json_snapshot!(metric.abc);
3415            },
3416        );
3417    }
3418
3419    #[test]
3420    fn kotlin_object_construction_branch() {
3421        // Kotlin's object construction is just `Foo()` — a `CallExpression`.
3422        check_metrics::<KotlinParser>(
3423            "class P(val x: Int)
3424            fun m(): P = P(1)",
3425            "foo.kt",
3426            |metric| {
3427                assert_eq!(metric.abc.branches_sum(), 1);
3428                insta::assert_json_snapshot!(metric.abc);
3429            },
3430        );
3431    }
3432
3433    #[test]
3434    fn kotlin_comparisons_count_conditions() {
3435        check_metrics::<KotlinParser>(
3436            "fun m(a: Int, b: Int): Boolean {
3437                val r1 = a < b
3438                val r2 = a > b
3439                val r3 = a <= b
3440                val r4 = a >= b
3441                val r5 = a == b
3442                val r6 = a != b
3443                return r1 || r2 || r3 || r4 || r5 || r6
3444            }",
3445            "foo.kt",
3446            |metric| {
3447                // Six comparison operators in the `val` initialisers
3448                // (<, >, <=, >=, ==, !=) → 6, plus the six bare-identifier
3449                // operands of the `r1 || … || r6` return chain, each a
3450                // Fitzpatrick Rule 9 unary condition (issue #557) → 6.
3451                // Total 12. Before the Kotlin walker was wired the chain
3452                // operands were silently dropped and this read 6.
3453                assert_eq!(metric.abc.conditions_sum(), 12);
3454                insta::assert_json_snapshot!(metric.abc);
3455            },
3456        );
3457    }
3458
3459    #[test]
3460    fn kotlin_identity_equality_conditions() {
3461        // `===` / `!==` are referential equality in Kotlin; they count too.
3462        check_metrics::<KotlinParser>(
3463            "fun m(a: Any, b: Any): Boolean {
3464                return a === b || a !== b
3465            }",
3466            "foo.kt",
3467            |metric| {
3468                assert_eq!(metric.abc.conditions_sum(), 2);
3469                insta::assert_json_snapshot!(metric.abc);
3470            },
3471        );
3472    }
3473
3474    #[test]
3475    fn kotlin_else_branch_counts() {
3476        check_metrics::<KotlinParser>(
3477            "fun m(x: Int): Int {
3478                return if (x > 0) 1 else -1
3479            }",
3480            "foo.kt",
3481            |metric| {
3482                // condition: > (1) + else (1) = 2
3483                assert_eq!(metric.abc.conditions_sum(), 2);
3484                insta::assert_json_snapshot!(metric.abc);
3485            },
3486        );
3487    }
3488
3489    #[test]
3490    fn kotlin_when_entries_count() {
3491        check_metrics::<KotlinParser>(
3492            "fun m(x: Int): Int {
3493                return when (x) {
3494                    1 -> 10
3495                    2 -> 20
3496                    else -> 0
3497                }
3498            }",
3499            "foo.kt",
3500            |metric| {
3501                // Non-`else` WhenEntry arms count; the `else ->` fallback
3502                // arm does not (issue #456). Two case arms + zero for the
3503                // `else` arm = 2.
3504                assert_eq!(metric.abc.conditions_sum(), 2);
3505                insta::assert_json_snapshot!(metric.abc);
3506            },
3507        );
3508    }
3509
3510    // Pins the `else ->` exclusion directly: a `when` whose only fallback
3511    // is `else ->` must not count that arm. Revert-verified — gating the
3512    // `WhenEntry` arm on `!kotlin_when_entry_is_else` is what drops this
3513    // from 3 to 2 (issue #456, lesson 11). Mirrors the cyclomatic gate.
3514    #[test]
3515    fn kotlin_when_else_not_a_condition() {
3516        check_metrics::<KotlinParser>(
3517            "fun m(x: Int): Int {
3518                return when (x) { 1 -> 10; 2 -> 20; else -> 0 }
3519            }",
3520            "foo.kt",
3521            |metric| {
3522                // case `1 ->` (+1) + case `2 ->` (+1) + `else ->` (+0) = 2.
3523                assert_eq!(metric.abc.conditions_sum(), 2);
3524            },
3525        );
3526    }
3527
3528    #[test]
3529    fn kotlin_catch_block_counts() {
3530        check_metrics::<KotlinParser>(
3531            "fun m() {
3532                try {
3533                    println(\"ok\")
3534                } catch (e: Exception) {
3535                    println(\"err\")
3536                }
3537            }",
3538            "foo.kt",
3539            |metric| {
3540                // `try` (+1) and `catch` (+1) each contribute one condition,
3541                // matching Java / C# / C++ / Groovy (Fitzpatrick counts both
3542                // keywords). Before #696 Kotlin counted only the catch block.
3543                assert_eq!(metric.abc.conditions_sum(), 2);
3544                insta::assert_json_snapshot!(metric.abc);
3545            },
3546        );
3547    }
3548
3549    #[test]
3550    fn kotlin_elvis_and_safe_cast() {
3551        // `?:` (elvis) and `as?` (safe cast) are condition-like.
3552        check_metrics::<KotlinParser>(
3553            "fun m(s: String?): Int {
3554                val n = (s as? Int) ?: 0
3555                return n
3556            }",
3557            "foo.kt",
3558            |metric| {
3559                // as? (+1) + ?: (+1) = 2 conditions.
3560                assert_eq!(metric.abc.conditions_sum(), 2);
3561                insta::assert_json_snapshot!(metric.abc);
3562            },
3563        );
3564    }
3565
3566    #[test]
3567    fn kotlin_generic_brackets_not_conditions() {
3568        // `<` / `>` used as type-parameter brackets must not be counted.
3569        check_metrics::<KotlinParser>(
3570            "class Box<T>(val v: T)
3571            fun <T> wrap(x: T): Box<T> = Box(x)",
3572            "foo.kt",
3573            |metric| {
3574                // No comparisons — only generic brackets.
3575                assert_eq!(metric.abc.conditions_sum(), 0);
3576                insta::assert_json_snapshot!(metric.abc);
3577            },
3578        );
3579    }
3580
3581    #[test]
3582    fn kotlin_class_with_methods_and_branches() {
3583        check_metrics::<KotlinParser>(
3584            "class C {
3585                var counter: Int = 0
3586                fun bump() {
3587                    counter += 1
3588                    println(counter)
3589                }
3590            }",
3591            "foo.kt",
3592            |metric| {
3593                // assignments: var counter = 0 (+1), counter += 1 (+1) = 2
3594                // branches: println(counter) = 1
3595                assert_eq!(metric.abc.assignments_sum(), 2);
3596                assert_eq!(metric.abc.branches_sum(), 1);
3597                assert_eq!(metric.abc.conditions_sum(), 0);
3598                insta::assert_json_snapshot!(metric.abc);
3599            },
3600        );
3601    }
3602
3603    #[test]
3604    fn kotlin_object_singleton_abc() {
3605        check_metrics::<KotlinParser>(
3606            "object Util {
3607                fun work(x: Int): Int {
3608                    var y = x
3609                    y += 1
3610                    if (y > 0) {
3611                        return y
3612                    }
3613                    return -1
3614                }
3615            }",
3616            "foo.kt",
3617            |metric| {
3618                // assignments: var y = x (+1), y += 1 (+1) = 2
3619                // branches: 0 (return is not a call)
3620                // conditions: y > 0 (+1) = 1
3621                assert_eq!(metric.abc.assignments_sum(), 2);
3622                assert_eq!(metric.abc.branches_sum(), 0);
3623                assert_eq!(metric.abc.conditions_sum(), 1);
3624                insta::assert_json_snapshot!(metric.abc);
3625            },
3626        );
3627    }
3628
3629    #[test]
3630    fn kotlin_interface_abc() {
3631        // Pure-abstract interface with no bodies — all-zero.
3632        check_metrics::<KotlinParser>(
3633            "interface I {
3634                fun work(): Int
3635                fun describe(): String
3636            }",
3637            "foo.kt",
3638            |metric| {
3639                assert_eq!(metric.abc.assignments_sum(), 0);
3640                assert_eq!(metric.abc.branches_sum(), 0);
3641                assert_eq!(metric.abc.conditions_sum(), 0);
3642                insta::assert_json_snapshot!(metric.abc);
3643            },
3644        );
3645    }
3646
3647    #[test]
3648    fn kotlin_nested_class_abc() {
3649        check_metrics::<KotlinParser>(
3650            "class Outer {
3651                var o: Int = 0
3652                class Nested {
3653                    var n: Int = 0
3654                    fun bump() { n += 1 }
3655                }
3656            }",
3657            "foo.kt",
3658            |metric| {
3659                // Outer: var o = 0 (+1)
3660                // Nested: var n = 0 (+1), n += 1 (+1) = 2
3661                // total assignments = 3
3662                assert_eq!(metric.abc.assignments_sum(), 3);
3663                insta::assert_json_snapshot!(metric.abc);
3664            },
3665        );
3666    }
3667
3668    #[test]
3669    fn kotlin_data_class_abc() {
3670        // `data class` with primary-constructor `val`s — no assignments
3671        // (vals don't count) and no body conditions.
3672        check_metrics::<KotlinParser>(
3673            "data class Point(val x: Int, val y: Int)",
3674            "foo.kt",
3675            |metric| {
3676                assert_eq!(metric.abc.assignments_sum(), 0);
3677                assert_eq!(metric.abc.branches_sum(), 0);
3678                assert_eq!(metric.abc.conditions_sum(), 0);
3679                insta::assert_json_snapshot!(metric.abc);
3680            },
3681        );
3682    }
3683
3684    #[test]
3685    fn kotlin_primary_constructor_default_value_not_assignment() {
3686        // Regression: default values on primary-constructor `val`
3687        // parameters are initialisers, not assignments. Without
3688        // `ClassParameter` pushing a declaration sentinel, the `=` token
3689        // here would be counted unconditionally as a standalone
3690        // assignment.
3691        check_metrics::<KotlinParser>("class C(val a: Int = 5)", "foo.kt", |metric| {
3692            // `val a = 5` → suppressed (Const sentinel).
3693            assert_eq!(metric.abc.assignments_sum(), 0);
3694            insta::assert_json_snapshot!(metric.abc);
3695        });
3696    }
3697
3698    #[test]
3699    fn kotlin_unary_conditions_in_chain() {
3700        // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
3701        // `&&` / `||` chain is one condition. `a && b || c` → a, b, c each
3702        // contribute one; the `&&` / `||` operators contribute nothing.
3703        // expected: 3 unary conditions, no comparisons, no `if`-keyword
3704        // condition in Kotlin (matches the Java byte-equivalent of 3).
3705        check_metrics::<KotlinParser>(
3706            "fun f(a: Boolean, b: Boolean, c: Boolean) {
3707                if (a && b || c) { println(\"x\") }
3708            }",
3709            "foo.kt",
3710            |metric| {
3711                assert_eq!(metric.abc.conditions_sum(), 3);
3712            },
3713        );
3714    }
3715
3716    #[test]
3717    fn kotlin_comparison_operands_add_nothing() {
3718        // Isolation check: comparison operands of a `&&` chain are nested
3719        // `binary_expression` nodes, not bare boolean leaves, so the
3720        // walker adds nothing — only the two `>` comparisons count.
3721        // expected: 2 (the two `>` tokens), walker contributes 0.
3722        check_metrics::<KotlinParser>(
3723            "fun g(x: Int, y: Int) {
3724                if (x > 0 && y > 0) { println(\"x\") }
3725            }",
3726            "foo.kt",
3727            |metric| {
3728                assert_eq!(metric.abc.conditions_sum(), 2);
3729            },
3730        );
3731    }
3732
3733    #[test]
3734    fn kotlin_negated_operand_is_unary_condition() {
3735        // A `!`-negated operand is still a unary condition: `a && !b`
3736        // unwraps the `unary_expression` to reach the inner identifier.
3737        // expected: 2 (`a` and the `!b` operand).
3738        check_metrics::<KotlinParser>(
3739            "fun f(a: Boolean, b: Boolean) {
3740                if (a && !b) { println(\"x\") }
3741            }",
3742            "foo.kt",
3743            |metric| {
3744                assert_eq!(metric.abc.conditions_sum(), 2);
3745            },
3746        );
3747    }
3748
3749    #[test]
3750    fn kotlin_bare_if_predicate_is_one_condition() {
3751        // Issue #773: a bare-boolean `if` predicate (`if (flag)`) is one
3752        // Fitzpatrick unary condition. Before the Phase-2B arm it counted
3753        // 0, so `if (flag) 1 else -1` scored 1 (only the `else`) instead of
3754        // 2, dropping below Kotlin's own cyclomatic decision count.
3755        // expected: predicate (1) + else (1) = 2.
3756        check_metrics::<KotlinParser>(
3757            "fun m(flag: Boolean): Int { return if (flag) 1 else -1 }",
3758            "foo.kt",
3759            |metric| {
3760                assert_eq!(metric.abc.conditions_sum(), 2);
3761            },
3762        );
3763    }
3764
3765    #[test]
3766    fn kotlin_bare_while_predicate_is_one_condition() {
3767        // Issue #773: the bare predicate of a `while` loop counts one
3768        // condition via the `condition` field. expected: 1.
3769        check_metrics::<KotlinParser>(
3770            "fun m(running: Boolean) { while (running) { println(\"x\") } }",
3771            "foo.kt",
3772            |metric| {
3773                assert_eq!(metric.abc.conditions_sum(), 1);
3774            },
3775        );
3776    }
3777
3778    #[test]
3779    fn kotlin_bare_do_while_predicate_is_one_condition() {
3780        // Issue #773: the bare predicate of a `do`/`while` loop counts one
3781        // condition. expected: 1.
3782        check_metrics::<KotlinParser>(
3783            "fun m(ok: Boolean) { do { println(\"x\") } while (ok) }",
3784            "foo.kt",
3785            |metric| {
3786                assert_eq!(metric.abc.conditions_sum(), 1);
3787            },
3788        );
3789    }
3790
3791    #[test]
3792    fn kotlin_comparison_predicate_not_double_counted() {
3793        // Double-count guard (#773): a comparison predicate (`if (a == b)`)
3794        // is a nested `binary_expression` already counted by the `==` token
3795        // arm, so the Phase-2B condition-slot arm must add nothing here.
3796        // expected: `==` (1) + else (1) = 2 — unchanged by the new arm.
3797        check_metrics::<KotlinParser>(
3798            "fun m(a: Int, b: Int): Int { return if (a == b) 1 else -1 }",
3799            "foo.kt",
3800            |metric| {
3801                assert_eq!(metric.abc.conditions_sum(), 2);
3802            },
3803        );
3804    }
3805
3806    #[test]
3807    fn kotlin_short_circuit_predicate_not_double_counted() {
3808        // Double-count guard (#773): an `&&`/`||` predicate is counted by
3809        // the Rule 9 chain walker (each operand once); the Phase-2B arm
3810        // must add nothing for it. expected: `x` (1) + `y` (1) + else (1)
3811        // = 3 — unchanged by the new arm.
3812        check_metrics::<KotlinParser>(
3813            "fun m(x: Boolean, y: Boolean): Int { return if (x && y) 1 else -1 }",
3814            "foo.kt",
3815            |metric| {
3816                assert_eq!(metric.abc.conditions_sum(), 3);
3817            },
3818        );
3819    }
3820
3821    #[test]
3822    fn kotlin_parenthesised_bare_predicate_is_one_condition() {
3823        // A parenthesised bare predicate (`if ((flag))`) is unwrapped by
3824        // `kotlin_inspect_container` and still counts one condition (#773).
3825        // expected: 1.
3826        check_metrics::<KotlinParser>(
3827            "fun m(flag: Boolean) { if ((flag)) { println(\"x\") } }",
3828            "foo.kt",
3829            |metric| {
3830                assert_eq!(metric.abc.conditions_sum(), 1);
3831            },
3832        );
3833    }
3834
3835    // --- TypeScript / TSX ABC tests --------------------------------------
3836    //
3837    // Assignment, branch, condition counting per Fitzpatrick:
3838    // - Augmented assignment / `++` / `--` always count.
3839    // - Plain `=` counts unless inside `const` declaration.
3840    // - `call_expression` / `new_expression` count as branches.
3841    // - Comparison / equality operators, ternary `?`, `??`, control-flow
3842    //   arms (`else`, `case`, `default`, `catch`, `try`, `instanceof`),
3843    //   and `<`/`>` (outside `type_arguments` / `type_parameters`) count
3844    //   as conditions.
3845
3846    #[test]
3847    fn typescript_assignments_basic() {
3848        check_metrics::<TypescriptParser>(
3849            "class C {
3850                m(): void {
3851                    let x = 0;          // const-sentinel suppressed since `let`, but x is Var → +1
3852                    x = 1;              // +1
3853                    x += 2;             // +1
3854                    x++;                // +1
3855                }
3856            }",
3857            "foo.ts",
3858            |metric| {
3859                assert_eq!(metric.abc.assignments_sum(), 4);
3860                insta::assert_json_snapshot!(metric.abc);
3861            },
3862        );
3863    }
3864
3865    #[test]
3866    fn typescript_const_excluded_from_assignments() {
3867        check_metrics::<TypescriptParser>(
3868            "class C {
3869                m(): void {
3870                    const a = 1;        // suppressed (Const sentinel)
3871                    const b = 2;        // suppressed
3872                    let c = 3;          // +1 (Var sentinel)
3873                }
3874            }",
3875            "foo.ts",
3876            |metric| {
3877                assert_eq!(metric.abc.assignments_sum(), 1);
3878                insta::assert_json_snapshot!(metric.abc);
3879            },
3880        );
3881    }
3882
3883    #[test]
3884    fn typescript_branches_function_calls() {
3885        check_metrics::<TypescriptParser>(
3886            "class C {
3887                m(): void {
3888                    foo();              // +1
3889                    bar(1, 2);          // +1
3890                    new Date();         // +1
3891                }
3892            }",
3893            "foo.ts",
3894            |metric| {
3895                assert_eq!(metric.abc.branches_sum(), 3);
3896                insta::assert_json_snapshot!(metric.abc);
3897            },
3898        );
3899    }
3900
3901    #[test]
3902    fn typescript_conditions_comparison_operators() {
3903        check_metrics::<TypescriptParser>(
3904            "class C {
3905                m(x: number, y: number): boolean {
3906                    return x == y       // +1
3907                        || x === y      // +1
3908                        || x != y       // +1
3909                        || x !== y      // +1
3910                        || x < y        // +1
3911                        || x <= y       // +1
3912                        || x > y        // +1
3913                        || x >= y;      // +1
3914                }
3915            }",
3916            "foo.ts",
3917            |metric| {
3918                assert_eq!(metric.abc.conditions_sum(), 8);
3919                insta::assert_json_snapshot!(metric.abc);
3920            },
3921        );
3922    }
3923
3924    #[test]
3925    fn typescript_conditions_control_flow_arms() {
3926        check_metrics::<TypescriptParser>(
3927            "class C {
3928                m(x: number): number {
3929                    try {                       // +1 (try)
3930                        if (x > 0) {            // +1 (>)
3931                            return 1;
3932                        } else {                // +1 (else)
3933                            return -1;
3934                        }
3935                    } catch (e) {               // +1 (catch)
3936                        return 0;
3937                    }
3938                }
3939            }",
3940            "foo.ts",
3941            |metric| {
3942                assert_eq!(metric.abc.conditions_sum(), 4);
3943                insta::assert_json_snapshot!(metric.abc);
3944            },
3945        );
3946    }
3947
3948    #[test]
3949    fn typescript_conditions_switch_case() {
3950        check_metrics::<TypescriptParser>(
3951            "class C {
3952                m(x: number): number {
3953                    switch (x) {
3954                        case 1:                 // +1
3955                            return 1;
3956                        case 2:                 // +1
3957                            return 2;
3958                        default:                // +0 (fallthrough, #469)
3959                            return 0;
3960                    }
3961                }
3962            }",
3963            "foo.ts",
3964            |metric| {
3965                assert_eq!(metric.abc.conditions_sum(), 2);
3966                insta::assert_json_snapshot!(metric.abc);
3967            },
3968        );
3969    }
3970
3971    #[test]
3972    fn typescript_ternary_and_nullish() {
3973        check_metrics::<TypescriptParser>(
3974            "class C {
3975                m(x: number | null): number {
3976                    return x !== null           // +1 (!==)
3977                        ? x                     // +1 (ternary ?)
3978                        : 0;
3979                }
3980                n(x: number | null): number {
3981                    return x ?? 0;              // +1 (??)
3982                }
3983            }",
3984            "foo.ts",
3985            |metric| {
3986                assert_eq!(metric.abc.conditions_sum(), 3);
3987                insta::assert_json_snapshot!(metric.abc);
3988            },
3989        );
3990    }
3991
3992    #[test]
3993    fn typescript_instanceof_counts_as_condition() {
3994        check_metrics::<TypescriptParser>(
3995            "class C {
3996                m(o: unknown): boolean {
3997                    return o instanceof C;      // +1
3998                }
3999            }",
4000            "foo.ts",
4001            |metric| {
4002                assert_eq!(metric.abc.conditions_sum(), 1);
4003                insta::assert_json_snapshot!(metric.abc);
4004            },
4005        );
4006    }
4007
4008    #[test]
4009    fn typescript_generic_lt_gt_not_a_condition() {
4010        // `<T>` in `class C<T>` and `Array<number>` should not contribute
4011        // to conditions even though the tokens are `<` and `>`.
4012        check_metrics::<TypescriptParser>(
4013            "class C<T> {
4014                xs: Array<number> = [];
4015                m(): void {
4016                    const arr: Array<string> = [];   // suppressed const
4017                    void arr;
4018                }
4019            }",
4020            "foo.ts",
4021            |metric| {
4022                assert_eq!(metric.abc.conditions_sum(), 0);
4023                insta::assert_json_snapshot!(metric.abc);
4024            },
4025        );
4026    }
4027
4028    #[test]
4029    fn typescript_abstract_class_abc() {
4030        // Abstract methods have no body — they contribute nothing.
4031        check_metrics::<TypescriptParser>(
4032            "abstract class C {
4033                abstract a(): void;
4034                m(x: number): number {
4035                    if (x > 0) return 1;        // +1 condition
4036                    return 0;
4037                }
4038            }",
4039            "foo.ts",
4040            |metric| {
4041                assert_eq!(metric.abc.conditions_sum(), 1);
4042                assert_eq!(metric.abc.branches_sum(), 0);
4043                insta::assert_json_snapshot!(metric.abc);
4044            },
4045        );
4046    }
4047
4048    #[test]
4049    fn typescript_interface_abc_zero() {
4050        check_metrics::<TypescriptParser>(
4051            "interface I {
4052                a(): void;
4053                b(): number;
4054                p: string;
4055            }",
4056            "foo.ts",
4057            |metric| {
4058                assert_eq!(metric.abc.assignments_sum(), 0);
4059                assert_eq!(metric.abc.branches_sum(), 0);
4060                assert_eq!(metric.abc.conditions_sum(), 0);
4061                insta::assert_json_snapshot!(metric.abc);
4062            },
4063        );
4064    }
4065
4066    #[test]
4067    fn typescript_arrow_field_contributes_abc() {
4068        // Arrow function class members are function spaces; their
4069        // assignments/branches/conditions are counted.
4070        check_metrics::<TypescriptParser>(
4071            "class C {
4072                arrow = (x: number) => {
4073                    if (x > 0) {                // +1 condition
4074                        return foo();           // +1 branch
4075                    }
4076                    return 0;
4077                };
4078            }",
4079            "foo.ts",
4080            |metric| {
4081                assert_eq!(metric.abc.conditions_sum(), 1);
4082                assert_eq!(metric.abc.branches_sum(), 1);
4083                insta::assert_json_snapshot!(metric.abc);
4084            },
4085        );
4086    }
4087
4088    #[test]
4089    fn typescript_parameter_property_init_not_assignment() {
4090        // Parameter properties don't introduce a `=` token themselves;
4091        // only the explicit `let z = 0` body assignment is counted.
4092        // The class field initializer `f: number = 0` likewise has a `=`
4093        // that DOES count (matches `typescript_assignments_basic`).
4094        check_metrics::<TypescriptParser>(
4095            "class C {
4096                f: number = 0;
4097                constructor(public x: number, private y: string) {
4098                    let z = 0;
4099                }
4100            }",
4101            "foo.ts",
4102            |metric| {
4103                // f's initializer + `let z = 0` = 2 assignments; the
4104                // parameter properties contribute zero.
4105                assert_eq!(metric.abc.assignments_sum(), 2);
4106                insta::assert_json_snapshot!(metric.abc);
4107            },
4108        );
4109    }
4110
4111    // TSX parity
4112
4113    #[test]
4114    fn tsx_assignments_basic() {
4115        check_metrics::<TsxParser>(
4116            "class C {
4117                m(): void {
4118                    let x = 0;
4119                    x = 1;
4120                    x += 2;
4121                    x++;
4122                }
4123            }",
4124            "foo.tsx",
4125            |metric| {
4126                assert_eq!(metric.abc.assignments_sum(), 4);
4127                insta::assert_json_snapshot!(metric.abc);
4128            },
4129        );
4130    }
4131
4132    #[test]
4133    fn tsx_const_excluded_from_assignments() {
4134        check_metrics::<TsxParser>(
4135            "class C {
4136                m(): void {
4137                    const a = 1;
4138                    let b = 2;
4139                }
4140            }",
4141            "foo.tsx",
4142            |metric| {
4143                assert_eq!(metric.abc.assignments_sum(), 1);
4144                insta::assert_json_snapshot!(metric.abc);
4145            },
4146        );
4147    }
4148
4149    #[test]
4150    fn tsx_branches_function_calls() {
4151        check_metrics::<TsxParser>(
4152            "class C {
4153                m(): void {
4154                    foo();
4155                    new Date();
4156                }
4157            }",
4158            "foo.tsx",
4159            |metric| {
4160                assert_eq!(metric.abc.branches_sum(), 2);
4161                insta::assert_json_snapshot!(metric.abc);
4162            },
4163        );
4164    }
4165
4166    #[test]
4167    fn tsx_conditions_comparison_operators() {
4168        check_metrics::<TsxParser>(
4169            "class C {
4170                m(x: number, y: number): boolean {
4171                    return x == y || x < y || x >= y;
4172                }
4173            }",
4174            "foo.tsx",
4175            |metric| {
4176                assert_eq!(metric.abc.conditions_sum(), 3);
4177                insta::assert_json_snapshot!(metric.abc);
4178            },
4179        );
4180    }
4181
4182    #[test]
4183    fn tsx_conditions_control_flow_arms() {
4184        check_metrics::<TsxParser>(
4185            "class C {
4186                m(x: number): number {
4187                    try {
4188                        if (x > 0) return 1;
4189                        else return -1;
4190                    } catch (e) {
4191                        return 0;
4192                    }
4193                }
4194            }",
4195            "foo.tsx",
4196            |metric| {
4197                assert_eq!(metric.abc.conditions_sum(), 4);
4198                insta::assert_json_snapshot!(metric.abc);
4199            },
4200        );
4201    }
4202
4203    #[test]
4204    fn tsx_conditions_switch_case() {
4205        check_metrics::<TsxParser>(
4206            "class C {
4207                m(x: number): number {
4208                    switch (x) {
4209                        case 1: return 1;       // +1
4210                        case 2: return 2;       // +1
4211                        default: return 0;      // +0 (fallthrough, #469)
4212                    }
4213                }
4214            }",
4215            "foo.tsx",
4216            |metric| {
4217                assert_eq!(metric.abc.conditions_sum(), 2);
4218                insta::assert_json_snapshot!(metric.abc);
4219            },
4220        );
4221    }
4222
4223    #[test]
4224    fn tsx_ternary_and_nullish() {
4225        check_metrics::<TsxParser>(
4226            "class C {
4227                m(x: number | null): number {
4228                    return x !== null ? x : 0;
4229                }
4230                n(x: number | null): number { return x ?? 0; }
4231            }",
4232            "foo.tsx",
4233            |metric| {
4234                assert_eq!(metric.abc.conditions_sum(), 3);
4235                insta::assert_json_snapshot!(metric.abc);
4236            },
4237        );
4238    }
4239
4240    #[test]
4241    fn tsx_instanceof_counts_as_condition() {
4242        check_metrics::<TsxParser>(
4243            "class C { m(o: unknown): boolean { return o instanceof C; } }",
4244            "foo.tsx",
4245            |metric| {
4246                assert_eq!(metric.abc.conditions_sum(), 1);
4247                insta::assert_json_snapshot!(metric.abc);
4248            },
4249        );
4250    }
4251
4252    #[test]
4253    fn tsx_generic_lt_gt_not_a_condition() {
4254        check_metrics::<TsxParser>(
4255            "class C<T> { xs: Array<number> = []; }",
4256            "foo.tsx",
4257            |metric| {
4258                assert_eq!(metric.abc.conditions_sum(), 0);
4259                insta::assert_json_snapshot!(metric.abc);
4260            },
4261        );
4262    }
4263
4264    #[test]
4265    fn tsx_abstract_class_abc() {
4266        check_metrics::<TsxParser>(
4267            "abstract class C {
4268                abstract a(): void;
4269                m(x: number): number {
4270                    if (x > 0) return 1;
4271                    return 0;
4272                }
4273            }",
4274            "foo.tsx",
4275            |metric| {
4276                assert_eq!(metric.abc.conditions_sum(), 1);
4277                assert_eq!(metric.abc.branches_sum(), 0);
4278                insta::assert_json_snapshot!(metric.abc);
4279            },
4280        );
4281    }
4282
4283    #[test]
4284    fn tsx_interface_abc_zero() {
4285        check_metrics::<TsxParser>(
4286            "interface I { a(): void; p: string; }",
4287            "foo.tsx",
4288            |metric| {
4289                assert_eq!(metric.abc.assignments_sum(), 0);
4290                assert_eq!(metric.abc.branches_sum(), 0);
4291                assert_eq!(metric.abc.conditions_sum(), 0);
4292                insta::assert_json_snapshot!(metric.abc);
4293            },
4294        );
4295    }
4296
4297    #[test]
4298    fn tsx_arrow_field_contributes_abc() {
4299        check_metrics::<TsxParser>(
4300            "class C {
4301                arrow = (x: number) => {
4302                    if (x > 0) return foo();
4303                    return 0;
4304                };
4305            }",
4306            "foo.tsx",
4307            |metric| {
4308                assert_eq!(metric.abc.conditions_sum(), 1);
4309                assert_eq!(metric.abc.branches_sum(), 1);
4310                insta::assert_json_snapshot!(metric.abc);
4311            },
4312        );
4313    }
4314
4315    #[test]
4316    fn tsx_parameter_property_init_not_assignment() {
4317        // Parameter properties contribute no `=`; the body's `let z = 0`
4318        // and the field initializer do.
4319        check_metrics::<TsxParser>(
4320            "class C {
4321                f: number = 0;
4322                constructor(public x: number) { let z = 0; }
4323            }",
4324            "foo.tsx",
4325            |metric| {
4326                assert_eq!(metric.abc.assignments_sum(), 2);
4327                insta::assert_json_snapshot!(metric.abc);
4328            },
4329        );
4330    }
4331
4332    // --- Ruby ABC tests ---------------------------------------------------
4333    //
4334    // Each Ruby `assignment` / `operator_assignment` is one assignment
4335    // regardless of whether the LHS is a local, instance, or class
4336    // variable. Every `call` / `super` / `yield` is one branch. Every
4337    // comparison-operator token inside a `binary` node plus each
4338    // `else` / `elsif` / `when` / `then` / `?` / `rescue` clause is
4339    // one condition.
4340
4341    #[test]
4342    fn ruby_zero_abc() {
4343        check_metrics::<RubyParser>("\n", "foo.rb", |metric| {
4344            assert_eq!(metric.abc.assignments_sum(), 0);
4345            assert_eq!(metric.abc.branches_sum(), 0);
4346            assert_eq!(metric.abc.conditions_sum(), 0);
4347            insta::assert_json_snapshot!(metric.abc);
4348        });
4349    }
4350
4351    #[test]
4352    fn ruby_simple_assignment() {
4353        check_metrics::<RubyParser>("def f\n  a = 1\n  b = 2\nend\n", "foo.rb", |metric| {
4354            assert_eq!(metric.abc.assignments_sum(), 2);
4355            assert_eq!(metric.abc.branches_sum(), 0);
4356            assert_eq!(metric.abc.conditions_sum(), 0);
4357            insta::assert_json_snapshot!(metric.abc);
4358        });
4359    }
4360
4361    #[test]
4362    fn ruby_augmented_assignment() {
4363        // `+=`, `-=`, `*=` are `operator_assignment` nodes — each is
4364        // one assignment. Plain `=` to set the initial value adds one
4365        // more.
4366        check_metrics::<RubyParser>(
4367            "def f(x)\n  a = 0\n  a += x\n  a -= 1\n  a *= 2\nend\n",
4368            "foo.rb",
4369            |metric| {
4370                assert_eq!(metric.abc.assignments_sum(), 4);
4371                insta::assert_json_snapshot!(metric.abc);
4372            },
4373        );
4374    }
4375
4376    #[test]
4377    fn ruby_logical_augmented_assignment() {
4378        // `||=` and `&&=` are also `operator_assignment` nodes.
4379        check_metrics::<RubyParser>("def f\n  @x ||= 0\n  @x &&= 1\nend\n", "foo.rb", |metric| {
4380            assert_eq!(metric.abc.assignments_sum(), 2);
4381            insta::assert_json_snapshot!(metric.abc);
4382        });
4383    }
4384
4385    #[test]
4386    fn ruby_method_call_branch() {
4387        // Each method invocation is one branch.
4388        check_metrics::<RubyParser>(
4389            "def f(obj)\n  foo()\n  obj.bar(1)\nend\n",
4390            "foo.rb",
4391            |metric| {
4392                assert_eq!(metric.abc.branches_sum(), 2);
4393                insta::assert_json_snapshot!(metric.abc);
4394            },
4395        );
4396    }
4397
4398    #[test]
4399    fn ruby_super_and_yield_branches() {
4400        // `super` and `yield` both count as branches (control-pass).
4401        check_metrics::<RubyParser>("def f\n  super\n  yield\nend\n", "foo.rb", |metric| {
4402            assert_eq!(metric.abc.branches_sum(), 2);
4403            assert_eq!(metric.abc.assignments_sum(), 0);
4404            insta::assert_json_snapshot!(metric.abc);
4405        });
4406    }
4407
4408    #[test]
4409    fn ruby_attr_macro_is_branch() {
4410        // `attr_accessor` is a `Call3` node and registers as a branch
4411        // like any method invocation.
4412        check_metrics::<RubyParser>("class A\n  attr_accessor :x\nend\n", "foo.rb", |metric| {
4413            assert_eq!(metric.abc.branches_sum(), 1);
4414            insta::assert_json_snapshot!(metric.abc);
4415        });
4416    }
4417
4418    #[test]
4419    fn ruby_comparison_conditions() {
4420        // Each comparison operator is one condition.
4421        check_metrics::<RubyParser>(
4422            "def f(a, b)\n  a == b\n  a != b\n  a < b\n  a > b\n  a <= b\n  a >= b\nend\n",
4423            "foo.rb",
4424            |metric| {
4425                assert_eq!(metric.abc.conditions_sum(), 6);
4426                insta::assert_json_snapshot!(metric.abc);
4427            },
4428        );
4429    }
4430
4431    #[test]
4432    fn ruby_case_match_in_arms_are_conditions() {
4433        // Regression for #977: each non-wildcard `case … in` arm is one
4434        // ABC condition, matching Python's `case_clause` handling. Using
4435        // literal patterns (no comparison operators) isolates the
4436        // `in_clause` contribution from any operand tokens.
4437        // expected: 2 conditions — one per `in 1` / `in 2` arm.
4438        check_metrics::<RubyParser>(
4439            "def f(x)\n  case x\n  in 1 then :one\n  in 2 then :two\n  end\nend\n",
4440            "foo.rb",
4441            |metric| {
4442                assert_eq!(metric.abc.conditions_sum(), 2);
4443            },
4444        );
4445    }
4446
4447    #[test]
4448    fn ruby_case_match_guarded_wildcard_is_a_condition() {
4449        // Regression for #977: a guarded wildcard arm `in _ if x` is not a
4450        // bare default and counts as one ABC condition, while the trailing
4451        // bare `in _` adds none. The guard predicate here is a bare
4452        // identifier (no comparison operator), so the single counted
4453        // condition is the guarded `in_clause` itself.
4454        // expected: 1 condition — the guarded `in _ if x` arm only.
4455        check_metrics::<RubyParser>(
4456            "def f(x)\n  case x\n  in _ if x then :y\n  in _ then :default\n  end\nend\n",
4457            "foo.rb",
4458            |metric| {
4459                assert_eq!(metric.abc.conditions_sum(), 1);
4460            },
4461        );
4462    }
4463
4464    #[test]
4465    fn ruby_case_match_bare_wildcard_is_not_a_condition() {
4466        // Regression for #977: a `case … in` whose only arm is the bare
4467        // wildcard `in _` (no guard) is the default arm and contributes no
4468        // ABC condition, keeping ABC and cyclomatic in lockstep on the
4469        // same construct.
4470        // expected: 0 conditions.
4471        check_metrics::<RubyParser>(
4472            "def f(x)\n  case x\n  in _ then :default\n  end\nend\n",
4473            "foo.rb",
4474            |metric| {
4475                assert_eq!(metric.abc.conditions_sum(), 0);
4476            },
4477        );
4478    }
4479
4480    #[test]
4481    fn ruby_bare_predicate_control_flow_counts_one_condition() {
4482        // Regression for #696: idiomatic Ruby bare predicates
4483        // (`if flag` / `while flag` / `unless flag` / `until flag`) each
4484        // count one unary condition, matching Rust / C# / PHP / Python. The
4485        // condition field is read for both block and modifier forms.
4486        //
4487        // expected: 8 conditions — four block forms (`if`/`unless`/`while`/
4488        // `until`) plus the same four as modifiers, one each.
4489        check_metrics::<RubyParser>(
4490            "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",
4491            "foo.rb",
4492            |metric| {
4493                assert_eq!(metric.abc.conditions_sum(), 8);
4494            },
4495        );
4496    }
4497
4498    #[test]
4499    fn ruby_bare_predicate_does_not_double_count_comparison_or_chain() {
4500        // `if a == b` counts only the `==` comparison (the condition field
4501        // is a `binary` node, adding nothing). `if a && b` counts the two
4502        // chain operands via the `&&` walker, again with the condition-field
4503        // arm adding nothing — so neither shape is double-counted (#696).
4504        //
4505        // expected: 3 — `==` (1) + the `a`,`b` operands of `&&` (2).
4506        check_metrics::<RubyParser>(
4507            "def f(a, b)\n  if a == b\n    x\n  end\n  if a && b\n    y\n  end\nend\n",
4508            "foo.rb",
4509            |metric| {
4510                assert_eq!(metric.abc.conditions_sum(), 3);
4511            },
4512        );
4513    }
4514
4515    #[test]
4516    fn ruby_spaceship_and_case_equality() {
4517        // `<=>` and `===` are comparison operators (conditions).
4518        check_metrics::<RubyParser>(
4519            "def f(a, b)\n  a <=> b\n  a === b\nend\n",
4520            "foo.rb",
4521            |metric| {
4522                assert_eq!(metric.abc.conditions_sum(), 2);
4523                insta::assert_json_snapshot!(metric.abc);
4524            },
4525        );
4526    }
4527
4528    #[test]
4529    fn ruby_ternary_condition() {
4530        // The `?` ternary marker is one condition; the inner `==` is
4531        // another.
4532        check_metrics::<RubyParser>("def f(x)\n  x == 0 ? :z : :nz\nend\n", "foo.rb", |metric| {
4533            assert_eq!(metric.abc.conditions_sum(), 2);
4534            insta::assert_json_snapshot!(metric.abc);
4535        });
4536    }
4537
4538    #[test]
4539    fn ruby_case_when_arms() {
4540        // Each `when` named clause and the `else` clause count as one
4541        // condition each; the `case` head and the implicit `then`
4542        // wrappers do not.
4543        check_metrics::<RubyParser>(
4544            "def f(x)\n  case x\n  when 1 then 'one'\n  when 2 then 'two'\n  else 'other'\n  end\nend\n",
4545            "foo.rb",
4546            |metric| {
4547                // 2 `when` + 1 `else` = 3 conditions.
4548                assert_eq!(metric.abc.conditions_sum(), 3);
4549                insta::assert_json_snapshot!(metric.abc);
4550            },
4551        );
4552    }
4553
4554    #[test]
4555    fn ruby_elsif_and_else() {
4556        // `elsif` and `else` named clauses are conditions; their inner
4557        // `then` wrappers are not.
4558        check_metrics::<RubyParser>(
4559            "def f(x)\n  if x > 0\n    1\n  elsif x < 0\n    -1\n  else\n    0\n  end\nend\n",
4560            "foo.rb",
4561            |metric| {
4562                // `>`(1) + `elsif`(1) + `<`(1) + `else`(1) = 4.
4563                assert_eq!(metric.abc.conditions_sum(), 4);
4564                insta::assert_json_snapshot!(metric.abc);
4565            },
4566        );
4567    }
4568
4569    #[test]
4570    fn ruby_rescue_clause_condition() {
4571        // The `rescue` named clause is one condition; the `rescue`
4572        // keyword token (`Rescue2`) is not counted on its own.
4573        // `do_it` without parens is an `identifier`, not a `call`, so
4574        // it contributes no branch. `handle(e)` is a `call` (1 branch).
4575        check_metrics::<RubyParser>(
4576            "def f\n  begin\n    do_it\n  rescue StandardError => e\n    handle(e)\n  end\nend\n",
4577            "foo.rb",
4578            |metric| {
4579                assert_eq!(metric.abc.conditions_sum(), 1);
4580                assert_eq!(metric.abc.branches_sum(), 1);
4581                insta::assert_json_snapshot!(metric.abc);
4582            },
4583        );
4584    }
4585
4586    #[test]
4587    fn ruby_class_complex_function() {
4588        // Mixed: assignment(=), branch(call), conditions(`>` and `==`).
4589        check_metrics::<RubyParser>(
4590            "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",
4591            "foo.rb",
4592            |metric| {
4593                assert_eq!(metric.abc.assignments_sum(), 1);
4594                assert_eq!(metric.abc.branches_sum(), 1);
4595                // `>`(1) + `==`(1) = 2 conditions. `if` is not a
4596                // token; `&&` is `AMPAMP` and is not counted (see
4597                // the module-level `Stats` doc-comment for the
4598                // cross-language policy; #395, walker tracked in
4599                // #403).
4600                assert_eq!(metric.abc.conditions_sum(), 2);
4601                insta::assert_json_snapshot!(metric.abc);
4602            },
4603        );
4604    }
4605
4606    #[test]
4607    fn ruby_unary_conditions_in_chain() {
4608        // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
4609        // `&&` / `||` chain is one condition. `a && b || c` → a, b, c each
4610        // contribute one. Ruby's `if` keyword is not a condition token.
4611        // expected: 3 unary conditions (matches the Java byte-equivalent).
4612        check_metrics::<RubyParser>(
4613            "def f(a, b, c)\n  if a && b || c\n    puts \"x\"\n  end\nend\n",
4614            "foo.rb",
4615            |metric| {
4616                assert_eq!(metric.abc.conditions_sum(), 3);
4617            },
4618        );
4619    }
4620
4621    #[test]
4622    fn ruby_keyword_and_or_chain_counts_operands() {
4623        // The keyword forms `and` / `or` get the same Rule 9 treatment as
4624        // `&&` / `||`. expected: 3 unary conditions (a, b, c).
4625        check_metrics::<RubyParser>(
4626            "def f(a, b, c)\n  if a and b or c\n    puts \"x\"\n  end\nend\n",
4627            "foo.rb",
4628            |metric| {
4629                assert_eq!(metric.abc.conditions_sum(), 3);
4630            },
4631        );
4632    }
4633
4634    #[test]
4635    fn ruby_negated_operand_is_unary_condition() {
4636        // A `!`-negated operand unwraps the `unary` node to the inner
4637        // identifier. expected: 2 (`a` and the `!b` operand).
4638        check_metrics::<RubyParser>(
4639            "def f(a, b)\n  if a && !b\n    puts \"x\"\n  end\nend\n",
4640            "foo.rb",
4641            |metric| {
4642                assert_eq!(metric.abc.conditions_sum(), 2);
4643            },
4644        );
4645    }
4646
4647    #[test]
4648    fn ruby_comparison_operands_add_nothing() {
4649        // Isolation for Rule 9 (issue #557): when the `&&` operands are
4650        // themselves comparisons, the unary-condition walker must add
4651        // nothing on top of the two `>` comparisons already counted as
4652        // conditions — distinguishing the gap (bare boolean operands)
4653        // from ordinary relational conditions. Mirrors the Kotlin and
4654        // Elixir isolation tests. expected: 2 (the two `>` comparisons).
4655        check_metrics::<RubyParser>(
4656            "def f(x, y)\n  if x > 0 && y > 0\n    puts \"x\"\n  end\nend\n",
4657            "foo.rb",
4658            |metric| {
4659                assert_eq!(metric.abc.conditions_sum(), 2);
4660            },
4661        );
4662    }
4663
4664    // ---------------------------------------------------------------
4665    // Default-impl placeholder smoke tests (audited in #188).
4666    //
4667    // These tests assert that the *current* default-impl languages
4668    // return ABC = 0/0/0 for source that DOES contain branches,
4669    // conditions, and assignments. When the real impl lands for any
4670    // of these languages, the corresponding assertion below will fire
4671    // — the implementer must update the expected values, which is the
4672    // gate. Tag the follow-up issue in each test.
4673    // ---------------------------------------------------------------
4674
4675    // --- Python ABC ---------------------------------------------------
4676
4677    #[test]
4678    fn python_empty_module_zero() {
4679        check_metrics::<PythonParser>("", "empty.py", |metric| {
4680            assert_eq!(metric.abc.assignments_sum(), 0);
4681            assert_eq!(metric.abc.branches_sum(), 0);
4682            assert_eq!(metric.abc.conditions_sum(), 0);
4683            insta::assert_json_snapshot!(metric.abc);
4684        });
4685    }
4686
4687    #[test]
4688    fn python_plain_assignments_count() {
4689        // Three plain `=` assignments → A=3. No branches, no conditions.
4690        check_metrics::<PythonParser>("x = 1\ny = 2\nz = x\n", "foo.py", |metric| {
4691            assert_eq!(metric.abc.assignments_sum(), 3);
4692            assert_eq!(metric.abc.branches_sum(), 0);
4693            assert_eq!(metric.abc.conditions_sum(), 0);
4694            insta::assert_json_snapshot!(metric.abc);
4695        });
4696    }
4697
4698    #[test]
4699    fn python_typed_assignment_counts_bare_annotation_does_not() {
4700        // `x: int = 1` carries an `=`, so it counts.
4701        // `y: int` is a bare annotation (no `=`) — declares a type but
4702        // binds nothing; it must NOT inflate the assignment count.
4703        check_metrics::<PythonParser>("x: int = 1\ny: int\n", "foo.py", |metric| {
4704            assert_eq!(metric.abc.assignments_sum(), 1);
4705            insta::assert_json_snapshot!(metric.abc);
4706        });
4707    }
4708
4709    #[test]
4710    fn python_augmented_assignments_count() {
4711        // Each augmented op counts once.
4712        check_metrics::<PythonParser>("x = 0\nx += 1\nx -= 1\nx *= 2\n", "foo.py", |metric| {
4713            // 1 plain `=` + 3 augmented = 4 assignments.
4714            assert_eq!(metric.abc.assignments_sum(), 4);
4715            insta::assert_json_snapshot!(metric.abc);
4716        });
4717    }
4718
4719    #[test]
4720    fn python_walrus_counts_as_assignment() {
4721        // `x := 10` is a `NamedExpression` (PEP 572). It binds a value
4722        // → one assignment under Fitzpatrick's rule.
4723        check_metrics::<PythonParser>("if (n := 10) > 5:\n    pass\n", "foo.py", |metric| {
4724            // 1 assignment (walrus) + 1 condition (`> 5` is a
4725            // ComparisonOperator).
4726            assert_eq!(metric.abc.assignments_sum(), 1);
4727            assert_eq!(metric.abc.conditions_sum(), 1);
4728            insta::assert_json_snapshot!(metric.abc);
4729        });
4730    }
4731
4732    #[test]
4733    fn python_calls_are_branches() {
4734        // `foo()`, `bar()`, `Baz()` (constructor) all parse as `Call`
4735        // → three branches.
4736        check_metrics::<PythonParser>(
4737            "def foo():\n    pass\ndef bar():\n    pass\nclass Baz:\n    pass\nfoo()\nbar()\nBaz()\n",
4738            "foo.py",
4739            |metric| {
4740                assert_eq!(metric.abc.branches_sum(), 3);
4741                assert_eq!(metric.abc.assignments_sum(), 0);
4742                insta::assert_json_snapshot!(metric.abc);
4743            },
4744        );
4745    }
4746
4747    #[test]
4748    fn python_comparisons_count_conditions() {
4749        // `x > 0`, `x == y`, `x is None` are each a single
4750        // `ComparisonOperator` node — three conditions.
4751        check_metrics::<PythonParser>(
4752            "def f(x, y):\n    a = x > 0\n    b = x == y\n    c = x is None\n",
4753            "foo.py",
4754            |metric| {
4755                assert_eq!(metric.abc.conditions_sum(), 3);
4756                // 3 plain assignments; the comparisons are operands.
4757                assert_eq!(metric.abc.assignments_sum(), 3);
4758                insta::assert_json_snapshot!(metric.abc);
4759            },
4760        );
4761    }
4762
4763    #[test]
4764    fn python_chained_comparison_counts_once() {
4765        // tree-sitter-python collapses `0 < x < 10` into a single
4766        // `ComparisonOperator` — one condition, not two.
4767        check_metrics::<PythonParser>("def f(x):\n    return 0 < x < 10\n", "foo.py", |metric| {
4768            assert_eq!(metric.abc.conditions_sum(), 1);
4769            insta::assert_json_snapshot!(metric.abc);
4770        });
4771    }
4772
4773    #[test]
4774    fn python_number_truthy_condition_counts() {
4775        // Regression for #772: Python treats every non-zero number as
4776        // truthy, so `if 5:` and `x and 5` should each count their
4777        // numeric literal as a Fitzpatrick unary condition. Pre-fix
4778        // `python_bool_terminal_kinds!()` listed `True` / `False` but
4779        // omitted `Integer` / `Float`, so the walker dropped every
4780        // numeric-truthy operand (mirrors the Lua `Number` fix).
4781        check_metrics::<PythonParser>(
4782            "def f(a):\n    if 5:\n        pass\n    return a and 2\n",
4783            "foo.py",
4784            |metric| {
4785                // `if 5:` → walker counts the Integer literal (+1).
4786                // `a and 2` → `and` walker counts both operands:
4787                //   identifier `a` (+1), Integer `2` (+1).
4788                // Total: 3.
4789                assert_eq!(metric.abc.conditions_sum(), 3);
4790                insta::assert_json_snapshot!(metric.abc);
4791            },
4792        );
4793    }
4794
4795    #[test]
4796    fn python_boolean_operators_not_counted_directly() {
4797        // Python's `and` / `or` are not counted as conditions on
4798        // their own (Fitzpatrick Rule 5; #395). Each operand is
4799        // instead counted as a unary conditional by the walker
4800        // (Rule 9; #403). `if a and b or c:` parses left-to-right
4801        // with `or` lower precedence: `(a and b) or c`. Walker
4802        // tallies: inner `and` counts `a`, `b` (+2); outer `or`
4803        // counts only the new outer operand `c` (+1; the inner
4804        // `(a and b)` BooleanOperator is not a terminal). Total
4805        // C = 3.
4806        check_metrics::<PythonParser>(
4807            "def f(a, b, c):\n    if a and b or c:\n        pass\n",
4808            "foo.py",
4809            |metric| {
4810                assert_eq!(metric.abc.conditions_sum(), 3);
4811                insta::assert_json_snapshot!(metric.abc);
4812            },
4813        );
4814    }
4815
4816    /// Python's unary `not` operator parses as `NotOperator` and now
4817    /// counts as one condition, matching Java's `!x` rule. Closes
4818    /// the parity gap noted in #214: without this, `if not flag:`
4819    /// reported 0 conditions while the Java equivalent reports 1.
4820    #[test]
4821    fn python_unary_not_counts_as_condition() {
4822        check_metrics::<PythonParser>(
4823            "def f(flag):\n    if not flag:\n        return 1\n    return 0\n",
4824            "foo.py",
4825            |metric| {
4826                // One `NotOperator` -> 1 condition. The `if` itself
4827                // is structural and doesn't add an Abc condition.
4828                assert_eq!(metric.abc.conditions_sum(), 1);
4829                insta::assert_json_snapshot!(metric.abc);
4830            },
4831        );
4832    }
4833
4834    /// `return not flag` — the unary `not` is the entire return
4835    /// expression. Without `NotOperator` counted, this reports zero
4836    /// conditions; with it, one. Java's `return !flag;` is one.
4837    #[test]
4838    fn python_return_unary_not_counts() {
4839        check_metrics::<PythonParser>("def f(flag):\n    return not flag\n", "foo.py", |metric| {
4840            assert_eq!(metric.abc.conditions_sum(), 1);
4841            insta::assert_json_snapshot!(metric.abc);
4842        });
4843    }
4844
4845    /// `foo(not ready, value)` — the unary `not` inside an argument
4846    /// list still contributes. Mirrors Java's
4847    /// `java_count_unary_conditions` walk over argument lists.
4848    #[test]
4849    fn python_unary_not_in_argument_list_counts() {
4850        check_metrics::<PythonParser>(
4851            "def f(ready, value):\n    log(not ready, value)\n",
4852            "foo.py",
4853            |metric| {
4854                // 1 Call (log) -> 1 branch.
4855                // 1 NotOperator (not ready) -> 1 condition.
4856                assert_eq!(metric.abc.branches_sum(), 1);
4857                assert_eq!(metric.abc.conditions_sum(), 1);
4858                insta::assert_json_snapshot!(metric.abc);
4859            },
4860        );
4861    }
4862
4863    /// Nested `not` + comparison counts each unique node once.
4864    /// `not (x > 0)` parses as `NotOperator(ParenthesizedExpression(
4865    /// ComparisonOperator))`; both the unary and the comparison
4866    /// contribute one condition (mirrors Java's `!(x > 0)` = 2
4867    /// conditions).
4868    #[test]
4869    fn python_unary_not_with_comparison_counts_each_once() {
4870        check_metrics::<PythonParser>(
4871            "def f(x):\n    if not (x > 0):\n        return 1\n    return 0\n",
4872            "foo.py",
4873            |metric| {
4874                // NotOperator (1) + ComparisonOperator (1) = 2.
4875                assert_eq!(metric.abc.conditions_sum(), 2);
4876                insta::assert_json_snapshot!(metric.abc);
4877            },
4878        );
4879    }
4880
4881    /// `not x and y` parses as `BooleanOperator(NotOperator(x), and,
4882    /// y)`. The `and` itself is NOT counted (Fitzpatrick Rule 5
4883    /// lists only comparison operators); the `NotOperator` is
4884    /// counted at the top level (Rule 7); and the `y` operand is
4885    /// counted by the Rule 9 walker (issue #403). Total: 2.
4886    /// `NotOperator` is intentionally not walked-into a second
4887    /// time — the walker skips it to avoid double-counting.
4888    #[test]
4889    fn python_unary_not_with_boolean_combinator_counts_each() {
4890        check_metrics::<PythonParser>(
4891            "def f(x, y):\n    if not x and y:\n        return 1\n    return 0\n",
4892            "foo.py",
4893            |metric| {
4894                // NotOperator (1) + walker on `and` finds `y` (1) = 2.
4895                assert_eq!(metric.abc.conditions_sum(), 2);
4896                insta::assert_json_snapshot!(metric.abc);
4897            },
4898        );
4899    }
4900
4901    #[test]
4902    fn python_control_flow_arms_count_conditions() {
4903        // `elif`, `else`, `except`, `finally`, `case` each contribute
4904        // one condition. The comparisons in the `if`/`elif`/`while`
4905        // headers contribute their own ComparisonOperator counts.
4906        check_metrics::<PythonParser>(
4907            "def f(x):\n    if x > 0:\n        a = 1\n    elif x > -1:\n        a = 2\n    else:\n        a = 3\n",
4908            "foo.py",
4909            |metric| {
4910                // 2 ComparisonOperator (`x > 0`, `x > -1`) + 1
4911                // ElifClause + 1 ElseClause = 4 conditions.
4912                assert_eq!(metric.abc.conditions_sum(), 4);
4913                insta::assert_json_snapshot!(metric.abc);
4914            },
4915        );
4916    }
4917
4918    #[test]
4919    fn python_ternary_counts_as_condition() {
4920        // `a if c else b` is `ConditionalExpression` → 1 condition.
4921        // `c > 0` adds 1 more (ComparisonOperator).
4922        check_metrics::<PythonParser>(
4923            "def f(c):\n    return 1 if c > 0 else 0\n",
4924            "foo.py",
4925            |metric| {
4926                assert_eq!(metric.abc.conditions_sum(), 2);
4927                insta::assert_json_snapshot!(metric.abc);
4928            },
4929        );
4930    }
4931
4932    #[test]
4933    fn python_try_except_finally_count_conditions() {
4934        // ExceptClause + FinallyClause → 2 conditions.
4935        check_metrics::<PythonParser>(
4936            "def f():\n    try:\n        pass\n    except ValueError:\n        pass\n    finally:\n        pass\n",
4937            "foo.py",
4938            |metric| {
4939                assert_eq!(metric.abc.conditions_sum(), 2);
4940                insta::assert_json_snapshot!(metric.abc);
4941            },
4942        );
4943    }
4944
4945    #[test]
4946    fn python_match_case_counts_conditions() {
4947        // Each non-wildcard `CaseClause` → 1 condition. The bare
4948        // `case _:` arm is the language-neutral `default:` equivalent
4949        // and is excluded (matches Rust's bare-`_` MatchArm filter and
4950        // Java/C#'s `default:` rule). Source has `case 1:` (counts) +
4951        // `case _:` (excluded) → C = 1.
4952        check_metrics::<PythonParser>(
4953            "def f(x):\n    match x:\n        case 1:\n            pass\n        case _:\n            pass\n",
4954            "foo.py",
4955            |metric| {
4956                assert_eq!(metric.abc.conditions_sum(), 1);
4957                insta::assert_json_snapshot!(metric.abc);
4958            },
4959        );
4960    }
4961
4962    #[test]
4963    fn python_match_case_guarded_wildcard_counts() {
4964        // `case _ if g:` is NOT a bare wildcard — the guard
4965        // contributes real branching, so the arm counts as a
4966        // condition. Mirrors Rust's `_ if g => ...` behavior.
4967        // Source: `case 1:` (counts) + `case _ if x > 0:` (guarded
4968        // wildcard, counts) + `case _:` (bare wildcard, excluded) →
4969        // C from CaseClause = 2; the guard's `x > 0` adds one
4970        // ComparisonOperator → total C = 3.
4971        check_metrics::<PythonParser>(
4972            "def f(x):\n    match x:\n        case 1:\n            pass\n        case _ if x > 0:\n            pass\n        case _:\n            pass\n",
4973            "foo.py",
4974            |metric| {
4975                assert_eq!(metric.abc.conditions_sum(), 3);
4976                insta::assert_json_snapshot!(metric.abc);
4977            },
4978        );
4979    }
4980
4981    #[test]
4982    fn python_complex_function_abc() {
4983        // Mixed-shape regression: assignments, calls, conditions all in
4984        // a single function.
4985        check_metrics::<PythonParser>(
4986            "def f(items, threshold):\n\
4987             \x20   result = []\n\
4988             \x20   for item in items:\n\
4989             \x20       if item > threshold:\n\
4990             \x20           result.append(item)\n\
4991             \x20   return result\n",
4992            "foo.py",
4993            |metric| {
4994                // assignments: `result = []` → 1
4995                // branches: `result.append(item)` is one call → 1
4996                // conditions: `item > threshold` is one
4997                // ComparisonOperator → 1
4998                assert_eq!(metric.abc.assignments_sum(), 1);
4999                assert_eq!(metric.abc.branches_sum(), 1);
5000                assert_eq!(metric.abc.conditions_sum(), 1);
5001                insta::assert_json_snapshot!(metric.abc);
5002            },
5003        );
5004    }
5005
5006    #[test]
5007    fn python_if_multiple_conditions() {
5008        // Fitzpatrick Rule 9 walker on `and` / `or` (issue #403).
5009        //   - `if a or b or c or d:` → 4 (each operand counted once)
5010        //   - `if a and b and c:`    → 3
5011        //   - `if not a and not b:`  → 2 (two `NotOperator`s counted
5012        //     by the top-level dispatcher arm; the walker SKIPS
5013        //     `NotOperator` children to avoid double-counting)
5014        // Total: 4 + 3 + 2 = 9.
5015        check_metrics::<PythonParser>(
5016            "def f(a, b, c, d):\n\
5017             \x20   if a or b or c or d:           # +4c\n\
5018             \x20       pass\n\
5019             \x20   if a and b and c:              # +3c\n\
5020             \x20       pass\n\
5021             \x20   if not a and not b:            # +2c (NotOperator x2)\n\
5022             \x20       pass\n",
5023            "foo.py",
5024            |metric| {
5025                assert_eq!(metric.abc.conditions_sum(), 9);
5026                insta::assert_json_snapshot!(metric.abc);
5027            },
5028        );
5029    }
5030
5031    #[test]
5032    fn python_while_conditions() {
5033        // Python has no `do { ... } while(cond);` construct, so this
5034        // mirrors only the `while` half of the Java suite. The
5035        // walker fires on each `and` / `or` token inside the loop
5036        // header.
5037        check_metrics::<PythonParser>(
5038            "def f(a, b):\n\
5039             \x20   while a or b:                  # +2c\n\
5040             \x20       break\n\
5041             \x20   while a and not b:             # +2c (a + NotOperator)\n\
5042             \x20       break\n",
5043            "foo.py",
5044            |metric| {
5045                assert_eq!(metric.abc.conditions_sum(), 4);
5046                insta::assert_json_snapshot!(metric.abc);
5047            },
5048        );
5049    }
5050
5051    #[test]
5052    fn python_short_circuit_with_boolean_literal_operand() {
5053        // `a and True` reports 2 conditions: one identifier, one
5054        // True literal. Confirms `True` / `False` are in the walker
5055        // terminal set.
5056        check_metrics::<PythonParser>("def f(a):\n    return a and True\n", "foo.py", |metric| {
5057            assert_eq!(metric.abc.conditions_sum(), 2);
5058            insta::assert_json_snapshot!(metric.abc);
5059        });
5060    }
5061
5062    #[test]
5063    fn python_await_expression_condition_counts() {
5064        // Regression for findings.md round-2 #2 (Python):
5065        // `if await ready(): pass` parses with `await` as the
5066        // condition node. Adding `Python::Await` to the
5067        // terminal-bool set mirrors the C# reference (lesson 19).
5068        check_metrics::<PythonParser>(
5069            "async def ready(): return True\n\
5070             async def f():\n    if await ready(): pass\n",
5071            "foo.py",
5072            |metric| {
5073                // ready() is a call (1 branch); await is the
5074                // condition (1).
5075                assert_eq!(metric.abc.branches_sum(), 1);
5076                assert_eq!(metric.abc.conditions_sum(), 1);
5077                insta::assert_json_snapshot!(metric.abc);
5078            },
5079        );
5080    }
5081
5082    #[test]
5083    fn python_if_call_terminal_condition_counts_once() {
5084        // Pins the Phase-2B behaviour for Python's `Call` terminal-bool
5085        // kind: `if foo():` is a Fitzpatrick Rule 6 unary conditional
5086        // (a bare boolean-evaluating call as the if-condition). The
5087        // walker's terminal-at-top check fires once per call-condition;
5088        // the call itself separately contributes 1 branch. Surfaced
5089        // (and verified intentional) by the code-review pass on
5090        // Phase 2B.
5091        check_metrics::<PythonParser>("def f():\n    if foo(): pass\n", "foo.py", |metric| {
5092            assert_eq!(metric.abc.branches_sum(), 1);
5093            assert_eq!(metric.abc.conditions_sum(), 1);
5094            insta::assert_json_snapshot!(metric.abc);
5095        });
5096    }
5097
5098    #[test]
5099    fn python_if_boolean_literal_condition() {
5100        // Phase 2B (issue #403): bare-boolean conditions count once.
5101        // Python has no paren wrap around if-conditions, so the
5102        // condition node is checked directly. The existing
5103        // NotOperator / ComparisonOperator arms continue to fire
5104        // for those shapes; only the bare-terminal cases (Identifier,
5105        // True, False, etc.) are added by the new arm.
5106        check_metrics::<PythonParser>(
5107            "def f(a):\n\
5108             \x20   if True: pass        # +1c\n\
5109             \x20   if False: pass       # +1c\n\
5110             \x20   while True: break    # +1c\n\
5111             \x20   if a: pass           # +1c (Rule 6 — bare identifier as condition)\n",
5112            "foo.py",
5113            |metric| {
5114                assert_eq!(metric.abc.conditions_sum(), 4);
5115                insta::assert_json_snapshot!(metric.abc);
5116            },
5117        );
5118    }
5119
5120    #[test]
5121    fn python_methods_arguments_with_conditions() {
5122        // `m(not a, not b)` reports 2 conditions — both `NotOperator`
5123        // nodes are counted by Python's pre-existing top-level
5124        // NotOperator dispatcher arm. The argument-list walker does
5125        // not need a separate Python arm.
5126        check_metrics::<PythonParser>(
5127            "def f(a, b):\n\
5128             \x20   m(a, b)             # +1b\n\
5129             \x20   m(not a, not b)     # +1b +2c\n",
5130            "foo.py",
5131            |metric| {
5132                assert_eq!(metric.abc.branches_sum(), 2);
5133                assert_eq!(metric.abc.conditions_sum(), 2);
5134                insta::assert_json_snapshot!(metric.abc);
5135            },
5136        );
5137    }
5138
5139    #[test]
5140    fn python_return_with_conditions() {
5141        // Phase 2B (issue #403). Python uses the pre-existing top-
5142        // level NotOperator / ComparisonOperator arms for return
5143        // expressions; no dedicated ReturnStatement walker arm is
5144        // needed.
5145        check_metrics::<PythonParser>(
5146            "def m1(z): return not (z >= 0)\n\
5147             def m2(x): return (((not x)))\n\
5148             def m3(x, y): return x and y\n",
5149            "foo.py",
5150            |metric| {
5151                // m1: NotOperator (1) + ComparisonOperator (1) = 2.
5152                // m2: NotOperator (1).
5153                // m3: walker on `and` counts both operands = 2.
5154                // Sum: 5.
5155                assert_eq!(metric.abc.conditions_sum(), 5);
5156                insta::assert_json_snapshot!(metric.abc);
5157            },
5158        );
5159    }
5160
5161    #[test]
5162    fn rust_empty_unit_zero() {
5163        // No code at all → A=B=C=0. Establishes the trait is wired up
5164        // and the per-language compute is reachable.
5165        check_metrics::<RustParser>("", "empty.rs", |metric| {
5166            assert_eq!(metric.abc.assignments_sum(), 0);
5167            assert_eq!(metric.abc.branches_sum(), 0);
5168            assert_eq!(metric.abc.conditions_sum(), 0);
5169            insta::assert_json_snapshot!(metric.abc);
5170        });
5171    }
5172
5173    #[test]
5174    fn rust_assignments_let_init_plain_and_compound() {
5175        // `let mut x = 0` is a `let_declaration` carrying an `=`
5176        // initializer → counts as 1 (matches Fitzpatrick's literal
5177        // "every `=` is an assignment" rule and the JS impl's
5178        // treatment of `let x = 5`). `x = 5` and `x = 7` are plain
5179        // `=` assignments → 2. `x += 2` is a compound assignment → 1.
5180        // Total A = 4.
5181        check_metrics::<RustParser>(
5182            "fn f() { let mut x = 0; x = 5; x += 2; x = 7; }",
5183            "foo.rs",
5184            |metric| {
5185                assert_eq!(metric.abc.assignments_sum(), 4);
5186                assert_eq!(metric.abc.branches_sum(), 0);
5187                assert_eq!(metric.abc.conditions_sum(), 0);
5188                insta::assert_json_snapshot!(metric.abc);
5189            },
5190        );
5191    }
5192
5193    #[test]
5194    fn rust_let_without_initializer_does_not_count() {
5195        // `let a;` is a `let_declaration` with NO `=` and no `value`
5196        // field — the binding is uninitialised. The arm only fires
5197        // when `value` is present, so this contributes zero to A.
5198        // `let _b;` is the same shape (the `_` pattern is still a
5199        // pattern, not a wildcard suppression of the binding).
5200        // Regression test for issue #393: only `=` counts, not the
5201        // bare declaration.
5202        check_metrics::<RustParser>(
5203            "fn f() { let a: i32; let _b: i32; a = 5; }",
5204            "foo.rs",
5205            |metric| {
5206                // Only `a = 5` (assignment_expression) → A = 1.
5207                assert_eq!(metric.abc.assignments_sum(), 1);
5208                insta::assert_json_snapshot!(metric.abc);
5209            },
5210        );
5211    }
5212
5213    #[test]
5214    fn rust_let_initializers_immutable_and_mutable_count() {
5215        // Issue #393: `let a = 1;`, `let b = 2;`, `let c = a + b;`,
5216        // `let mut d = 0;` are all `let_declaration` nodes carrying
5217        // an `=` initializer — each counts as 1 (Option B in the
5218        // issue body: literal Fitzpatrick, both `let` and `let mut`
5219        // count). `d = 5;` is one plain assignment_expression, `d
5220        // += 1;` is one compound. Total A = 4 + 1 + 1 = 6.
5221        check_metrics::<RustParser>(
5222            "fn f() { let a=1; let b=2; let c=a+b; let mut d=0; d=5; d+=1; }",
5223            "foo.rs",
5224            |metric| {
5225                assert_eq!(metric.abc.assignments_sum(), 6);
5226                insta::assert_json_snapshot!(metric.abc);
5227            },
5228        );
5229    }
5230
5231    #[test]
5232    fn rust_calls_are_branches() {
5233        // Free function call + method call (parses as call_expression
5234        // with a field_expression callee) + associated-fn call. All
5235        // three are `call_expression` → B = 3. Macro invocations like
5236        // `println!` parse as `macro_invocation`, NOT `call_expression`,
5237        // so they are not branches.
5238        check_metrics::<RustParser>(
5239            "fn f() { g(); 1.to_string(); String::new(); }\nfn g() {}\n",
5240            "foo.rs",
5241            |metric| {
5242                assert_eq!(metric.abc.branches_sum(), 3);
5243                assert_eq!(metric.abc.assignments_sum(), 0);
5244                assert_eq!(metric.abc.conditions_sum(), 0);
5245                insta::assert_json_snapshot!(metric.abc);
5246            },
5247        );
5248    }
5249
5250    #[test]
5251    fn rust_try_operator_is_branch() {
5252        // `?` parses as `try_expression` and counts as one branch
5253        // (short-circuit return on Err / None). The `Err(())` call
5254        // contributes one branch in addition (call_expression).
5255        check_metrics::<RustParser>(
5256            "fn f() -> Result<i32, ()> { let r: Result<i32, ()> = Err(()); Ok(r?) }",
5257            "foo.rs",
5258            |metric| {
5259                // Err(()) + Ok(...) + r? → 2 calls + 1 try = 3 branches.
5260                assert_eq!(metric.abc.branches_sum(), 3);
5261                insta::assert_json_snapshot!(metric.abc);
5262            },
5263        );
5264    }
5265
5266    #[test]
5267    fn rust_comparisons_count_conditions() {
5268        // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six
5269        // comparisons → C = 6.
5270        check_metrics::<RustParser>(
5271            "fn f(a: i32, b: i32) -> bool { a < b || a > b || a <= b || a >= b || a == b || a != b }",
5272            "foo.rs",
5273            |metric| {
5274                assert_eq!(metric.abc.conditions_sum(), 6);
5275                insta::assert_json_snapshot!(metric.abc);
5276            },
5277        );
5278    }
5279
5280    #[test]
5281    fn rust_generic_brackets_not_conditions() {
5282        // `<` / `>` in `Vec<i32>` are TypeArguments delimiters, not
5283        // comparison operators. The parent-check in the LT/GT arms
5284        // must filter them out. Expected C = 0.
5285        check_metrics::<RustParser>(
5286            "fn f() -> Vec<i32> { Vec::<i32>::new() }",
5287            "foo.rs",
5288            |metric| {
5289                assert_eq!(metric.abc.conditions_sum(), 0);
5290                insta::assert_json_snapshot!(metric.abc);
5291            },
5292        );
5293    }
5294
5295    #[test]
5296    fn rust_if_let_counts_as_condition() {
5297        // `if let Some(v) = opt { ... }` introduces a `let_condition`
5298        // → 1 condition. The `if` keyword itself does not add another
5299        // count — Fitzpatrick counts conditions, not branch keywords.
5300        check_metrics::<RustParser>(
5301            "fn f(opt: Option<i32>) { if let Some(_v) = opt { } }",
5302            "foo.rs",
5303            |metric| {
5304                assert_eq!(metric.abc.conditions_sum(), 1);
5305                insta::assert_json_snapshot!(metric.abc);
5306            },
5307        );
5308    }
5309
5310    #[test]
5311    fn rust_while_let_counts_as_condition() {
5312        // `while let Some(y) = it.next() { ... }` is also a
5313        // `let_condition` (the `while` form). One condition; the
5314        // `it.next()` call adds one branch.
5315        check_metrics::<RustParser>(
5316            "fn f(mut it: std::vec::IntoIter<i32>) { while let Some(_y) = it.next() { } }",
5317            "foo.rs",
5318            |metric| {
5319                assert_eq!(metric.abc.conditions_sum(), 1);
5320                assert_eq!(metric.abc.branches_sum(), 1);
5321                insta::assert_json_snapshot!(metric.abc);
5322            },
5323        );
5324    }
5325
5326    #[test]
5327    fn rust_match_arms_count_conditions_wildcard_excluded() {
5328        // Three arms: `0 => 1`, `n if n > 0 => n`, `_ => -1`. The
5329        // bare wildcard is the `default:` equivalent and is skipped.
5330        // The guarded arm has a `n if n > 0` pattern (more than one
5331        // child in the match_pattern) and still counts. Two non-wildcard
5332        // arms → C = 2 from MatchArm. Plus the comparison `n > 0`
5333        // adds one more → C = 3.
5334        check_metrics::<RustParser>(
5335            "fn f(x: i32) -> i32 { match x { 0 => 1, n if n > 0 => n, _ => -1, } }",
5336            "foo.rs",
5337            |metric| {
5338                assert_eq!(metric.abc.conditions_sum(), 3);
5339                insta::assert_json_snapshot!(metric.abc);
5340            },
5341        );
5342    }
5343
5344    #[test]
5345    fn rust_else_counts_as_condition() {
5346        // `if a > b { ... } else { ... }` → `a > b` is one condition,
5347        // `else` is one condition → C = 2.
5348        check_metrics::<RustParser>(
5349            "fn f(a: i32, b: i32) -> i32 { if a > b { a } else { b } }",
5350            "foo.rs",
5351            |metric| {
5352                assert_eq!(metric.abc.conditions_sum(), 2);
5353                insta::assert_json_snapshot!(metric.abc);
5354            },
5355        );
5356    }
5357
5358    #[test]
5359    fn rust_let_chain2_hidden_rule_drift_marker() {
5360        // Drift marker (findings.md round-2 #3): `Rust::LetChain2`
5361        // maps to the hidden grammar rule `_let_chain`. At the
5362        // pinned tree-sitter-rust version it is never emitted as a
5363        // concrete node — the visible `LetChain` (= 352) carries
5364        // every let-chain. We list `LetChain2` defensively in
5365        // `rust_inspect_container` and `rust_count_unary_conditions`
5366        // (lesson 34); if a future grammar bump promotes
5367        // `_let_chain` to a visible rule, this assertion fails
5368        // loudly so the maintainer knows to verify the walker still
5369        // counts correctly for the new shape.
5370        let src = "fn f(a: bool, b: Option<i32>) {\n\
5371                   \x20   if a && let Some(_) = b { }\n\
5372                   }\n";
5373        let parser = RustParser::new(
5374            src.as_bytes().to_vec(),
5375            &std::path::PathBuf::from("foo.rs"),
5376            None,
5377        );
5378        assert!(!ast_has_kind_id(&parser, Rust::LetChain2 as u16));
5379    }
5380
5381    #[test]
5382    fn rust_scoped_identifier_condition_counts() {
5383        // Regression for findings.md round-2 #1 (Rust):
5384        // `if crate::FLAG {}` parses with `scoped_identifier` as the
5385        // condition node. Pre-fix, `rust_bool_terminal_kinds!()`
5386        // listed only `Identifier` so the walker reached the
5387        // `scoped_identifier` child, found it non-terminal /
5388        // non-paren / non-unary, and broke without counting.
5389        // Mirrors the C# fix in #372 (lesson 19) for
5390        // `MemberAccessExpression`.
5391        check_metrics::<RustParser>("fn f() { if crate::FLAG { } }\n", "foo.rs", |metric| {
5392            assert_eq!(metric.abc.conditions_sum(), 1);
5393            insta::assert_json_snapshot!(metric.abc);
5394        });
5395    }
5396
5397    #[test]
5398    fn rust_await_expression_condition_counts() {
5399        // Regression for findings.md round-2 #2 (Rust):
5400        // `if ready().await {}` parses with `await_expression` as
5401        // the condition node. Adding `Rust::AwaitExpression` to the
5402        // terminal-bool set closes the parity gap with the C#
5403        // reference (`csharp_bool_terminal_kinds!()`).
5404        check_metrics::<RustParser>(
5405            "async fn ready() -> bool { true }\n\
5406             async fn f() { if ready().await { } }\n",
5407            "foo.rs",
5408            |metric| {
5409                // ready() is a call (1 branch); `ready().await` is
5410                // the unary boolean condition (1).
5411                assert_eq!(metric.abc.branches_sum(), 1);
5412                assert_eq!(metric.abc.conditions_sum(), 1);
5413                insta::assert_json_snapshot!(metric.abc);
5414            },
5415        );
5416    }
5417
5418    #[test]
5419    fn rust_complex_function_abc() {
5420        // Mixed-shape regression: assignments, calls, conditions, `?`,
5421        // `if let`, `match` in one body. Verified by hand:
5422        // - assignments: `let mut x = 0` (let init), `x = 5`, `x += 2`,
5423        //   `let _ = ...` (let init), `let r: ... = Err(())` (let init),
5424        //   `let _v = r?` (let init) → A = 6 (post-#393: every `=`
5425        //   initializer in a `let_declaration` is one assignment, in
5426        //   line with the literal Fitzpatrick reading).
5427        // - branches: `xs.iter()`, `.next()`, `Err(())`, `r?` → B = 4
5428        //   (3 calls + 1 try).
5429        // - conditions: `if let Some(v) = opt` → 1, `match x` arms
5430        //   `0`, `n if n>0` (wildcard excluded) → 2, `n > 0` → 1.
5431        //   Total C = 4.
5432        check_metrics::<RustParser>(
5433            "fn f(opt: Option<i32>, xs: Vec<i32>) -> Result<i32, ()> {\n\
5434             \x20   let mut x = 0;\n\
5435             \x20   x = 5;\n\
5436             \x20   x += 2;\n\
5437             \x20   if let Some(_v) = opt { }\n\
5438             \x20   let _ = xs.iter().next();\n\
5439             \x20   let r: Result<i32, ()> = Err(());\n\
5440             \x20   let _v = r?;\n\
5441             \x20   Ok(match x {\n\
5442             \x20       0 => 1,\n\
5443             \x20       n if n > 0 => n,\n\
5444             \x20       _ => -1,\n\
5445             \x20   })\n\
5446             }\n",
5447            "foo.rs",
5448            |metric| {
5449                assert_eq!(metric.abc.assignments_sum(), 6);
5450                // calls: xs.iter(), .next(), Err(()), Ok(...) → 4 calls
5451                // plus 1 try (`r?`) → 5 branches.
5452                assert_eq!(metric.abc.branches_sum(), 5);
5453                // 1 let_condition + 2 non-wildcard match_arms + 1
5454                // comparison (`n > 0`) → 4.
5455                assert_eq!(metric.abc.conditions_sum(), 4);
5456                insta::assert_json_snapshot!(metric.abc);
5457            },
5458        );
5459    }
5460
5461    #[test]
5462    fn rust_let_chain_bare_identifier_operand_counts() {
5463        // Regression: pre-fix, `if a && let Some(_z) = y { }` reported
5464        // 1 condition (only the LetCondition). The bare-identifier
5465        // `a` operand was lost because Rust 2024 wraps let-chain
5466        // `&&` operands in a `LetChain` node (not `BinaryExpression`)
5467        // and `rust_count_unary_conditions` only counted terminals
5468        // under a `BinaryExpression` parent. Allowing `LetChain` /
5469        // `LetChain2` as known-bool list parents fixes the loss.
5470        // Expected: LetCondition (1) + walker on `a` (1) = 2.
5471        check_metrics::<RustParser>(
5472            "fn f(a: bool, y: Option<i32>) {\n\
5473             \x20   if a && let Some(_z) = y { }\n\
5474             }\n",
5475            "foo.rs",
5476            |metric| {
5477                assert_eq!(metric.abc.conditions_sum(), 2);
5478                insta::assert_json_snapshot!(metric.abc);
5479            },
5480        );
5481    }
5482
5483    #[test]
5484    fn rust_if_multiple_conditions() {
5485        // Fitzpatrick Rule 7 / Listing 2 (issue #403): every operand of
5486        // a `&&` / `||` chain is one condition. Mirrors
5487        // `java_if_multiple_conditions`. Rust's `if` head has no
5488        // parentheses, but the walker fires on each `&&` / `||` token
5489        // and walks the parent `binary_expression` regardless.
5490        check_metrics::<RustParser>(
5491            "fn f(a: bool, b: bool, c: bool, d: bool) -> i32 {\n\
5492             \x20   if a || b || c || d { return 1; }    // +4c\n\
5493             \x20   if a && b && c { return 2; }         // +3c\n\
5494             \x20   if !a && !b { return 3; }            // +2c\n\
5495             \x20   0\n\
5496             }\n",
5497            "foo.rs",
5498            |metric| {
5499                // 4 + 3 + 2 = 9
5500                assert_eq!(metric.abc.conditions_sum(), 9);
5501                insta::assert_json_snapshot!(metric.abc);
5502            },
5503        );
5504    }
5505
5506    #[test]
5507    fn rust_while_conditions() {
5508        // Rust has no `do { ... } while(cond);` construct, so this
5509        // mirrors only the `while` half of `java_while_and_do_while_conditions`.
5510        // Each operand of the `&&` / `||` chain in the loop condition
5511        // counts as one Fitzpatrick condition (Rule 7).
5512        check_metrics::<RustParser>(
5513            "fn f(a: bool, b: bool) {\n\
5514             \x20   while a || b { break; }       // +2c\n\
5515             \x20   while a && !b { break; }      // +2c\n\
5516             }\n",
5517            "foo.rs",
5518            |metric| {
5519                assert_eq!(metric.abc.conditions_sum(), 4);
5520                insta::assert_json_snapshot!(metric.abc);
5521            },
5522        );
5523    }
5524
5525    #[test]
5526    fn rust_if_boolean_literal_condition() {
5527        // Phase 2B (issue #403): a condition whose entire body is a
5528        // boolean literal counts as one Fitzpatrick condition.
5529        // `if true {}` → 1, `if !false {}` → 1 (unary unwrap), and
5530        // `while true { break }` → 1.
5531        check_metrics::<RustParser>(
5532            "fn f() {\n\
5533             \x20   if true { }                  // +1c\n\
5534             \x20   if !false { }                // +1c\n\
5535             \x20   while true { break; }        // +1c\n\
5536             }\n",
5537            "foo.rs",
5538            |metric| {
5539                assert_eq!(metric.abc.conditions_sum(), 3);
5540                insta::assert_json_snapshot!(metric.abc);
5541            },
5542        );
5543    }
5544
5545    #[test]
5546    fn rust_methods_arguments_with_conditions() {
5547        // Phase 2B (issue #403): unary-conditional arguments to a
5548        // call each count once. `m(!a, !b)` → 2 conditions + 1
5549        // branch (the call itself). Bare identifier arguments do
5550        // NOT count (they reach the count_unary_conditions list with
5551        // list_kind = Arguments, not BinaryExpression).
5552        check_metrics::<RustParser>(
5553            "fn f(a: bool, b: bool) {\n\
5554             \x20   m(a, b);                     // +1b\n\
5555             \x20   m(!a, !b);                   // +1b +2c\n\
5556             \x20   m(!a, b, !a);                // +1b +2c\n\
5557             }\n",
5558            "foo.rs",
5559            |metric| {
5560                assert_eq!(metric.abc.branches_sum(), 3);
5561                assert_eq!(metric.abc.conditions_sum(), 4);
5562                insta::assert_json_snapshot!(metric.abc);
5563            },
5564        );
5565    }
5566
5567    #[test]
5568    fn rust_return_with_conditions() {
5569        // Phase 2B (issue #403). Mirrors `java_return_with_conditions`
5570        // — `return !a` / `return x && y` count their unary
5571        // conditional operands. Per Fitzpatrick Rule 7, a `!`-wrapped
5572        // relational expression contributes ONE condition (the
5573        // relational op itself) — the `!` does not add a second
5574        // count when its operand is already a comparison.
5575        check_metrics::<RustParser>(
5576            "fn m1(z: i32) -> bool { return !(z >= 0); }\n\
5577             fn m2(x: bool) -> bool { return (((!x))); }\n\
5578             fn m3(x: bool, y: bool) -> bool { return x && y; }\n\
5579             fn m4(y: bool, z: i32) -> bool { return y || (z < 0); }\n",
5580            "foo.rs",
5581            |metric| {
5582                // m1: !(z >= 0) → the `>=` contributes 1; the unary
5583                //     `!` wraps a paren'd BinaryExpression, which
5584                //     inspect_container does not unwrap further →
5585                //     no walker count. Total: 1.
5586                // m2: (((!x))) → ReturnExpression arm walks (((!x))).
5587                //     inspect_container unwraps three parens and one
5588                //     unary, reaches Identifier `x`, has_boolean_content
5589                //     was seeded true by the unary-not flip. +1.
5590                // m3: x && y → `&&` walker counts both terminals → 2.
5591                // m4: y || (z < 0) → `||` walker counts `y` (terminal,
5592                //     +1); the `<` contributes 1 via its own arm; the
5593                //     paren'd BinaryExpression `(z < 0)` is not
5594                //     terminal under the walker → no extra count.
5595                //     Total: 2.
5596                // Sum: 1 + 1 + 2 + 2 = 6.
5597                assert_eq!(metric.abc.conditions_sum(), 6);
5598                insta::assert_json_snapshot!(metric.abc);
5599            },
5600        );
5601    }
5602
5603    #[test]
5604    fn rust_short_circuit_with_boolean_literal_operand() {
5605        // `if a && true` reports 2 conditions: one for the identifier
5606        // operand, one for the boolean-literal operand. Confirms the
5607        // walker terminal set includes `BooleanLiteral`.
5608        check_metrics::<RustParser>(
5609            "fn f(a: bool) -> bool { a && true }\n",
5610            "foo.rs",
5611            |metric| {
5612                assert_eq!(metric.abc.conditions_sum(), 2);
5613                insta::assert_json_snapshot!(metric.abc);
5614            },
5615        );
5616    }
5617
5618    // ----- Go -----
5619
5620    #[test]
5621    fn go_empty_unit_zero() {
5622        // Package declaration only — no Fitzpatrick events. Confirms the
5623        // GoCode Abc trait is wired up and emits zero counts.
5624        check_metrics::<GoParser>("package main\n", "empty.go", |metric| {
5625            assert_eq!(metric.abc.assignments_sum(), 0);
5626            assert_eq!(metric.abc.branches_sum(), 0);
5627            assert_eq!(metric.abc.conditions_sum(), 0);
5628            insta::assert_json_snapshot!(metric.abc);
5629        });
5630    }
5631
5632    #[test]
5633    fn go_assignments_count_plain_compound_short_var_and_incdec() {
5634        // `x := 0` (short var decl), `x = 5` and `x = 7` (plain `=`),
5635        // `x += 2` (compound), `x++` (inc) → A = 5. `var y = 1` is a
5636        // declaration — its `=` is not counted (matches the Rust/Java
5637        // rule for `let` / `int y = 1`).
5638        check_metrics::<GoParser>(
5639            "package main\nfunc f() { var y = 1; _ = y; x := 0; x = 5; x += 2; x = 7; x++ }\n",
5640            "foo.go",
5641            |metric| {
5642                // `_ = y` is itself an assignment_statement → +1.
5643                // x:= + x=5 + x+=2 + x=7 + x++ + _=y → 6
5644                assert_eq!(metric.abc.assignments_sum(), 6);
5645                assert_eq!(metric.abc.branches_sum(), 0);
5646                assert_eq!(metric.abc.conditions_sum(), 0);
5647                insta::assert_json_snapshot!(metric.abc);
5648            },
5649        );
5650    }
5651
5652    #[test]
5653    fn go_calls_are_branches() {
5654        // Three calls: free function `g()`, method call `r.Inc()`, and
5655        // builtin call `len(s)`. All parse as `call_expression` → B = 3.
5656        // Composite literal `Foo{}` is NOT a call.
5657        check_metrics::<GoParser>(
5658            "package main\n\
5659             type R struct{}\n\
5660             func (r R) Inc() {}\n\
5661             func g() {}\n\
5662             func f(s string) { g(); var r R = R{}; r.Inc(); _ = len(s) }\n",
5663            "foo.go",
5664            |metric| {
5665                assert_eq!(metric.abc.branches_sum(), 3);
5666                insta::assert_json_snapshot!(metric.abc);
5667            },
5668        );
5669    }
5670
5671    #[test]
5672    fn go_comparisons_count_conditions() {
5673        // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six
5674        // comparisons → C = 6.
5675        check_metrics::<GoParser>(
5676            "package main\nfunc f(a, b int) bool { return a < b || a > b || a <= b || a >= b || a == b || a != b }\n",
5677            "foo.go",
5678            |metric| {
5679                assert_eq!(metric.abc.conditions_sum(), 6);
5680                insta::assert_json_snapshot!(metric.abc);
5681            },
5682        );
5683    }
5684
5685    #[test]
5686    fn go_generic_brackets_not_conditions() {
5687        // Generic instantiation `Min[int](a, b)` puts `int` inside
5688        // `TypeArguments`, not `BinaryExpression`. The parent guard on
5689        // `<` / `>` must not count these. Expected C = 0; B = 1 (one call).
5690        check_metrics::<GoParser>(
5691            "package main\nfunc Min[T int | float64](a, b T) T { return a }\nfunc f() { _ = Min[int](1, 2) }\n",
5692            "foo.go",
5693            |metric| {
5694                assert_eq!(metric.abc.conditions_sum(), 0);
5695                assert_eq!(metric.abc.branches_sum(), 1);
5696                insta::assert_json_snapshot!(metric.abc);
5697            },
5698        );
5699    }
5700
5701    #[test]
5702    fn go_switch_arms_count_conditions_default_excluded() {
5703        // Four arms: `case 1:`, `case 2:`, `case 3:`, `default:`. The
5704        // bare `default` is the C/Java `default:` equivalent and is
5705        // excluded — 3 conditions from ExpressionCase. The switch
5706        // expression `x` is bare (no comparison), so no extra
5707        // condition from `==`-style operators.
5708        check_metrics::<GoParser>(
5709            "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",
5710            "foo.go",
5711            |metric| {
5712                assert_eq!(metric.abc.conditions_sum(), 3);
5713                insta::assert_json_snapshot!(metric.abc);
5714            },
5715        );
5716    }
5717
5718    #[test]
5719    fn go_type_switch_arms_count_conditions() {
5720        // Type switch: `case int:`, `case string:`, `default:`. Two
5721        // non-default type-case arms → C = 2.
5722        check_metrics::<GoParser>(
5723            "package main\nfunc f(v interface{}) { switch v.(type) { case int: return; case string: return; default: return } }\n",
5724            "foo.go",
5725            |metric| {
5726                assert_eq!(metric.abc.conditions_sum(), 2);
5727                insta::assert_json_snapshot!(metric.abc);
5728            },
5729        );
5730    }
5731
5732    #[test]
5733    fn go_select_arms_count_conditions() {
5734        // `select { case <-ch: ...; case ch <- 1: ...; default: ... }`.
5735        // Two non-default communication cases → C = 2.
5736        check_metrics::<GoParser>(
5737            "package main\nfunc f(ch chan int) { select { case <-ch: return; case ch <- 1: return; default: return } }\n",
5738            "foo.go",
5739            |metric| {
5740                assert_eq!(metric.abc.conditions_sum(), 2);
5741                insta::assert_json_snapshot!(metric.abc);
5742            },
5743        );
5744    }
5745
5746    #[test]
5747    fn go_else_counts_as_condition() {
5748        // `if a > b { ... } else { ... }` → `a > b` is one condition,
5749        // `else` is one condition → C = 2.
5750        check_metrics::<GoParser>(
5751            "package main\nfunc f(a, b int) int { if a > b { return a } else { return b } }\n",
5752            "foo.go",
5753            |metric| {
5754                assert_eq!(metric.abc.conditions_sum(), 2);
5755                insta::assert_json_snapshot!(metric.abc);
5756            },
5757        );
5758    }
5759
5760    #[test]
5761    fn go_complex_function_abc() {
5762        // Mixed shape, verified by hand:
5763        // - Assignments: `_ = x` (after `var`), `n := 0`,
5764        //   `n = n + 1`, `n += 2`, `n++`, `_ = len(s)` → A = 6.
5765        //   `var x = 10` is a declaration, not counted. Every `_ = ...`
5766        //   IS counted as an assignment_statement.
5767        // - Branches: `len(s)` → B = 1.
5768        // - Conditions: `n < 10` → 1, `else` → 1, switch arms `case 0:`
5769        //   and `case 1:` (default excluded) → 2 → total C = 4.
5770        check_metrics::<GoParser>(
5771            "package main\nfunc f(s string) int {\n\
5772             \x20   var x = 10\n\
5773             \x20   _ = x\n\
5774             \x20   n := 0\n\
5775             \x20   if n < 10 { n = n + 1 } else { n += 2 }\n\
5776             \x20   n++\n\
5777             \x20   _ = len(s)\n\
5778             \x20   switch n {\n\
5779             \x20   case 0: return 0\n\
5780             \x20   case 1: return 1\n\
5781             \x20   default: return n\n\
5782             \x20   }\n\
5783             }\n",
5784            "foo.go",
5785            |metric| {
5786                assert_eq!(metric.abc.assignments_sum(), 6);
5787                assert_eq!(metric.abc.branches_sum(), 1);
5788                assert_eq!(metric.abc.conditions_sum(), 4);
5789                insta::assert_json_snapshot!(metric.abc);
5790            },
5791        );
5792    }
5793
5794    #[test]
5795    fn go_if_multiple_conditions() {
5796        // Fitzpatrick Rule 7 walker fan-out (issue #403). Mirrors
5797        // `rust_if_multiple_conditions`.
5798        check_metrics::<GoParser>(
5799            "package p\n\
5800             func F(a, b, c, d bool) int {\n\
5801             \x20   if a || b || c || d { return 1 }    // +4c\n\
5802             \x20   if a && b && c { return 2 }         // +3c\n\
5803             \x20   if !a && !b { return 3 }            // +2c\n\
5804             \x20   return 0\n\
5805             }\n",
5806            "foo.go",
5807            |metric| {
5808                assert_eq!(metric.abc.conditions_sum(), 9);
5809                insta::assert_json_snapshot!(metric.abc);
5810            },
5811        );
5812    }
5813
5814    #[test]
5815    fn go_for_with_conditions() {
5816        // Go has no `while` or `do { … } while(…);` — the `for` loop
5817        // header is the sole condition slot. Each operand of the
5818        // `&&` / `||` chain in the for-condition counts as one
5819        // Fitzpatrick condition.
5820        check_metrics::<GoParser>(
5821            "package p\n\
5822             func F(a, b bool) {\n\
5823             \x20   for a || b { break }       // +2c\n\
5824             \x20   for a && !b { break }      // +2c\n\
5825             }\n",
5826            "foo.go",
5827            |metric| {
5828                assert_eq!(metric.abc.conditions_sum(), 4);
5829                insta::assert_json_snapshot!(metric.abc);
5830            },
5831        );
5832    }
5833
5834    #[test]
5835    fn go_for_bare_condition_counts() {
5836        // Regression for findings.md #1: `for true {}` / `for !ready {}`
5837        // are Go's only loop-condition slot. Pre-fix, the Phase-2B
5838        // dispatcher had no `G::ForStatement` arm, so bare-boolean
5839        // and `!`-wrapped `for` conditions silently reported zero.
5840        // `go_count_condition`'s terminal-bool / paren / unary filter
5841        // makes the arm safe across all three for-statement shapes:
5842        // bare condition, `for_clause` (init; cond; post), and
5843        // `range_clause` (the latter two fall through harmlessly).
5844        check_metrics::<GoParser>(
5845            "package p\n\
5846             func F(ready bool) {\n\
5847             \x20   for true { break }      // +1c\n\
5848             \x20   for !ready { break }    // +1c\n\
5849             \x20   for i := 0; i < 3; i++ { _ = i }    // +1c (the `<`)\n\
5850             }\n",
5851            "foo.go",
5852            |metric| {
5853                // `for true`: walker counts True (+1).
5854                // `for !ready`: walker on unary unwraps to `ready`
5855                //   (+1).
5856                // `for_clause` falls through go_count_condition with
5857                //   no count; the inner `i < 3` contributes 1 via the
5858                //   pre-existing LT/GT arm.
5859                // Total: 3.
5860                assert_eq!(metric.abc.conditions_sum(), 3);
5861                insta::assert_json_snapshot!(metric.abc);
5862            },
5863        );
5864    }
5865
5866    #[test]
5867    fn go_if_init_statement_condition_counts() {
5868        // Regression for the code-review finding: Go's
5869        // `if x := f(); x { ... }` init-statement form puts the
5870        // short-var declaration at child(1) and the condition at
5871        // child(2). Pre-fix, the dispatcher used child(1) and
5872        // counted zero conditions for this idiomatic Go shape.
5873        // The fix uses `child_by_field_name("condition")` which
5874        // returns the condition regardless of init presence.
5875        check_metrics::<GoParser>(
5876            "package p\nfunc F() { if x := g(); x { } }\n",
5877            "foo.go",
5878            |metric| {
5879                // `x` bare-identifier condition contributes 1
5880                // (Rule 6 — bare boolean identifier in if-condition).
5881                // `g()` call contributes 1 branch but no condition.
5882                assert_eq!(metric.abc.branches_sum(), 1);
5883                assert_eq!(metric.abc.conditions_sum(), 1);
5884                insta::assert_json_snapshot!(metric.abc);
5885            },
5886        );
5887    }
5888
5889    #[test]
5890    fn go_if_boolean_literal_condition() {
5891        check_metrics::<GoParser>(
5892            "package p\n\
5893             func F() {\n\
5894             \x20   if true {}                  // +1c\n\
5895             \x20   if !false {}                // +1c\n\
5896             }\n",
5897            "foo.go",
5898            |metric| {
5899                assert_eq!(metric.abc.conditions_sum(), 2);
5900                insta::assert_json_snapshot!(metric.abc);
5901            },
5902        );
5903    }
5904
5905    #[test]
5906    fn go_methods_arguments_with_conditions() {
5907        check_metrics::<GoParser>(
5908            "package p\n\
5909             func F(a, b bool) {\n\
5910             \x20   m(a, b)                     // +1b\n\
5911             \x20   m(!a, !b)                   // +1b +2c\n\
5912             }\n",
5913            "foo.go",
5914            |metric| {
5915                assert_eq!(metric.abc.branches_sum(), 2);
5916                assert_eq!(metric.abc.conditions_sum(), 2);
5917                insta::assert_json_snapshot!(metric.abc);
5918            },
5919        );
5920    }
5921
5922    #[test]
5923    fn go_return_with_conditions() {
5924        check_metrics::<GoParser>(
5925            "package p\n\
5926             func M1(z int) bool { return !(z >= 0) }\n\
5927             func M2(x bool) bool { return !x }\n\
5928             func M3(x, y bool) bool { return x && y }\n",
5929            "foo.go",
5930            |metric| {
5931                // M1: `>=` (1). `!(z >= 0)` walker on the unary
5932                //     doesn't reach a terminal — stops at the
5933                //     BinaryExpression z>=0 inside the parens. +1.
5934                // M2: walker on `!x` → 1.
5935                // M3: `&&` walker counts both → 2.
5936                // Sum: 1 + 1 + 2 = 4.
5937                assert_eq!(metric.abc.conditions_sum(), 4);
5938                insta::assert_json_snapshot!(metric.abc);
5939            },
5940        );
5941    }
5942
5943    #[test]
5944    fn go_short_circuit_with_boolean_literal_operand() {
5945        // `a && true` reports 2 conditions: one identifier, one
5946        // boolean literal. Confirms the terminal set includes
5947        // `True` / `False`.
5948        check_metrics::<GoParser>(
5949            "package p\nfunc F(a bool) bool { return a && true }\n",
5950            "foo.go",
5951            |metric| {
5952                assert_eq!(metric.abc.conditions_sum(), 2);
5953                insta::assert_json_snapshot!(metric.abc);
5954            },
5955        );
5956    }
5957
5958    // ----- Elixir -----
5959
5960    // No top-level Calls and no operators → all three vectors are
5961    // zero. Uses a bare expression rather than a `defmodule` wrapper
5962    // (which would itself be a Call → 1 branch). Confirms the
5963    // ElixirCode Abc trait is wired up and the metric emits.
5964    #[test]
5965    fn elixir_empty_unit_zero() {
5966        check_metrics::<ElixirParser>(":ok\n", "foo.ex", |metric| {
5967            assert_eq!(metric.abc.assignments_sum(), 0);
5968            assert_eq!(metric.abc.branches_sum(), 0);
5969            assert_eq!(metric.abc.conditions_sum(), 0);
5970            insta::assert_json_snapshot!(metric.abc);
5971        });
5972    }
5973
5974    // An empty `defmodule Foo do ... end` is itself ONE `Call` →
5975    // Documents that module-/function-defining macros (`defmodule`,
5976    // `def`, `defp`, `defmacro`, `defmacrop`) and declarative
5977    // directives (`alias`, `import`, `require`, `use`) are NOT
5978    // runtime dispatch and therefore do NOT inflate `branches`,
5979    // matching Cognitive's treatment.
5980    #[test]
5981    fn elixir_defmodule_is_zero_branches() {
5982        check_metrics::<ElixirParser>("defmodule Foo do\nend\n", "foo.ex", |metric| {
5983            assert_eq!(metric.abc.branches_sum(), 0);
5984            assert_eq!(metric.abc.assignments_sum(), 0);
5985            assert_eq!(metric.abc.conditions_sum(), 0);
5986            insta::assert_json_snapshot!(metric.abc);
5987        });
5988    }
5989
5990    // Pattern-match `=` counts as an assignment. Two bindings → A = 2.
5991    // `defmodule` and `def` are declarative-Call wrappers and are
5992    // filtered out of branches; the assertion focuses on assignments
5993    // so we only pin that vector.
5994    #[test]
5995    fn elixir_pattern_match_is_assignment() {
5996        check_metrics::<ElixirParser>(
5997            "defmodule Foo do\n  def f do\n    x = 1\n    y = x + 1\n    y\n  end\nend\n",
5998            "foo.ex",
5999            |metric| {
6000                assert_eq!(metric.abc.assignments_sum(), 2);
6001                insta::assert_json_snapshot!(metric.abc);
6002            },
6003        );
6004    }
6005
6006    // `|>` pipeline operator: each `|>` token contributes one branch.
6007    // Two `|>` ops → +2 from the pipe operator itself. Each pipeline
6008    // step also dispatches a Call (`String.upcase(...)`,
6009    // `String.trim(...)`) — these are wrapped inside the outer
6010    // pipeline Call tree, contributing additional Call branches.
6011    // The headline assertion confirms (a) `|>` is detected and (b)
6012    // pipeline steps are not silently dropped.
6013    #[test]
6014    fn elixir_pipeline_each_step_is_branch() {
6015        check_metrics::<ElixirParser>(
6016            "defmodule Foo do\n  def normalize(s) do\n    s |> String.trim() |> String.upcase()\n  end\nend\n",
6017            "foo.ex",
6018            |metric| {
6019                // Pipeline yields 2 `|>` branches plus Calls for
6020                // String.trim, String.upcase, and the outer pipeline
6021                // (which surfaces as a Call wrapping the binary
6022                // operator). `def` and `defmodule` are declarative
6023                // and excluded. Empirical total: B = 5.
6024                assert_eq!(metric.abc.branches_sum(), 5);
6025                assert_eq!(metric.abc.assignments_sum(), 0);
6026                insta::assert_json_snapshot!(metric.abc);
6027            },
6028        );
6029    }
6030
6031    // Comparison operators all count as conditions. Six comparisons
6032    // (`==`, `!=`, `<`, `>`, `<=`, `>=`) → C = 6.
6033    #[test]
6034    fn elixir_comparisons_are_conditions() {
6035        check_metrics::<ElixirParser>(
6036            "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",
6037            "foo.ex",
6038            |metric| {
6039                assert_eq!(metric.abc.conditions_sum(), 6);
6040                insta::assert_json_snapshot!(metric.abc);
6041            },
6042        );
6043    }
6044
6045    // Strict-equality operators `===` / `!==` count as conditions too.
6046    #[test]
6047    fn elixir_strict_equality_is_condition() {
6048        check_metrics::<ElixirParser>(
6049            "defmodule Foo do\n  def f(a, b) do\n    a === b or a !== b\n  end\nend\n",
6050            "foo.ex",
6051            |metric| {
6052                assert_eq!(metric.abc.conditions_sum(), 2);
6053                insta::assert_json_snapshot!(metric.abc);
6054            },
6055        );
6056    }
6057
6058    // Guard `when` clause counts as a condition. One `when` → +1.
6059    // `def f(x) when x > 0` also has `>` → +1, totalling 2.
6060    #[test]
6061    fn elixir_guard_when_is_condition() {
6062        check_metrics::<ElixirParser>(
6063            "defmodule Foo do\n  def f(x) when x > 0 do\n    :pos\n  end\nend\n",
6064            "foo.ex",
6065            |metric| {
6066                // when (+1) + > (+1) = 2
6067                assert_eq!(metric.abc.conditions_sum(), 2);
6068                insta::assert_json_snapshot!(metric.abc);
6069            },
6070        );
6071    }
6072
6073    // Keyword-shaped Calls (`case`, `cond`, `if`, `with`) each count
6074    // as one condition AND one branch. `case` here adds 1 condition
6075    // (the keyword Call) + 1 branch (the Call itself).
6076    #[test]
6077    fn elixir_case_is_condition_and_branch() {
6078        check_metrics::<ElixirParser>(
6079            "defmodule Foo do\n  def f(x) do\n    case x do\n      1 -> :one\n      _ -> :other\n    end\n  end\nend\n",
6080            "foo.ex",
6081            |metric| {
6082                // conditions: case → 1
6083                assert_eq!(metric.abc.conditions_sum(), 1);
6084                insta::assert_json_snapshot!(metric.abc);
6085            },
6086        );
6087    }
6088
6089    // `cond` is structurally identical to `case` for Abc.
6090    #[test]
6091    fn elixir_cond_is_condition() {
6092        check_metrics::<ElixirParser>(
6093            "defmodule Foo do\n  def f(x) do\n    cond do\n      x > 0 -> :pos\n      true -> :other\n    end\n  end\nend\n",
6094            "foo.ex",
6095            |metric| {
6096                // conditions: cond (+1) + > (+1) = 2
6097                assert_eq!(metric.abc.conditions_sum(), 2);
6098                insta::assert_json_snapshot!(metric.abc);
6099            },
6100        );
6101    }
6102
6103    // `for` is a comprehension/loop, NOT in the issue's condition
6104    // list. It is still a Call so it contributes one branch, but no
6105    // condition.
6106    #[test]
6107    fn elixir_for_is_branch_not_condition() {
6108        check_metrics::<ElixirParser>(
6109            "defmodule Foo do\n  def f(xs) do\n    for x <- xs, do: x * 2\n  end\nend\n",
6110            "foo.ex",
6111            |metric| {
6112                assert_eq!(metric.abc.conditions_sum(), 0);
6113                insta::assert_json_snapshot!(metric.abc);
6114            },
6115        );
6116    }
6117
6118    // Mixed shape, verified by hand: defmodule Call + def Call + if Call
6119    // + Call to side_effect/0 + assignment `x = 1` + comparison `x > 0`.
6120    // - Assignments: `x = 1` → A = 1.
6121    // - Branches: `defmodule` and `def` are declarative and excluded;
6122    //   `if` Call + `side_effect()` Call → 2 Calls, plus 0 `|>` → B = 2.
6123    // - Conditions: `if` keyword → 1, `x > 0` → 1 → C = 2.
6124    #[test]
6125    fn elixir_mixed_abc() {
6126        check_metrics::<ElixirParser>(
6127            "defmodule Foo do\n  def f do\n    x = 1\n    if x > 0 do\n      side_effect()\n    end\n  end\nend\n",
6128            "foo.ex",
6129            |metric| {
6130                assert_eq!(metric.abc.assignments_sum(), 1);
6131                assert_eq!(metric.abc.branches_sum(), 2);
6132                assert_eq!(metric.abc.conditions_sum(), 2);
6133                insta::assert_json_snapshot!(metric.abc);
6134            },
6135        );
6136    }
6137
6138    #[test]
6139    fn elixir_unary_conditions_in_chain() {
6140        // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
6141        // `&&` / `||` chain is one condition. For `if a && b || c`: the
6142        // `if` keyword Call contributes 1 condition, and the walker adds
6143        // a, b, c → 3. expected: 4 conditions, consistent with the
6144        // function's cyclomatic complexity of 4 (base 1 + if + && + ||).
6145        check_metrics::<ElixirParser>(
6146            "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",
6147            "foo.ex",
6148            |metric| {
6149                assert_eq!(metric.abc.conditions_sum(), 4);
6150            },
6151        );
6152    }
6153
6154    #[test]
6155    fn elixir_comparison_operands_add_nothing() {
6156        // Isolation check: comparison operands of a `&&` chain are nested
6157        // `binary_operator` nodes, not bare boolean leaves, so the walker
6158        // adds nothing. expected: 3 = `if` (1) + `>` (1) + `>` (1); the
6159        // `&&` walker contributes 0.
6160        check_metrics::<ElixirParser>(
6161            "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",
6162            "foo.ex",
6163            |metric| {
6164                assert_eq!(metric.abc.conditions_sum(), 3);
6165            },
6166        );
6167    }
6168
6169    #[test]
6170    fn elixir_keyword_and_or_chain_counts_operands() {
6171        // The keyword forms `and` / `or` get the same Rule 9 treatment as
6172        // `&&` / `||`. expected: 4 = `if` (1) + operands a, b, c (3).
6173        check_metrics::<ElixirParser>(
6174            "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",
6175            "foo.ex",
6176            |metric| {
6177                assert_eq!(metric.abc.conditions_sum(), 4);
6178            },
6179        );
6180    }
6181
6182    // ----- C++ -----
6183
6184    #[test]
6185    fn cpp_empty_unit_zero() {
6186        // No code → A=B=C=0. Wires up the trait and exercises the
6187        // per-language compute reachability.
6188        check_metrics::<CppParser>("", "empty.cpp", |metric| {
6189            assert_eq!(metric.abc.assignments_sum(), 0);
6190            assert_eq!(metric.abc.branches_sum(), 0);
6191            assert_eq!(metric.abc.conditions_sum(), 0);
6192            insta::assert_json_snapshot!(metric.abc);
6193        });
6194    }
6195
6196    #[test]
6197    fn cpp_plain_and_compound_assignments_count() {
6198        // `int x = 0` is an `init_declarator` carrying an `=` token
6199        // and counts as 1 (post-#393: the literal Fitzpatrick rule
6200        // counts every `=` operator, matching the JS impl's
6201        // `let x = 5` treatment). `x = 5`, `x += 2`, `x = 7` all
6202        // parse as `assignment_expression` → 3. Total A = 4.
6203        check_metrics::<CppParser>(
6204            "void f() { int x = 0; x = 5; x += 2; x = 7; }",
6205            "foo.cpp",
6206            |metric| {
6207                assert_eq!(metric.abc.assignments_sum(), 4);
6208                assert_eq!(metric.abc.branches_sum(), 0);
6209                assert_eq!(metric.abc.conditions_sum(), 0);
6210                insta::assert_json_snapshot!(metric.abc);
6211            },
6212        );
6213    }
6214
6215    #[test]
6216    fn cpp_increment_and_decrement_count_as_assignment() {
6217        // `x++` / `--x` / prefix and postfix forms each parse as
6218        // `update_expression` and count as 1 assignment per
6219        // Fitzpatrick — 4. `int x = 0` (init_declarator with `=`)
6220        // adds 1 (post-#393). Total A = 5.
6221        check_metrics::<CppParser>(
6222            "void f() { int x = 0; x++; --x; ++x; x--; }",
6223            "foo.cpp",
6224            |metric| {
6225                assert_eq!(metric.abc.assignments_sum(), 5);
6226                insta::assert_json_snapshot!(metric.abc);
6227            },
6228        );
6229    }
6230
6231    #[test]
6232    fn cpp_init_declarators_count_as_assignments() {
6233        // Issue #393 regression: `int a=1;`, `int b=2;`, `int c=a+b;`,
6234        // `int d=0;` are all `init_declarator` nodes with `=` → 4
6235        // assignments. `d=5;` is one plain `assignment_expression`,
6236        // `d+=1;` is one compound. Total A = 4 + 1 + 1 = 6.
6237        check_metrics::<CppParser>(
6238            "void f() { int a=1; int b=2; int c=a+b; int d=0; d=5; d+=1; }",
6239            "foo.cpp",
6240            |metric| {
6241                assert_eq!(metric.abc.assignments_sum(), 6);
6242                insta::assert_json_snapshot!(metric.abc);
6243            },
6244        );
6245    }
6246
6247    #[test]
6248    fn cpp_declaration_without_initializer_does_not_count() {
6249        // `int a;` parses as a plain declarator inside `declaration`,
6250        // NOT an `init_declarator` (the latter only appears when an
6251        // initializer is present). Regression test for issue #393:
6252        // un-initialised declarations contribute zero to A.
6253        check_metrics::<CppParser>("void f() { int a; a = 5; }", "foo.cpp", |metric| {
6254            // Only `a = 5` (assignment_expression) → A = 1.
6255            assert_eq!(metric.abc.assignments_sum(), 1);
6256            insta::assert_json_snapshot!(metric.abc);
6257        });
6258    }
6259
6260    #[test]
6261    fn cpp_init_declarator_brace_paren_init_does_not_count() {
6262        // `init_declarator` has two grammar forms: `declarator = value`
6263        // (the `=` form) and `declarator argument_list_or_initializer_list`
6264        // (the `int x(5);` / `int x{5};` direct-init forms). Only the
6265        // first form contains an `=` token, so only it should count.
6266        // Regression test pinning that distinction so that
6267        // refactorings of the init_declarator arm don't accidentally
6268        // start counting direct-init too.
6269        check_metrics::<CppParser>(
6270            "void f() { int x(5); int y{7}; x = 1; }",
6271            "foo.cpp",
6272            |metric| {
6273                // Only `x = 1` (assignment_expression) → A = 1.
6274                assert_eq!(metric.abc.assignments_sum(), 1);
6275                insta::assert_json_snapshot!(metric.abc);
6276            },
6277        );
6278    }
6279
6280    #[test]
6281    fn cpp_calls_are_branches() {
6282        // Free call + member-fn call (parses as `call_expression` with
6283        // a `field_expression` callee) + `new` allocation. All three
6284        // are branches → B = 3. `auto* p = new int(5)` is also an
6285        // `init_declarator` with `=` so it contributes one assignment
6286        // (post-#393); the snapshot pins that magnitude.
6287        check_metrics::<CppParser>(
6288            "struct S { void m(); }; void g(); void f() { g(); S s; s.m(); auto* p = new int(5); }",
6289            "foo.cpp",
6290            |metric| {
6291                assert_eq!(metric.abc.branches_sum(), 3);
6292                assert_eq!(metric.abc.assignments_sum(), 1);
6293                insta::assert_json_snapshot!(metric.abc);
6294            },
6295        );
6296    }
6297
6298    #[test]
6299    fn cpp_comparisons_count_conditions() {
6300        // `<`, `>`, `<=`, `>=`, `==`, `!=`, and the C++20 spaceship
6301        // `<=>` each contribute one condition. The `||` short-
6302        // circuits add 0 (Fitzpatrick Rule 5, issue #395). Six
6303        // comparisons in the `||` chain plus `<=>` (1) plus the
6304        // outer `== 0` (1) → C = 8.
6305        check_metrics::<CppParser>(
6306            "#include <compare>\n\
6307             bool f(int a, int b) {\n\
6308                 return a < b || a > b || a <= b || a >= b || a == b || a != b || (a <=> b) == 0;\n\
6309             }\n",
6310            "foo.cpp",
6311            |metric| {
6312                // `<`, `>`, `<=`, `>=`, `==`, `!=` → 6 comparisons
6313                // from the chained `||` expression. `(a <=> b) == 0`
6314                // adds the spaceship `<=>` (1) + the outer `== 0`
6315                // (1) → 8 total. The six `||` short-circuits add 0
6316                // (Fitzpatrick Rule 5; issue #395).
6317                assert_eq!(metric.abc.conditions_sum(), 8);
6318                insta::assert_json_snapshot!(metric.abc);
6319            },
6320        );
6321    }
6322
6323    #[test]
6324    fn cpp_short_circuit_ops_not_counted_directly() {
6325        // `&&` and `||` do NOT count on their own (see the
6326        // module-level `Stats` doc-comment; #395). Phase-2 walker
6327        // counts each operand of a logical chain once (#403), but
6328        // when every operand is itself a relational expression
6329        // (`a == b`, `a > 0`, `b < 0`) the walker doesn't add
6330        // anything on top of the existing comparison-token tally
6331        // — relational sub-expressions are not in
6332        // `cpp_bool_terminal_kinds!()` and `cpp_inspect_container`
6333        // does not recurse into them.
6334        check_metrics::<CppParser>(
6335            "bool f(int a, int b) { return a == b && a > 0 || b < 0; }",
6336            "foo.cpp",
6337            |metric| {
6338                // == 1, > 1, < 1; the walker on && and || finds
6339                // BinaryExpression operands (not terminal-bool) and
6340                // adds nothing. Total: 3.
6341                assert_eq!(metric.abc.conditions_sum(), 3);
6342                insta::assert_json_snapshot!(metric.abc);
6343            },
6344        );
6345    }
6346
6347    #[test]
6348    fn cpp_generic_brackets_not_conditions() {
6349        // `<` / `>` in `std::vector<int>` are `template_argument_list`
6350        // delimiters, NOT comparison operators. The `binary_expression`
6351        // parent check must filter them out → C = 0.
6352        check_metrics::<CppParser>(
6353            "#include <vector>\nstd::vector<int> f() { return std::vector<int>{}; }",
6354            "foo.cpp",
6355            |metric| {
6356                assert_eq!(metric.abc.conditions_sum(), 0);
6357                insta::assert_json_snapshot!(metric.abc);
6358            },
6359        );
6360    }
6361
6362    #[test]
6363    fn cpp_else_and_ternary_count_conditions() {
6364        // `if (cond) ... else ...` + ternary `cond ? a : b`. The
6365        // `if`-keyword is NOT a condition (its condition is the
6366        // comparison inside, which counts separately). `else` adds 1,
6367        // `?` adds 1. Two comparisons (`a > b`, `b < 0`) → 2. Total = 4.
6368        check_metrics::<CppParser>(
6369            "int f(int a, int b) {\n\
6370                 if (a > b) { return a; } else { return b; }\n\
6371                 return (b < 0) ? -b : b;\n\
6372             }\n",
6373            "foo.cpp",
6374            |metric| {
6375                assert_eq!(metric.abc.conditions_sum(), 4);
6376                insta::assert_json_snapshot!(metric.abc);
6377            },
6378        );
6379    }
6380
6381    #[test]
6382    fn cpp_switch_cases_count_default_excluded() {
6383        // `case 1`, `case 2` → 2 conditions. `default` is intentionally
6384        // excluded (the unconditional fallthrough, mirroring cyclomatic's
6385        // `Case`-only count). Since #469 every C-family language —
6386        // Java, C#, Groovy, JS, TS — agrees on this; C++ already did.
6387        // C = 2.
6388        check_metrics::<CppParser>(
6389            "void f(int x) {\n\
6390                 switch (x) {\n\
6391                     case 1: break;\n\
6392                     case 2: break;\n\
6393                     default: break;\n\
6394                 }\n\
6395             }\n",
6396            "foo.cpp",
6397            |metric| {
6398                assert_eq!(metric.abc.conditions_sum(), 2);
6399                insta::assert_json_snapshot!(metric.abc);
6400            },
6401        );
6402    }
6403
6404    #[test]
6405    fn cpp_try_catch_count_conditions() {
6406        // `try` and `catch` each add one condition (Fitzpatrick's rule;
6407        // Java's impl above counts them too).
6408        check_metrics::<CppParser>(
6409            "void f() { try { } catch (int) { } catch (...) { } }",
6410            "foo.cpp",
6411            |metric| {
6412                // 1 `try` + 2 `catch` arms = 3.
6413                assert_eq!(metric.abc.conditions_sum(), 3);
6414                insta::assert_json_snapshot!(metric.abc);
6415            },
6416        );
6417    }
6418
6419    #[test]
6420    fn cpp_complex_function_abc() {
6421        // Mixed-shape regression: assignments, calls, conditions,
6422        // ternary, switch, new. Verified by hand:
6423        // - assignments: `int x = 0` (init_declarator with `=`),
6424        //   `x = 5`, `x += 2`, `x++`, `x = (a > b) ? a : b`, `x = b`,
6425        //   `auto* p = new int(5)` (init_declarator with `=`) → A = 7
6426        //   (post-#393: every `=` in an init_declarator counts).
6427        // - branches: `f(a, b)` self-call + `new int(5)` → B = 2.
6428        // - conditions: `a == b` (1) + `a > 0` (1) inside the if;
6429        //   `&&` itself is NOT a condition (Fitzpatrick Rule 5,
6430        //   issue #395). `a > b` (1) + `?` (1) in the ternary.
6431        //   `else` (1, from the `else if` keyword) + `a < b` (1)
6432        //   in the else-if. `!x` contributes 1 via the unary-
6433        //   conditional walker (Fitzpatrick Rule 9, issue #403):
6434        //   the `||` walker treats `!x` as a unary boolean operand
6435        //   and counts the wrapped Identifier once. `case 1`,
6436        //   `case 2` → 2. `default` excluded. Total C = 9.
6437        check_metrics::<CppParser>(
6438            "int f(int a, int b) {\n\
6439                 int x = 0;\n\
6440                 x = 5;\n\
6441                 x += 2;\n\
6442                 x++;\n\
6443                 if (a == b && a > 0) {\n\
6444                     x = (a > b) ? a : b;\n\
6445                 } else if (a < b || !x) {\n\
6446                     x = b;\n\
6447                 }\n\
6448                 switch (x) {\n\
6449                     case 1: break;\n\
6450                     case 2: break;\n\
6451                     default: break;\n\
6452                 }\n\
6453                 auto* p = new int(5);\n\
6454                 return f(a, b);\n\
6455             }\n",
6456            "foo.cpp",
6457            |metric| {
6458                assert_eq!(metric.abc.assignments_sum(), 7);
6459                assert_eq!(metric.abc.branches_sum(), 2);
6460                assert_eq!(metric.abc.conditions_sum(), 9);
6461                insta::assert_json_snapshot!(metric.abc);
6462            },
6463        );
6464    }
6465
6466    #[test]
6467    fn cpp_if_multiple_conditions() {
6468        // Fitzpatrick Rule 9 walker (issue #403): each operand of a
6469        // `&&` / `||` chain is one condition.
6470        check_metrics::<CppParser>(
6471            "void f(bool a, bool b, bool c, bool d) {\n\
6472             \x20   if (a || b || c || d) {}        // +4c\n\
6473             \x20   if (a && b && c) {}             // +3c\n\
6474             \x20   if (!a && !b) {}                // +2c\n\
6475             }\n",
6476            "foo.cpp",
6477            |metric| {
6478                assert_eq!(metric.abc.conditions_sum(), 9);
6479                insta::assert_json_snapshot!(metric.abc);
6480            },
6481        );
6482    }
6483
6484    #[test]
6485    fn cpp_while_and_do_while_conditions() {
6486        // Exercise both the WhileStatement and DoStatement arms via
6487        // the walker on the `&&` / `||` tokens inside their parens.
6488        check_metrics::<CppParser>(
6489            "void f(bool a, bool b) {\n\
6490             \x20   while (a || b) {}              // +2c\n\
6491             \x20   do {} while (a && !b);         // +2c\n\
6492             }\n",
6493            "foo.cpp",
6494            |metric| {
6495                assert_eq!(metric.abc.conditions_sum(), 4);
6496                insta::assert_json_snapshot!(metric.abc);
6497            },
6498        );
6499    }
6500
6501    #[test]
6502    fn cpp_if_constexpr_condition_counts() {
6503        // Regression for the code-review finding: C++ `if constexpr
6504        // (cond)` puts the `constexpr` keyword at child(1) and the
6505        // condition_clause at child(2). Pre-fix, the dispatcher used
6506        // child(1) and counted zero conditions for the `constexpr`
6507        // form. The fix uses `child_by_field_name("condition")`
6508        // which returns the condition_clause regardless of the
6509        // optional `constexpr` keyword.
6510        check_metrics::<CppParser>(
6511            "template <int N> void f() {\n\
6512             \x20   if constexpr (true) { }      // +1c\n\
6513             \x20   if (false) { }               // +1c\n\
6514             }\n",
6515            "foo.cpp",
6516            |metric| {
6517                assert_eq!(metric.abc.conditions_sum(), 2);
6518                insta::assert_json_snapshot!(metric.abc);
6519            },
6520        );
6521    }
6522
6523    #[test]
6524    fn cpp_cast_expression_in_logical_chain_counts() {
6525        // Regression for findings.md round-2 #1 (C++):
6526        // `if ((bool)ptr && ready) {}` had the `||` walker missing
6527        // the `(bool)ptr` operand because `CastExpression` was not
6528        // in `cpp_bool_terminal_kinds!()`. Mirrors C#'s
6529        // `csharp_bool_terminal_kinds!()` which lists
6530        // `CastExpression` (lesson 19, #372).
6531        check_metrics::<CppParser>(
6532            "void f(void* ptr, bool ready) { if ((bool)ptr && ready) { } }\n",
6533            "foo.cpp",
6534            |metric| {
6535                // `&&` walker counts both operands: `(bool)ptr` (1)
6536                // and `ready` (1). Total: 2.
6537                assert_eq!(metric.abc.conditions_sum(), 2);
6538                insta::assert_json_snapshot!(metric.abc);
6539            },
6540        );
6541    }
6542
6543    #[test]
6544    fn cpp_qualified_identifier_condition_counts() {
6545        // Regression for findings.md #3 (C++): tree-sitter-cpp emits
6546        // `qualified_identifier` under four kind_ids (573..576) per
6547        // the production-rule path; runtime kind for `ns::flag` is
6548        // 574 (`QualifiedIdentifier2`). Pre-fix the
6549        // `cpp_bool_terminal_kinds!()` macro listed neither the
6550        // primary nor any alias, so `if (n::flag) {}` reported zero
6551        // conditions. The macro now includes all four variants
6552        // (lesson #2).
6553        check_metrics::<CppParser>(
6554            "namespace n { extern bool flag; }\n\
6555             void f() { if (n::flag) { } }\n",
6556            "foo.cpp",
6557            |metric| {
6558                assert_eq!(metric.abc.conditions_sum(), 1);
6559                insta::assert_json_snapshot!(metric.abc);
6560            },
6561        );
6562    }
6563
6564    #[test]
6565    fn cpp_if_boolean_literal_condition() {
6566        check_metrics::<CppParser>(
6567            "void f() {\n\
6568             \x20   if (true) {}                 // +1c\n\
6569             \x20   if (!false) {}               // +1c\n\
6570             \x20   while (true) {}              // +1c\n\
6571             \x20   do {} while (false);         // +1c\n\
6572             }\n",
6573            "foo.cpp",
6574            |metric| {
6575                assert_eq!(metric.abc.conditions_sum(), 4);
6576                insta::assert_json_snapshot!(metric.abc);
6577            },
6578        );
6579    }
6580
6581    #[test]
6582    fn cpp_methods_arguments_with_conditions() {
6583        check_metrics::<CppParser>(
6584            "void f(bool a, bool b) {\n\
6585             \x20   m(a, b);                     // +1b\n\
6586             \x20   m(!a, !b);                   // +1b +2c\n\
6587             }\n",
6588            "foo.cpp",
6589            |metric| {
6590                assert_eq!(metric.abc.branches_sum(), 2);
6591                assert_eq!(metric.abc.conditions_sum(), 2);
6592                insta::assert_json_snapshot!(metric.abc);
6593            },
6594        );
6595    }
6596
6597    #[test]
6598    fn cpp_return_with_conditions() {
6599        check_metrics::<CppParser>(
6600            "bool m1(int z) { return !(z >= 0); }\n\
6601             bool m2(bool x) { return (((!x))); }\n\
6602             bool m3(bool x, bool y) { return x && y; }\n",
6603            "foo.cpp",
6604            |metric| {
6605                // m1: !(z >= 0) → `>=` (1). `!` wraps a paren'd
6606                //     BinaryExpression — inspect_container reaches
6607                //     the inner BinaryExpression and stops, no
6608                //     walker count. +1.
6609                // m2: (((!x))) → ReturnStatement → inspect_container
6610                //     unwraps three parens + one unary → reaches `x`
6611                //     in has_boolean_content=true (seeded by the
6612                //     unary `!`). +1.
6613                // m3: x && y → `&&` walker counts both → +2.
6614                // Sum: 1 + 1 + 2 = 4.
6615                assert_eq!(metric.abc.conditions_sum(), 4);
6616                insta::assert_json_snapshot!(metric.abc);
6617            },
6618        );
6619    }
6620
6621    #[test]
6622    fn cpp_short_circuit_with_boolean_literal_operand() {
6623        // `a && true` reports 2 conditions: one for the identifier
6624        // operand, one for the `True` literal operand.
6625        check_metrics::<CppParser>(
6626            "bool f(bool a) { return a && true; }\n",
6627            "foo.cpp",
6628            |metric| {
6629                assert_eq!(metric.abc.conditions_sum(), 2);
6630                insta::assert_json_snapshot!(metric.abc);
6631            },
6632        );
6633    }
6634
6635    #[test]
6636    fn javascript_empty_unit_zero() {
6637        // No code → A=B=C=0. Wires up the trait and exercises the
6638        // per-language compute reachability.
6639        check_metrics::<JavascriptParser>("", "empty.js", |metric| {
6640            assert_eq!(metric.abc.assignments_sum(), 0);
6641            assert_eq!(metric.abc.branches_sum(), 0);
6642            assert_eq!(metric.abc.conditions_sum(), 0);
6643            insta::assert_json_snapshot!(metric.abc);
6644        });
6645    }
6646
6647    #[test]
6648    fn javascript_plain_and_compound_assignments_count() {
6649        // `let` / `var` declarations behave like TypeScript: the `Var`
6650        // sentinel is pushed but only `const` suppresses the
6651        // initializer `=`. So `let x = 0` does count as A=+1; only
6652        // `const PI = 3.14` would be elided. Plain `x = 5`, `x += 2`,
6653        // `x = 7` all count → A = 4 total here.
6654        check_metrics::<JavascriptParser>(
6655            "function f() { let x = 0; x = 5; x += 2; x = 7; }",
6656            "foo.js",
6657            |metric| {
6658                assert_eq!(metric.abc.assignments_sum(), 4);
6659                assert_eq!(metric.abc.branches_sum(), 0);
6660                assert_eq!(metric.abc.conditions_sum(), 0);
6661                insta::assert_json_snapshot!(metric.abc);
6662            },
6663        );
6664    }
6665
6666    #[test]
6667    fn javascript_const_initializer_not_assignment() {
6668        // `const PI = 3.14` must NOT count as an assignment — the
6669        // `Const` sentinel suppresses the initializer `=`. `let x = 1`
6670        // and `var y = 2` still count (matches the TS impl: only
6671        // `const` suppresses).
6672        check_metrics::<JavascriptParser>(
6673            "function f() { const PI = 3.14; let x = 1; var y = 2; x = 9; }",
6674            "foo.js",
6675            |metric| {
6676                // `const PI` suppressed; `let x = 1`, `var y = 2`,
6677                // `x = 9` all count → A = 3.
6678                assert_eq!(metric.abc.assignments_sum(), 3);
6679                insta::assert_json_snapshot!(metric.abc);
6680            },
6681        );
6682    }
6683
6684    #[test]
6685    fn javascript_increment_and_decrement_count_as_assignment() {
6686        // `x++` (post) and `--x` (pre) both update an lvalue and so
6687        // count as assignments. Combined with the `let x = 0`
6688        // initializer (which counts under the JS/TS sentinel rule —
6689        // only `const` suppresses), A = 3.
6690        check_metrics::<JavascriptParser>(
6691            "function f() { let x = 0; x++; --x; }",
6692            "foo.js",
6693            |metric| {
6694                assert_eq!(metric.abc.assignments_sum(), 3);
6695                insta::assert_json_snapshot!(metric.abc);
6696            },
6697        );
6698    }
6699
6700    #[test]
6701    fn javascript_calls_are_branches() {
6702        // `g(1)` is a `call_expression` → B = 1. `new Foo(2)` is a
6703        // `new_expression` → B = 1. Total B = 2.
6704        check_metrics::<JavascriptParser>(
6705            "function f() { g(1); new Foo(2); }",
6706            "foo.js",
6707            |metric| {
6708                assert_eq!(metric.abc.branches_sum(), 2);
6709                assert_eq!(metric.abc.conditions_sum(), 0);
6710                insta::assert_json_snapshot!(metric.abc);
6711            },
6712        );
6713    }
6714
6715    #[test]
6716    fn javascript_comparisons_count_conditions() {
6717        // `==`, `===`, `!=`, `!==`, `<`, `>`, `<=`, `>=` each count
6718        // once. The `&&` / `||` short-circuit operators are NOT
6719        // counted as conditions in this impl (matches the TS
6720        // precedent — short-circuit ops are folded into the
6721        // surrounding `if` / control-flow arm, not separately).
6722        // Total C = 8.
6723        check_metrics::<JavascriptParser>(
6724            "function f(a, b) { return a == b && a === b && a != b && a !== b && a < b && a > b && a <= b && a >= b; }",
6725            "foo.js",
6726            |metric| {
6727                assert_eq!(metric.abc.conditions_sum(), 8);
6728                insta::assert_json_snapshot!(metric.abc);
6729            },
6730        );
6731    }
6732
6733    #[test]
6734    fn javascript_number_truthy_condition_counts() {
6735        // Regression for #772: JS treats every non-zero number as
6736        // truthy, so `while (5)` and `x && 5` should each count their
6737        // numeric literal as a Fitzpatrick unary condition. Pre-fix
6738        // `javascript_bool_terminal_kinds!()` listed `True` / `False`
6739        // but omitted `Number`, so the walker dropped every numeric-
6740        // truthy operand (mirrors the Lua `Number` fix).
6741        check_metrics::<JavascriptParser>(
6742            "function f(x) { while (5) {} return x && 5; }",
6743            "foo.js",
6744            |metric| {
6745                // `while (5)` → Number literal (+1). `x && 5` → both
6746                // operands count: identifier `x` (+1), Number `5` (+1).
6747                // Total: 3.
6748                assert_eq!(metric.abc.conditions_sum(), 3);
6749                insta::assert_json_snapshot!(metric.abc);
6750            },
6751        );
6752    }
6753
6754    #[test]
6755    fn typescript_number_truthy_condition_counts() {
6756        // Regression for #772: TS shares the JS truthy semantics. The
6757        // numeric *literal* `5` (kind `Number`) counts; the type-keyword
6758        // `number` (kind `Number2`, the `predefined_type`) must not —
6759        // see `typescript_bool_terminal_kinds!`.
6760        check_metrics::<TypescriptParser>(
6761            "function f(x: number) { while (5) {} return x && 5; }",
6762            "foo.ts",
6763            |metric| {
6764                // `while (5)` → +1; `x && 5` → `x` (+1) + `5` (+1).
6765                // Total: 3. The `: number` annotation contributes 0.
6766                assert_eq!(metric.abc.conditions_sum(), 3);
6767                insta::assert_json_snapshot!(metric.abc);
6768            },
6769        );
6770    }
6771
6772    #[test]
6773    fn javascript_nullish_coalescing_counts_condition() {
6774        // `a ?? b` is one nullish-coalescing operator → C = 1.
6775        check_metrics::<JavascriptParser>(
6776            "function f(a, b) { return a ?? b; }",
6777            "foo.js",
6778            |metric| {
6779                assert_eq!(metric.abc.conditions_sum(), 1);
6780                insta::assert_json_snapshot!(metric.abc);
6781            },
6782        );
6783    }
6784
6785    #[test]
6786    fn javascript_else_ternary_case_default_try_catch() {
6787        // `else`, `?` (ternary), `case`, `try`, `catch` all count.
6788        // `default` is the unconditional fallthrough → +0 (#469).
6789        // With the comparisons:
6790        //   - `a > 0` → 1
6791        //   - `else` opens an else_clause → 1
6792        //   - `?` ternary → 1
6793        //   - `case 1` → 1
6794        //   - `default` → 0 (fallthrough, #469)
6795        //   - `try` + `catch` → 2
6796        // Total C = 6.
6797        check_metrics::<JavascriptParser>(
6798            "function f(a) { if (a > 0) {} else {} let x = a ? 1 : 2; switch (x) { case 1: break; default: break; } try { } catch (e) { } }",
6799            "foo.js",
6800            |metric| {
6801                assert_eq!(metric.abc.conditions_sum(), 6);
6802                insta::assert_json_snapshot!(metric.abc);
6803            },
6804        );
6805    }
6806
6807    #[test]
6808    fn javascript_instanceof_counts_condition() {
6809        // `x instanceof Foo` is a binary expression whose operator is
6810        // the `instanceof` keyword token → C = 1.
6811        check_metrics::<JavascriptParser>(
6812            "function f(x) { return x instanceof Foo; }",
6813            "foo.js",
6814            |metric| {
6815                assert_eq!(metric.abc.conditions_sum(), 1);
6816                insta::assert_json_snapshot!(metric.abc);
6817            },
6818        );
6819    }
6820
6821    #[test]
6822    fn javascript_complex_function_abc() {
6823        // Mixed-shape regression. Verified by hand:
6824        // - assignments: `let x = 0` (Var sentinel does not suppress)
6825        //   + `x = 5`, `x += 2`, `x++`, `x = (a>b)?a:b`, `x = b`,
6826        //   `let p = ...` (Var sentinel) → A = 7.
6827        // - branches: `f(a, b)` self-call + `new Bar()` → B = 2.
6828        // - conditions: `a == b`, `a > 0` → 2 inside the if header
6829        //   (`&&` is not counted directly). `else` (1) + `a > b`,
6830        //   `?` → 2 in the ternary. `a < b` → 1 in the else-if.
6831        //   `!x` → 1 from the Fitzpatrick Rule 9 walker on `||`
6832        //   (issue #403): the wrapped Identifier counts once.
6833        //   `case 1` → 1 in the switch; `default` → 0 (fallthrough,
6834        //   #469). Total C = 8.
6835        check_metrics::<JavascriptParser>(
6836            "function f(a, b) {\n\
6837                 let x = 0;\n\
6838                 x = 5;\n\
6839                 x += 2;\n\
6840                 x++;\n\
6841                 if (a == b && a > 0) {\n\
6842                     x = (a > b) ? a : b;\n\
6843                 } else if (a < b || !x) {\n\
6844                     x = b;\n\
6845                 }\n\
6846                 switch (x) {\n\
6847                     case 1: break;\n\
6848                     default: break;\n\
6849                 }\n\
6850                 let p = new Bar();\n\
6851                 return f(a, b);\n\
6852             }\n",
6853            "foo.js",
6854            |metric| {
6855                assert_eq!(metric.abc.assignments_sum(), 7);
6856                assert_eq!(metric.abc.branches_sum(), 2);
6857                assert_eq!(metric.abc.conditions_sum(), 8);
6858                insta::assert_json_snapshot!(metric.abc);
6859            },
6860        );
6861    }
6862
6863    #[test]
6864    fn mozjs_complex_function_abc() {
6865        // Mozjs shares JavaScript's expression / statement vocabulary;
6866        // the `js_abc_compute!` macro expands identical token-level
6867        // rules for both. This test pins parity against the JS impl.
6868        check_metrics::<MozjsParser>(
6869            "function f(a, b) {\n\
6870                 let x = 0;\n\
6871                 x = 5;\n\
6872                 x += 2;\n\
6873                 x++;\n\
6874                 if (a == b && a > 0) {\n\
6875                     x = (a > b) ? a : b;\n\
6876                 } else if (a < b || !x) {\n\
6877                     x = b;\n\
6878                 }\n\
6879                 switch (x) {\n\
6880                     case 1: break;\n\
6881                     default: break;\n\
6882                 }\n\
6883                 let p = new Bar();\n\
6884                 return f(a, b);\n\
6885             }\n",
6886            "foo.js",
6887            |metric| {
6888                assert_eq!(metric.abc.assignments_sum(), 7);
6889                assert_eq!(metric.abc.branches_sum(), 2);
6890                assert_eq!(metric.abc.conditions_sum(), 8);
6891                insta::assert_json_snapshot!(metric.abc);
6892            },
6893        );
6894    }
6895
6896    // ----- JS / TS / Tsx / Mozjs Phase-2B condition slots -----
6897
6898    #[test]
6899    fn javascript_await_expression_condition_counts() {
6900        // Regression for findings.md round-2 #2 (JS):
6901        // `if (await ready()) {}` parses with `await_expression` as
6902        // the condition node inside the `parenthesized_expression`.
6903        // `javascript_inspect_container` unwraps the paren but the
6904        // await child was not in the terminal-bool set, so the
6905        // walker broke without counting. Mirrors C# (lesson 19).
6906        check_metrics::<JavascriptParser>(
6907            "async function ready() { return true; }\n\
6908             async function f() { if (await ready()) { } }\n",
6909            "foo.js",
6910            |metric| {
6911                assert_eq!(metric.abc.branches_sum(), 1);
6912                assert_eq!(metric.abc.conditions_sum(), 1);
6913                insta::assert_json_snapshot!(metric.abc);
6914            },
6915        );
6916    }
6917
6918    #[test]
6919    fn javascript_member_expression_condition_counts() {
6920        // Regression for findings.md #3 (JS-family): tree-sitter-
6921        // javascript emits `member_expression` under three kind_ids
6922        // (191 primary, 208, 228 — `MemberExpression2/3`) depending
6923        // on the production rule path. The verifier in this audit
6924        // confirmed runtime kind for `o.x` is 208. Pre-fix the
6925        // shared `js_family_bool_terminal_kinds!()` macro listed
6926        // only the primary, so every `if (o.x) {}` / `o.x && o.y`
6927        // condition silently reported zero. The per-language macro
6928        // now includes all three aliases (lesson #2).
6929        check_metrics::<JavascriptParser>(
6930            "function f(o) {\n\
6931             \x20   if (o.x) {}                  // +1c\n\
6932             \x20   return o.x && o.y;           // +2c (walker on &&)\n\
6933             }\n",
6934            "foo.js",
6935            |metric| {
6936                assert_eq!(metric.abc.conditions_sum(), 3);
6937                insta::assert_json_snapshot!(metric.abc);
6938            },
6939        );
6940    }
6941
6942    #[test]
6943    fn javascript_if_boolean_literal_condition() {
6944        check_metrics::<JavascriptParser>(
6945            "function f() {\n\
6946             \x20   if (true) {}                 // +1c\n\
6947             \x20   if (!false) {}               // +1c\n\
6948             \x20   while (true) {}              // +1c\n\
6949             \x20   do {} while (false);         // +1c\n\
6950             }\n",
6951            "foo.js",
6952            |metric| {
6953                assert_eq!(metric.abc.conditions_sum(), 4);
6954                insta::assert_json_snapshot!(metric.abc);
6955            },
6956        );
6957    }
6958
6959    #[test]
6960    fn javascript_methods_arguments_with_conditions() {
6961        check_metrics::<JavascriptParser>(
6962            "function f(a, b) {\n\
6963             \x20   m(a, b);                     // +1b\n\
6964             \x20   m(!a, !b);                   // +1b +2c\n\
6965             }\n",
6966            "foo.js",
6967            |metric| {
6968                assert_eq!(metric.abc.branches_sum(), 2);
6969                assert_eq!(metric.abc.conditions_sum(), 2);
6970                insta::assert_json_snapshot!(metric.abc);
6971            },
6972        );
6973    }
6974
6975    #[test]
6976    fn javascript_return_with_conditions() {
6977        check_metrics::<JavascriptParser>(
6978            "function m1(z) { return !(z >= 0); }\n\
6979             function m2(x) { return (((!x))); }\n\
6980             function m3(x, y) { return x && y; }\n",
6981            "foo.js",
6982            |metric| {
6983                // m1: 1 (`>=`). m2: 1 (walker unwraps to `x`).
6984                // m3: 2 (`&&` walker counts both terminals).
6985                assert_eq!(metric.abc.conditions_sum(), 4);
6986                insta::assert_json_snapshot!(metric.abc);
6987            },
6988        );
6989    }
6990
6991    #[test]
6992    fn typescript_if_boolean_literal_condition() {
6993        check_metrics::<TypescriptParser>(
6994            "function f() {\n\
6995             \x20   if (true) {}\n\
6996             \x20   if (!false) {}\n\
6997             \x20   while (true) {}\n\
6998             \x20   do {} while (false);\n\
6999             }\n",
7000            "foo.ts",
7001            |metric| {
7002                assert_eq!(metric.abc.conditions_sum(), 4);
7003                insta::assert_json_snapshot!(metric.abc);
7004            },
7005        );
7006    }
7007
7008    #[test]
7009    fn typescript_methods_arguments_with_conditions() {
7010        check_metrics::<TypescriptParser>(
7011            "function f(a: boolean, b: boolean) {\n\
7012             \x20   m(a, b);\n\
7013             \x20   m(!a, !b);\n\
7014             }\n",
7015            "foo.ts",
7016            |metric| {
7017                assert_eq!(metric.abc.branches_sum(), 2);
7018                assert_eq!(metric.abc.conditions_sum(), 2);
7019                insta::assert_json_snapshot!(metric.abc);
7020            },
7021        );
7022    }
7023
7024    #[test]
7025    fn typescript_return_with_conditions() {
7026        check_metrics::<TypescriptParser>(
7027            "function m1(z: number): boolean { return !(z >= 0); }\n\
7028             function m2(x: boolean): boolean { return (((!x))); }\n\
7029             function m3(x: boolean, y: boolean): boolean { return x && y; }\n",
7030            "foo.ts",
7031            |metric| {
7032                assert_eq!(metric.abc.conditions_sum(), 4);
7033                insta::assert_json_snapshot!(metric.abc);
7034            },
7035        );
7036    }
7037
7038    #[test]
7039    fn tsx_if_boolean_literal_condition() {
7040        check_metrics::<TsxParser>(
7041            "function f() {\n\
7042             \x20   if (true) {}\n\
7043             \x20   if (!false) {}\n\
7044             \x20   while (true) {}\n\
7045             \x20   do {} while (false);\n\
7046             }\n",
7047            "foo.tsx",
7048            |metric| {
7049                assert_eq!(metric.abc.conditions_sum(), 4);
7050                insta::assert_json_snapshot!(metric.abc);
7051            },
7052        );
7053    }
7054
7055    #[test]
7056    fn tsx_methods_arguments_with_conditions() {
7057        check_metrics::<TsxParser>(
7058            "function f(a: boolean, b: boolean) {\n\
7059             \x20   m(a, b);\n\
7060             \x20   m(!a, !b);\n\
7061             }\n",
7062            "foo.tsx",
7063            |metric| {
7064                assert_eq!(metric.abc.branches_sum(), 2);
7065                assert_eq!(metric.abc.conditions_sum(), 2);
7066                insta::assert_json_snapshot!(metric.abc);
7067            },
7068        );
7069    }
7070
7071    #[test]
7072    fn tsx_return_with_conditions() {
7073        check_metrics::<TsxParser>(
7074            "function m1(z: number): boolean { return !(z >= 0); }\n\
7075             function m2(x: boolean): boolean { return (((!x))); }\n\
7076             function m3(x: boolean, y: boolean): boolean { return x && y; }\n",
7077            "foo.tsx",
7078            |metric| {
7079                assert_eq!(metric.abc.conditions_sum(), 4);
7080                insta::assert_json_snapshot!(metric.abc);
7081            },
7082        );
7083    }
7084
7085    #[test]
7086    fn mozjs_if_boolean_literal_condition() {
7087        check_metrics::<MozjsParser>(
7088            "function f() {\n\
7089             \x20   if (true) {}\n\
7090             \x20   if (!false) {}\n\
7091             \x20   while (true) {}\n\
7092             \x20   do {} while (false);\n\
7093             }\n",
7094            "foo.js",
7095            |metric| {
7096                assert_eq!(metric.abc.conditions_sum(), 4);
7097                insta::assert_json_snapshot!(metric.abc);
7098            },
7099        );
7100    }
7101
7102    #[test]
7103    fn mozjs_methods_arguments_with_conditions() {
7104        check_metrics::<MozjsParser>(
7105            "function f(a, b) {\n\
7106             \x20   m(a, b);\n\
7107             \x20   m(!a, !b);\n\
7108             }\n",
7109            "foo.js",
7110            |metric| {
7111                assert_eq!(metric.abc.branches_sum(), 2);
7112                assert_eq!(metric.abc.conditions_sum(), 2);
7113                insta::assert_json_snapshot!(metric.abc);
7114            },
7115        );
7116    }
7117
7118    #[test]
7119    fn mozjs_return_with_conditions() {
7120        check_metrics::<MozjsParser>(
7121            "function m1(z) { return !(z >= 0); }\n\
7122             function m2(x) { return (((!x))); }\n\
7123             function m3(x, y) { return x && y; }\n",
7124            "foo.js",
7125            |metric| {
7126                assert_eq!(metric.abc.conditions_sum(), 4);
7127                insta::assert_json_snapshot!(metric.abc);
7128            },
7129        );
7130    }
7131
7132    // ----- JS / TS / Tsx / Mozjs unary-conditional walker -----
7133
7134    #[test]
7135    fn javascript_if_multiple_conditions() {
7136        check_metrics::<JavascriptParser>(
7137            "function f(a, b, c, d) {\n\
7138             \x20   if (a || b || c || d) {}        // +4c\n\
7139             \x20   if (a && b && c) {}             // +3c\n\
7140             \x20   if (!a && !b) {}                // +2c\n\
7141             }\n",
7142            "foo.js",
7143            |metric| {
7144                assert_eq!(metric.abc.conditions_sum(), 9);
7145                insta::assert_json_snapshot!(metric.abc);
7146            },
7147        );
7148    }
7149
7150    #[test]
7151    fn javascript_while_and_do_while_conditions() {
7152        check_metrics::<JavascriptParser>(
7153            "function f(a, b) {\n\
7154             \x20   while (a || b) {}              // +2c\n\
7155             \x20   do {} while (a && !b);         // +2c\n\
7156             }\n",
7157            "foo.js",
7158            |metric| {
7159                assert_eq!(metric.abc.conditions_sum(), 4);
7160                insta::assert_json_snapshot!(metric.abc);
7161            },
7162        );
7163    }
7164
7165    #[test]
7166    fn javascript_short_circuit_with_boolean_literal_operand() {
7167        check_metrics::<JavascriptParser>(
7168            "function f(a) { return a && true; }\n",
7169            "foo.js",
7170            |metric| {
7171                assert_eq!(metric.abc.conditions_sum(), 2);
7172                insta::assert_json_snapshot!(metric.abc);
7173            },
7174        );
7175    }
7176
7177    #[test]
7178    fn typescript_if_multiple_conditions() {
7179        check_metrics::<TypescriptParser>(
7180            "function f(a: boolean, b: boolean, c: boolean, d: boolean) {\n\
7181             \x20   if (a || b || c || d) {}        // +4c\n\
7182             \x20   if (a && b && c) {}             // +3c\n\
7183             \x20   if (!a && !b) {}                // +2c\n\
7184             }\n",
7185            "foo.ts",
7186            |metric| {
7187                assert_eq!(metric.abc.conditions_sum(), 9);
7188                insta::assert_json_snapshot!(metric.abc);
7189            },
7190        );
7191    }
7192
7193    #[test]
7194    fn typescript_while_and_do_while_conditions() {
7195        check_metrics::<TypescriptParser>(
7196            "function f(a: boolean, b: boolean) {\n\
7197             \x20   while (a || b) {}              // +2c\n\
7198             \x20   do {} while (a && !b);         // +2c\n\
7199             }\n",
7200            "foo.ts",
7201            |metric| {
7202                assert_eq!(metric.abc.conditions_sum(), 4);
7203                insta::assert_json_snapshot!(metric.abc);
7204            },
7205        );
7206    }
7207
7208    #[test]
7209    fn typescript_short_circuit_with_boolean_literal_operand() {
7210        check_metrics::<TypescriptParser>(
7211            "function f(a: boolean): boolean { return a && true; }\n",
7212            "foo.ts",
7213            |metric| {
7214                assert_eq!(metric.abc.conditions_sum(), 2);
7215                insta::assert_json_snapshot!(metric.abc);
7216            },
7217        );
7218    }
7219
7220    #[test]
7221    fn tsx_if_multiple_conditions() {
7222        check_metrics::<TsxParser>(
7223            "function f(a: boolean, b: boolean, c: boolean, d: boolean) {\n\
7224             \x20   if (a || b || c || d) {}        // +4c\n\
7225             \x20   if (a && b && c) {}             // +3c\n\
7226             \x20   if (!a && !b) {}                // +2c\n\
7227             }\n",
7228            "foo.tsx",
7229            |metric| {
7230                assert_eq!(metric.abc.conditions_sum(), 9);
7231                insta::assert_json_snapshot!(metric.abc);
7232            },
7233        );
7234    }
7235
7236    #[test]
7237    fn tsx_while_and_do_while_conditions() {
7238        check_metrics::<TsxParser>(
7239            "function f(a: boolean, b: boolean) {\n\
7240             \x20   while (a || b) {}              // +2c\n\
7241             \x20   do {} while (a && !b);         // +2c\n\
7242             }\n",
7243            "foo.tsx",
7244            |metric| {
7245                assert_eq!(metric.abc.conditions_sum(), 4);
7246                insta::assert_json_snapshot!(metric.abc);
7247            },
7248        );
7249    }
7250
7251    #[test]
7252    fn tsx_short_circuit_with_boolean_literal_operand() {
7253        check_metrics::<TsxParser>(
7254            "function f(a: boolean): boolean { return a && true; }\n",
7255            "foo.tsx",
7256            |metric| {
7257                assert_eq!(metric.abc.conditions_sum(), 2);
7258                insta::assert_json_snapshot!(metric.abc);
7259            },
7260        );
7261    }
7262
7263    #[test]
7264    fn mozjs_if_multiple_conditions() {
7265        check_metrics::<MozjsParser>(
7266            "function f(a, b, c, d) {\n\
7267             \x20   if (a || b || c || d) {}        // +4c\n\
7268             \x20   if (a && b && c) {}             // +3c\n\
7269             \x20   if (!a && !b) {}                // +2c\n\
7270             }\n",
7271            "foo.js",
7272            |metric| {
7273                assert_eq!(metric.abc.conditions_sum(), 9);
7274                insta::assert_json_snapshot!(metric.abc);
7275            },
7276        );
7277    }
7278
7279    #[test]
7280    fn mozjs_while_and_do_while_conditions() {
7281        check_metrics::<MozjsParser>(
7282            "function f(a, b) {\n\
7283             \x20   while (a || b) {}              // +2c\n\
7284             \x20   do {} while (a && !b);         // +2c\n\
7285             }\n",
7286            "foo.js",
7287            |metric| {
7288                assert_eq!(metric.abc.conditions_sum(), 4);
7289                insta::assert_json_snapshot!(metric.abc);
7290            },
7291        );
7292    }
7293
7294    #[test]
7295    fn mozjs_short_circuit_with_boolean_literal_operand() {
7296        check_metrics::<MozjsParser>(
7297            "function f(a) { return a && true; }\n",
7298            "foo.js",
7299            |metric| {
7300                assert_eq!(metric.abc.conditions_sum(), 2);
7301                insta::assert_json_snapshot!(metric.abc);
7302            },
7303        );
7304    }
7305
7306    // ---------- Perl ABC tests ----------
7307
7308    #[test]
7309    fn perl_empty_unit_zero() {
7310        // Empty source produces zero ABC magnitude — pins the trait
7311        // wiring without exercising any compute branch.
7312        check_metrics::<PerlParser>("", "empty.pl", |metric| {
7313            assert_eq!(metric.abc.assignments_sum(), 0);
7314            assert_eq!(metric.abc.branches_sum(), 0);
7315            assert_eq!(metric.abc.conditions_sum(), 0);
7316            insta::assert_json_snapshot!(metric.abc);
7317        });
7318    }
7319
7320    #[test]
7321    fn perl_plain_and_compound_assignments_count() {
7322        // `my $x = 0` parses as a `binary_expression` with an `=`
7323        // token, so the initialiser counts (Perl has no equivalent of
7324        // the JS `const` initialiser-suppression rule). Each
7325        // assignment operator token contributes one assignment:
7326        // `=`, `=`, `+=`, `.=`, `**=` → A = 5. Two of those `=` come
7327        // from the `my $x = 0` initialiser and the later `$x = 5`
7328        // reassignment.
7329        check_metrics::<PerlParser>(
7330            "sub f { my $x = 0; $x = 5; $x += 2; $x .= \"a\"; $x **= 3; }",
7331            "foo.pl",
7332            |metric| {
7333                assert_eq!(metric.abc.assignments_sum(), 5);
7334                assert_eq!(metric.abc.branches_sum(), 0);
7335                assert_eq!(metric.abc.conditions_sum(), 0);
7336                insta::assert_json_snapshot!(metric.abc);
7337            },
7338        );
7339    }
7340
7341    #[test]
7342    fn perl_calls_are_branches() {
7343        // `foo()` parses as `call_expression_with_args_with_brackets`
7344        // wrapping an inner `call_expression_with_bareword(foo)`;
7345        // `bar 1, 2` wraps `bar` likewise under spaced-args; `shift`
7346        // appears as a standalone bareword. The bareword-inside-
7347        // wrapper case must NOT double-count — only the outer wrapper
7348        // contributes a branch. So B = 3 (foo, bar, shift), not 5.
7349        check_metrics::<PerlParser>(
7350            "sub f { foo(); bar 1, 2; my $a = shift; }",
7351            "foo.pl",
7352            |metric| {
7353                // shift's `my $a = shift` initialiser contributes one
7354                // assignment via the `=` token.
7355                assert_eq!(metric.abc.assignments_sum(), 1);
7356                assert_eq!(metric.abc.branches_sum(), 3);
7357                assert_eq!(metric.abc.conditions_sum(), 0);
7358                insta::assert_json_snapshot!(metric.abc);
7359            },
7360        );
7361    }
7362
7363    #[test]
7364    fn perl_method_invocation_counts_as_branch() {
7365        // `$obj->method(...)` parses as `method_invocation`. Any
7366        // arrow-dispatch counts as one branch regardless of how the
7367        // arguments are passed.
7368        check_metrics::<PerlParser>(
7369            "sub f { my $obj = shift; $obj->run($x); $obj->ping; }",
7370            "foo.pl",
7371            |metric| {
7372                // `my $obj = shift` → A=1, B=1 (shift bareword).
7373                // `$obj->run($x)` and `$obj->ping` → 2 more branches.
7374                assert_eq!(metric.abc.assignments_sum(), 1);
7375                assert_eq!(metric.abc.branches_sum(), 3);
7376                assert_eq!(metric.abc.conditions_sum(), 0);
7377                insta::assert_json_snapshot!(metric.abc);
7378            },
7379        );
7380    }
7381
7382    #[test]
7383    fn perl_numeric_and_string_comparisons_count_conditions() {
7384        // Numeric ops `==`, `!=`, `<`, `>`, `<=`, `>=`, `<=>` and
7385        // string ops `eq`, `ne`, `lt`, `gt`, `le`, `ge`, `cmp` each
7386        // fire once per token. The sample below uses one of each →
7387        // C = 14. No assignments, no branches.
7388        check_metrics::<PerlParser>(
7389            "sub f {\n\
7390                 my $r;\n\
7391                 $r = $a == $b;\n\
7392                 $r = $a != $b;\n\
7393                 $r = $a <  $b;\n\
7394                 $r = $a >  $b;\n\
7395                 $r = $a <= $b;\n\
7396                 $r = $a >= $b;\n\
7397                 $r = $a <=> $b;\n\
7398                 $r = $a eq $b;\n\
7399                 $r = $a ne $b;\n\
7400                 $r = $a lt $b;\n\
7401                 $r = $a gt $b;\n\
7402                 $r = $a le $b;\n\
7403                 $r = $a ge $b;\n\
7404                 $r = $a cmp $b;\n\
7405             }",
7406            "foo.pl",
7407            |metric| {
7408                // 15 `=` tokens: one declaration `my $r` (no `=`),
7409                // then 14 `$r = …` plus there's no `=` in `my $r;`.
7410                // Actually: `my $r;` has no `=`; the 14 `$r = …` are
7411                // 14 `=` tokens. So A=14, C=14.
7412                assert_eq!(metric.abc.assignments_sum(), 14);
7413                assert_eq!(metric.abc.branches_sum(), 0);
7414                assert_eq!(metric.abc.conditions_sum(), 14);
7415                insta::assert_json_snapshot!(metric.abc);
7416            },
7417        );
7418    }
7419
7420    #[test]
7421    fn perl_short_circuit_not_counted_directly_ternary_counts() {
7422        // `&&`, `||`, `//`, low-precedence `and`, `or`, `xor` are
7423        // NOT counted as conditions on their own (Fitzpatrick Rule
7424        // 5; #395) — instead each operand is counted as a unary
7425        // conditional by the walker (Rule 9; #403). At the pinned
7426        // tree-sitter-perl grammar version, only the four
7427        // punctuation forms plus one keyword form parse under a
7428        // `binary_expression` parent that triggers the walker; the
7429        // other two keyword forms parse under a distinct grammar
7430        // node and contribute zero. Net: 4 walker-firing lines × 2
7431        // scalar-variable operands + 1 ternary `?` = 9. The exact
7432        // mix of "which two keyword forms are silent" is grammar-
7433        // version-dependent; a future grammar bump that normalises
7434        // the keyword forms' parent kind will shift this count to
7435        // 13. See follow-up note above the test name.
7436        check_metrics::<PerlParser>(
7437            "sub f {\n\
7438                 my $r;\n\
7439                 $r = $a && $b;\n\
7440                 $r = $a || $b;\n\
7441                 $r = $a // $b;\n\
7442                 $r = $a and $b;\n\
7443                 $r = $a or  $b;\n\
7444                 $r = $a xor $b;\n\
7445                 $r = $a ? 1 : 2;\n\
7446             }",
7447            "foo.pl",
7448            |metric| {
7449                // 7 `=` tokens (one per reassignment line).
7450                assert_eq!(metric.abc.assignments_sum(), 7);
7451                assert_eq!(metric.abc.branches_sum(), 0);
7452                // 4 walker-triggered lines × 2 operands + 1 ternary
7453                // `?` = 9. The two remaining low-precedence keyword
7454                // forms (one of `and`/`or`/`xor`) fall under a
7455                // non-binary_expression parent in this grammar
7456                // version and contribute zero via the walker.
7457                assert_eq!(metric.abc.conditions_sum(), 9);
7458                insta::assert_json_snapshot!(metric.abc);
7459            },
7460        );
7461    }
7462
7463    #[test]
7464    fn perl_elsif_and_else_count_conditions() {
7465        // `if (… == …) { … } elsif (… < …) { … } else { … }` →
7466        // 2 comparison tokens (`==`, `<`), plus `elsif_clause` and
7467        // `else_clause` each + 1 → C = 4. Branches: 0 (only
7468        // assignments). Assignments: just the `=` initialisers /
7469        // reassignments — there are 4 here (`$x` init plus three
7470        // `$x = …` reassigns).
7471        check_metrics::<PerlParser>(
7472            "sub f {\n\
7473                 my $x = 0;\n\
7474                 if ($a == $b) {\n\
7475                     $x = 1;\n\
7476                 } elsif ($a < $b) {\n\
7477                     $x = 2;\n\
7478                 } else {\n\
7479                     $x = 3;\n\
7480                 }\n\
7481             }",
7482            "foo.pl",
7483            |metric| {
7484                assert_eq!(metric.abc.assignments_sum(), 4);
7485                assert_eq!(metric.abc.branches_sum(), 0);
7486                assert_eq!(metric.abc.conditions_sum(), 4);
7487                insta::assert_json_snapshot!(metric.abc);
7488            },
7489        );
7490    }
7491
7492    #[test]
7493    fn perl_regex_match_operators_count_conditions() {
7494        // `=~` and `!~` are pattern-match operators; we count both
7495        // as conditions because they evaluate the regex match in a
7496        // boolean context.
7497        check_metrics::<PerlParser>(
7498            "sub f { my $s = shift; my $m = $s =~ /foo/; my $n = $s !~ /bar/; }",
7499            "foo.pl",
7500            |metric| {
7501                // 3 `=` tokens, 0 branches except `shift` bareword.
7502                assert_eq!(metric.abc.assignments_sum(), 3);
7503                assert_eq!(metric.abc.branches_sum(), 1);
7504                assert_eq!(metric.abc.conditions_sum(), 2);
7505                insta::assert_json_snapshot!(metric.abc);
7506            },
7507        );
7508    }
7509
7510    #[test]
7511    fn perl_complex_function_abc() {
7512        // Mixed program exercising every category. Computed
7513        // expected:
7514        //   Assignments: `my $i = 0` (1), `$i++` is a unary
7515        //     increment — Perl's grammar emits `PLUSPLUS` not an `=`
7516        //     operator, so it does NOT count under the operator-
7517        //     token rule. The for-loop's `$i++` is similarly
7518        //     uncounted.
7519        //     Total A: 1 from `my $i = 0`, 1 from `$total += $i`
7520        //     (the `+=` token) → A = 2.
7521        //   Branches: `do_work($i)` → 1; `print "done\n"` is a
7522        //     call_expression_with_spaced_args → 1; `return $total`
7523        //     uses the `return` keyword not a call → 0. B = 2.
7524        //   Conditions: `$i < 10` (`<`) → 1; `$i % 2 == 0` (`==`) →
7525        //     1; `else_clause` → 1. C = 3.
7526        check_metrics::<PerlParser>(
7527            "sub run {\n\
7528                 my $total = 0;\n\
7529                 for (my $i = 0; $i < 10; $i++) {\n\
7530                     if ($i % 2 == 0) {\n\
7531                         do_work($i);\n\
7532                     } else {\n\
7533                         $total += $i;\n\
7534                     }\n\
7535                 }\n\
7536                 print \"done\\n\";\n\
7537                 return $total;\n\
7538             }",
7539            "foo.pl",
7540            |metric| {
7541                // `my $total = 0` is one `=`; `my $i = 0` is another
7542                // `=`; `$total += $i` is one `+=`. Total = 3.
7543                assert_eq!(metric.abc.assignments_sum(), 3);
7544                assert_eq!(metric.abc.branches_sum(), 2);
7545                assert_eq!(metric.abc.conditions_sum(), 3);
7546                insta::assert_json_snapshot!(metric.abc);
7547            },
7548        );
7549    }
7550
7551    #[test]
7552    fn perl_if_multiple_conditions() {
7553        // Fitzpatrick Rule 9 walker (issue #403): each operand of a
7554        // `&&` / `||` / `//` / `and` / `or` / `xor` chain is one
7555        // condition. ScalarVariable operands ($a, $b, …) qualify as
7556        // terminal-bool kinds for the walker.
7557        check_metrics::<PerlParser>(
7558            "sub f {\n\
7559                 my ($a, $b, $c, $d) = @_;\n\
7560                 if ($a || $b || $c || $d) { return 1; }    # +4c\n\
7561                 if ($a && $b && $c) { return 2; }          # +3c\n\
7562                 if (!$a && !$b) { return 3; }              # +2c\n\
7563                 return 0;\n\
7564             }",
7565            "foo.pl",
7566            |metric| {
7567                assert_eq!(metric.abc.conditions_sum(), 9);
7568                insta::assert_json_snapshot!(metric.abc);
7569            },
7570        );
7571    }
7572
7573    #[test]
7574    fn perl_while_and_until_conditions() {
7575        // Perl has no `do { ... } while(cond);` shape in this grammar
7576        // — `while` and `until` are the loop forms with a condition
7577        // slot. The walker fires on each `&&` / `||` token inside
7578        // those headers.
7579        check_metrics::<PerlParser>(
7580            "sub f {\n\
7581                 my ($a, $b) = @_;\n\
7582                 while ($a || $b) { last; }            # +2c\n\
7583                 until ($a && !$b) { last; }           # +2c\n\
7584             }",
7585            "foo.pl",
7586            |metric| {
7587                assert_eq!(metric.abc.conditions_sum(), 4);
7588                insta::assert_json_snapshot!(metric.abc);
7589            },
7590        );
7591    }
7592
7593    #[test]
7594    fn perl_short_circuit_counts_scalar_variable_operands() {
7595        // `$a && $b` reports 2 conditions — one walker count per
7596        // `ScalarVariable` operand. Renamed from the cross-language
7597        // `_with_boolean_literal_operand` convention because Perl has
7598        // no readily-grammar-exposed boolean literal in an `&&`
7599        // operand slot at the pinned grammar version (the `Boolean`
7600        // kind only fires on the `boolean` pragma's named constants,
7601        // not bareword `1` / `0`). Two scalar variables are the
7602        // grammar-stable terminal-set witness for Perl.
7603        check_metrics::<PerlParser>(
7604            "sub f { my ($a) = @_; return $a && $b; }\n",
7605            "foo.pl",
7606            |metric| {
7607                assert_eq!(metric.abc.conditions_sum(), 2);
7608                insta::assert_json_snapshot!(metric.abc);
7609            },
7610        );
7611    }
7612
7613    #[test]
7614    fn perl_array_in_binary_operand_descends_to_scalar_context_value() {
7615        // Regression test for the code-review findings on the
7616        // Phase-2B Perl walker:
7617        //   - Pre-fix-A: `perl_inspect_container` descended `Array`
7618        //     via `node.child(1)` — the FIRST element — wrongly
7619        //     attributing `$x` for `($x, $y)` (semantically `$y`
7620        //     is the scalar-context value).
7621        //   - Fix-A (the `array_is_paren` guard, 5db8078): dropped
7622        //     Array-as-paren entirely in `BinaryExpression` operand
7623        //     contexts to avoid the wrong attribution — but
7624        //     regressed `$a || ($x)` (single paren-grouped operand)
7625        //     to C=1 instead of 2.
7626        //   - Fix-B (this change): keeps Array-as-paren unconditional
7627        //     but descends via the LAST named child. `$a || ($x)`
7628        //     reaches `$x` (count both operands → 2);
7629        //     `$a || ($x, $y)` reaches `$y` (count `$a` + `$y` →
7630        //     still 2, matching Fitzpatrick Rule 7 "one per
7631        //     operand"); `if ($a)` still reaches `$a` (single-
7632        //     element grouping → 1).
7633        check_metrics::<PerlParser>(
7634            "sub f { my ($a, $x, $y) = @_;\n\
7635             \x20   my $r = $a || ($x, $y);    # +2c: $a + last-named $y\n\
7636             \x20   my $s = $a || ($x);        # +2c: $a + only-named $x\n\
7637             \x20   $r + $s;\n\
7638             }\n",
7639            "foo.pl",
7640            |metric| {
7641                // 2 + 2 = 4 unary conditions from the two `||`s.
7642                assert_eq!(metric.abc.conditions_sum(), 4);
7643                insta::assert_json_snapshot!(metric.abc);
7644            },
7645        );
7646    }
7647
7648    #[test]
7649    fn perl_if_scalar_variable_condition() {
7650        // Renamed from the cross-language
7651        // `_if_boolean_literal_condition` convention because
7652        // Perl has no readily-grammar-exposed boolean literal in
7653        // an `if (cond)` slot at the pinned grammar version:
7654        // tree-sitter-perl's `Boolean` kind only fires for the
7655        // `boolean` pragma's named constants (not bareword `1` /
7656        // `0`, which surface as `Integer` / not in the
7657        // terminal-bool set). A scalar-variable condition is the
7658        // grammar-stable witness — `if ($a)` reaches
7659        // `scalar_variable` via the `Array` paren unwrap.
7660        check_metrics::<PerlParser>(
7661            "sub f { my ($a) = @_; if ($a) { return 1; } }\n",
7662            "foo.pl",
7663            |metric| {
7664                assert_eq!(metric.abc.conditions_sum(), 1);
7665                insta::assert_json_snapshot!(metric.abc);
7666            },
7667        );
7668    }
7669
7670    #[test]
7671    fn perl_methods_arguments_with_conditions() {
7672        // `call(!$a, !$b)` — argument list walker counts each
7673        // unary-conditional argument once. Cannot use `m(...)` as
7674        // the function name — tree-sitter-perl parses `m(...)` as
7675        // the regex-match operator, not a function call.
7676        check_metrics::<PerlParser>(
7677            "sub f { my ($a, $b) = @_; call($a, $b); call(!$a, !$b); }\n",
7678            "foo.pl",
7679            |metric| {
7680                // Two calls × 1 branch each = 2 branches.
7681                // `call(!$a, !$b)` contributes 2 walker conditions
7682                // (one per `!`-wrapped scalar-variable argument);
7683                // `call($a, $b)` contributes 0 (bare-args don't
7684                // count via the Arguments walker — list_kind !=
7685                // BinaryExpression).
7686                assert_eq!(metric.abc.branches_sum(), 2);
7687                assert_eq!(metric.abc.conditions_sum(), 2);
7688                insta::assert_json_snapshot!(metric.abc);
7689            },
7690        );
7691    }
7692
7693    #[test]
7694    fn perl_return_with_conditions() {
7695        // `return !$a` reports 1 condition via the walker (unary
7696        // unwrap to scalar-variable terminal). `return $a` reports
7697        // 0 (no paren / unary wrap, has_boolean_content stays
7698        // false from ReturnExpression parent).
7699        check_metrics::<PerlParser>(
7700            "sub m1 { my ($z) = @_; return !($z); }\n\
7701             sub m2 { my ($x) = @_; return (((!$x))); }\n\
7702             sub m3 { my ($x, $y) = @_; return $x && $y; }\n",
7703            "foo.pl",
7704            |metric| {
7705                // m1: !($z) → walker on `!` unwraps paren to $z (1).
7706                // m2: (((!$x))) → walker unwraps three parens + one
7707                //     unary to $x (1).
7708                // m3: $x && $y → walker on `&&` counts both (2).
7709                // Sum: 4.
7710                assert_eq!(metric.abc.conditions_sum(), 4);
7711                insta::assert_json_snapshot!(metric.abc);
7712            },
7713        );
7714    }
7715
7716    // ---------- Lua ABC tests ----------
7717
7718    #[test]
7719    fn lua_empty_unit_zero() {
7720        check_metrics::<LuaParser>("", "empty.lua", |metric| {
7721            assert_eq!(metric.abc.assignments_sum(), 0);
7722            assert_eq!(metric.abc.branches_sum(), 0);
7723            assert_eq!(metric.abc.conditions_sum(), 0);
7724            insta::assert_json_snapshot!(metric.abc);
7725        });
7726    }
7727
7728    #[test]
7729    fn lua_assignments_count_locals_and_plain() {
7730        // `local x = 0` wraps an `assignment_statement` under a
7731        // `variable_declaration`; the inner wrapper still counts.
7732        // Multi-target assignment `a, b = 1, 2` is a single
7733        // `assignment_statement` and contributes 1, NOT 2 — the
7734        // wrapper is the unit of counting (matches the Python rule:
7735        // one `Assignment` node, one assignment).
7736        check_metrics::<LuaParser>(
7737            "function f()\n\
7738                 local x = 0\n\
7739                 x = 1\n\
7740                 local a, b = 1, 2\n\
7741                 a, b = b, a\n\
7742             end",
7743            "foo.lua",
7744            |metric| {
7745                assert_eq!(metric.abc.assignments_sum(), 4);
7746                assert_eq!(metric.abc.branches_sum(), 0);
7747                assert_eq!(metric.abc.conditions_sum(), 0);
7748                insta::assert_json_snapshot!(metric.abc);
7749            },
7750        );
7751    }
7752
7753    #[test]
7754    fn lua_calls_are_branches() {
7755        // `print(x)`, `obj.m(x)`, `obj:m(x)`, `f(g(1))` — every
7756        // call form is a `function_call` node. The nested
7757        // `f(g(1))` counts as 2 branches (one per dispatch).
7758        check_metrics::<LuaParser>(
7759            "function r(x)\n\
7760                 print(x)\n\
7761                 obj.m(x)\n\
7762                 obj:m(x)\n\
7763                 return f(g(1))\n\
7764             end",
7765            "foo.lua",
7766            |metric| {
7767                assert_eq!(metric.abc.assignments_sum(), 0);
7768                assert_eq!(metric.abc.branches_sum(), 5);
7769                assert_eq!(metric.abc.conditions_sum(), 0);
7770                insta::assert_json_snapshot!(metric.abc);
7771            },
7772        );
7773    }
7774
7775    #[test]
7776    fn lua_comparisons_count_logical_ops_do_not() {
7777        // Each comparison token contributes one condition; `and` /
7778        // `or` are NOT counted on their own (Fitzpatrick Rule 5;
7779        // #395) — instead each operand is counted as a unary
7780        // conditional by the walker (Rule 9; #403). The two
7781        // `a and b` / `a or b` lines add 2 walker conditions each.
7782        check_metrics::<LuaParser>(
7783            "function f(a, b)\n\
7784                 local r\n\
7785                 r = a == b\n\
7786                 r = a ~= b\n\
7787                 r = a <  b\n\
7788                 r = a >  b\n\
7789                 r = a <= b\n\
7790                 r = a >= b\n\
7791                 r = a and b\n\
7792                 r = a or  b\n\
7793             end",
7794            "foo.lua",
7795            |metric| {
7796                // 8 `r = …` reassignments, plus `local r` (no `=`).
7797                assert_eq!(metric.abc.assignments_sum(), 8);
7798                assert_eq!(metric.abc.branches_sum(), 0);
7799                // 6 comparisons (+6) + 2 logical lines × 2 walker
7800                // operands (+4) = 10.
7801                assert_eq!(metric.abc.conditions_sum(), 10);
7802                insta::assert_json_snapshot!(metric.abc);
7803            },
7804        );
7805    }
7806
7807    #[test]
7808    fn lua_elseif_and_else_count_conditions() {
7809        // Each elseif / else arm of the if contributes one
7810        // condition, mirroring the Python rule.
7811        check_metrics::<LuaParser>(
7812            "function f(x)\n\
7813                 if x > 0 then\n\
7814                     return 1\n\
7815                 elseif x < 0 then\n\
7816                     return -1\n\
7817                 else\n\
7818                     return 0\n\
7819                 end\n\
7820             end",
7821            "foo.lua",
7822            |metric| {
7823                // Comparisons: `>`, `<` → 2; elseif_statement → 1;
7824                // else_statement → 1. C = 4. No branches (no calls).
7825                assert_eq!(metric.abc.assignments_sum(), 0);
7826                assert_eq!(metric.abc.branches_sum(), 0);
7827                assert_eq!(metric.abc.conditions_sum(), 4);
7828                insta::assert_json_snapshot!(metric.abc);
7829            },
7830        );
7831    }
7832
7833    #[test]
7834    fn lua_complex_function_abc() {
7835        // Combines every category to pin the metric.
7836        check_metrics::<LuaParser>(
7837            "function run(n)\n\
7838                 local total = 0\n\
7839                 for i = 1, n do\n\
7840                     if i % 2 == 0 then\n\
7841                         do_work(i)\n\
7842                     else\n\
7843                         total = total + i\n\
7844                     end\n\
7845                 end\n\
7846                 print(\"done\")\n\
7847                 return total\n\
7848             end",
7849            "foo.lua",
7850            |metric| {
7851                // Assignments: `local total = 0` (1), `total = total + i` (1) → 2.
7852                // Branches: `do_work(i)` (1), `print(\"done\")` (1) → 2.
7853                // Conditions: `==` (1), `else_statement` (1) → 2.
7854                assert_eq!(metric.abc.assignments_sum(), 2);
7855                assert_eq!(metric.abc.branches_sum(), 2);
7856                assert_eq!(metric.abc.conditions_sum(), 2);
7857                insta::assert_json_snapshot!(metric.abc);
7858            },
7859        );
7860    }
7861
7862    #[test]
7863    fn lua_if_multiple_conditions() {
7864        // Fitzpatrick Rule 9 walker (issue #403). Lua's `and` / `or`
7865        // are keyword tokens inside a `binary_expression`.
7866        check_metrics::<LuaParser>(
7867            "function f(a, b, c, d)\n\
7868                 if a or b or c or d then return 1 end       -- +4c\n\
7869                 if a and b and c then return 2 end          -- +3c\n\
7870                 if not a and not b then return 3 end        -- +2c\n\
7871                 return 0\n\
7872             end",
7873            "foo.lua",
7874            |metric| {
7875                assert_eq!(metric.abc.conditions_sum(), 9);
7876                insta::assert_json_snapshot!(metric.abc);
7877            },
7878        );
7879    }
7880
7881    #[test]
7882    fn lua_while_conditions() {
7883        // Lua has no `do { ... } while(cond);` — `while cond do …
7884        // end` and `repeat … until cond` are the loop forms.
7885        check_metrics::<LuaParser>(
7886            "function f(a, b)\n\
7887                 while a or b do break end                   -- +2c\n\
7888                 repeat break until a and not b              -- +2c\n\
7889             end",
7890            "foo.lua",
7891            |metric| {
7892                assert_eq!(metric.abc.conditions_sum(), 4);
7893                insta::assert_json_snapshot!(metric.abc);
7894            },
7895        );
7896    }
7897
7898    #[test]
7899    fn lua_short_circuit_with_boolean_literal_operand() {
7900        // `a and true` reports 2 conditions: one Identifier, one
7901        // True keyword literal.
7902        check_metrics::<LuaParser>("function f(a) return a and true end", "foo.lua", |metric| {
7903            assert_eq!(metric.abc.conditions_sum(), 2);
7904            insta::assert_json_snapshot!(metric.abc);
7905        });
7906    }
7907
7908    #[test]
7909    fn lua_number_truthy_condition_counts() {
7910        // Regression for findings.md #2: Lua treats every non-nil,
7911        // non-false value as truthy, so `if 1 then ... end` and
7912        // `return a and 2` should each count their numeric literal
7913        // as a Fitzpatrick Rule 6 / 7 unary condition. Pre-fix,
7914        // `lua_bool_terminal_kinds!()` listed `True` / `False` /
7915        // `Nil` but omitted `Number`, so the walker dropped every
7916        // numeric-truthy operand. The walker comment at the top of
7917        // `lua_inspect_container` already promised numbers were
7918        // terminal-bool kinds; this commit closes the gap.
7919        check_metrics::<LuaParser>(
7920            "function f(a)\n\
7921             \x20   if 1 then return 1 end\n\
7922             \x20   return a and 2\n\
7923             end",
7924            "foo.lua",
7925            |metric| {
7926                // `if 1 then` → walker counts the Number literal (+1).
7927                // `a and 2` → `and` walker counts both operands:
7928                //   identifier `a` (+1), Number `2` (+1).
7929                // Total: 3.
7930                assert_eq!(metric.abc.conditions_sum(), 3);
7931                insta::assert_json_snapshot!(metric.abc);
7932            },
7933        );
7934    }
7935
7936    #[test]
7937    fn lua_if_boolean_literal_condition() {
7938        check_metrics::<LuaParser>(
7939            "function f()\n\
7940                 if true then end                  -- +1c\n\
7941                 if not false then end             -- +1c\n\
7942                 while true do break end           -- +1c\n\
7943                 repeat break until false          -- +1c\n\
7944             end",
7945            "foo.lua",
7946            |metric| {
7947                assert_eq!(metric.abc.conditions_sum(), 4);
7948                insta::assert_json_snapshot!(metric.abc);
7949            },
7950        );
7951    }
7952
7953    #[test]
7954    fn lua_methods_arguments_with_conditions() {
7955        // `m(not a, not b)` — argument list walker counts each
7956        // unary-conditional argument once. Bare-identifier args
7957        // (`m(a, b)`) do not count (list_kind != BinaryExpression).
7958        check_metrics::<LuaParser>(
7959            "function f(a, b) m(a, b); m(not a, not b) end",
7960            "foo.lua",
7961            |metric| {
7962                assert_eq!(metric.abc.branches_sum(), 2);
7963                assert_eq!(metric.abc.conditions_sum(), 2);
7964                insta::assert_json_snapshot!(metric.abc);
7965            },
7966        );
7967    }
7968
7969    #[test]
7970    fn lua_return_with_conditions() {
7971        // `return not (z >= 0)` → walker on `not` unwraps the paren
7972        // chain and reaches the inner BinaryExpression; the inner
7973        // `>=` comparison is the actual Fitzpatrick condition.
7974        check_metrics::<LuaParser>(
7975            "function m1(z) return not (z >= 0) end\n\
7976             function m2(x) return (((not x))) end\n\
7977             function m3(x, y) return x and y end",
7978            "foo.lua",
7979            |metric| {
7980                // m1: `>=` (1). `not` wraps a paren'd
7981                //     BinaryExpression — Lua's lua_inspect_container
7982                //     reaches the inner BinaryExpression and stops,
7983                //     no walker count. +1.
7984                // m2: ReturnStatement → iterate expression_list →
7985                //     inspect_container on the outermost paren →
7986                //     unwraps to `x` in has_boolean_content-true
7987                //     (seeded by the `not`). +1.
7988                // m3: x and y → `and` walker counts both → +2.
7989                // Sum: 4.
7990                assert_eq!(metric.abc.conditions_sum(), 4);
7991                insta::assert_json_snapshot!(metric.abc);
7992            },
7993        );
7994    }
7995
7996    // ---------- Tcl ABC tests ----------
7997
7998    #[test]
7999    fn tcl_empty_unit_zero() {
8000        check_metrics::<TclParser>("", "empty.tcl", |metric| {
8001            assert_eq!(metric.abc.assignments_sum(), 0);
8002            assert_eq!(metric.abc.branches_sum(), 0);
8003            assert_eq!(metric.abc.conditions_sum(), 0);
8004            insta::assert_json_snapshot!(metric.abc);
8005        });
8006    }
8007
8008    #[test]
8009    fn tcl_set_command_counts_assignment() {
8010        // `set` has its own grammar production; each invocation is
8011        // one assignment.
8012        check_metrics::<TclParser>(
8013            "proc f {} {\n\
8014                 set x 1\n\
8015                 set y 2\n\
8016                 set x [expr {$x + $y}]\n\
8017             }",
8018            "foo.tcl",
8019            |metric| {
8020                // 3 `set` invocations → A=3. The inner `expr` is a
8021                // sub-command (`command_substitution` + `expr_cmd`),
8022                // not a `command` node, so it doesn't add a branch.
8023                assert_eq!(metric.abc.assignments_sum(), 3);
8024                assert_eq!(metric.abc.branches_sum(), 0);
8025                assert_eq!(metric.abc.conditions_sum(), 0);
8026                insta::assert_json_snapshot!(metric.abc);
8027            },
8028        );
8029    }
8030
8031    #[test]
8032    fn tcl_incr_append_lappend_count_assignment() {
8033        // Variable-mutation commands (`incr`, `append`, `lappend`)
8034        // are recognised by name and count as assignments, not
8035        // branches.
8036        check_metrics::<TclParser>(
8037            "proc f {} {\n\
8038                 set x 0\n\
8039                 incr x\n\
8040                 append s \"hi\"\n\
8041                 lappend lst 1\n\
8042             }",
8043            "foo.tcl",
8044            |metric| {
8045                // `set` (1) + `incr` (1) + `append` (1) + `lappend`
8046                // (1) → A=4. No branches, no conditions.
8047                assert_eq!(metric.abc.assignments_sum(), 4);
8048                assert_eq!(metric.abc.branches_sum(), 0);
8049                assert_eq!(metric.abc.conditions_sum(), 0);
8050                insta::assert_json_snapshot!(metric.abc);
8051            },
8052        );
8053    }
8054
8055    #[test]
8056    fn tcl_generic_commands_are_branches() {
8057        // Anything that isn't `set` or a known mutator command
8058        // counts as a branch — including builtins like `puts` and
8059        // `return`.
8060        check_metrics::<TclParser>(
8061            "proc f {} {\n\
8062                 puts \"hello\"\n\
8063                 do_work 1 2\n\
8064                 return 0\n\
8065             }",
8066            "foo.tcl",
8067            |metric| {
8068                // 3 commands, all branches.
8069                assert_eq!(metric.abc.assignments_sum(), 0);
8070                assert_eq!(metric.abc.branches_sum(), 3);
8071                assert_eq!(metric.abc.conditions_sum(), 0);
8072                insta::assert_json_snapshot!(metric.abc);
8073            },
8074        );
8075    }
8076
8077    #[test]
8078    fn tcl_comparisons_count_logical_ops_do_not() {
8079        // `expr` predicates expose comparison / logical tokens at
8080        // the leaf level. Each comparison token contributes one
8081        // condition; `&&` and `||` are NOT counted on their own
8082        // (Fitzpatrick Rule 5; #395) — instead each operand is
8083        // counted as a unary conditional by the walker (Rule 9;
8084        // #403). The two logical lines add 2 walker conditions
8085        // each (variable-substitution operands).
8086        check_metrics::<TclParser>(
8087            "proc f {a b} {\n\
8088                 set r [expr {$a == $b}]\n\
8089                 set r [expr {$a != $b}]\n\
8090                 set r [expr {$a <  $b}]\n\
8091                 set r [expr {$a >  $b}]\n\
8092                 set r [expr {$a <= $b}]\n\
8093                 set r [expr {$a >= $b}]\n\
8094                 set r [expr {$a eq $b}]\n\
8095                 set r [expr {$a ne $b}]\n\
8096                 set r [expr {$a && $b}]\n\
8097                 set r [expr {$a || $b}]\n\
8098             }",
8099            "foo.tcl",
8100            |metric| {
8101                // 10 `set` assignments.
8102                assert_eq!(metric.abc.assignments_sum(), 10);
8103                assert_eq!(metric.abc.branches_sum(), 0);
8104                // 8 comparisons (+8) + 2 logical lines × 2 walker
8105                // operands (+4) = 12.
8106                assert_eq!(metric.abc.conditions_sum(), 12);
8107                insta::assert_json_snapshot!(metric.abc);
8108            },
8109        );
8110    }
8111
8112    #[test]
8113    fn tcl_ternary_counts_condition() {
8114        // `$a ? $b : $c` inside an `expr` is one `ternary_expr`
8115        // node → 1 condition.
8116        check_metrics::<TclParser>(
8117            "proc f {a b c} {\n\
8118                 set r [expr {$a ? $b : $c}]\n\
8119             }",
8120            "foo.tcl",
8121            |metric| {
8122                assert_eq!(metric.abc.assignments_sum(), 1);
8123                assert_eq!(metric.abc.branches_sum(), 0);
8124                assert_eq!(metric.abc.conditions_sum(), 1);
8125                insta::assert_json_snapshot!(metric.abc);
8126            },
8127        );
8128    }
8129
8130    #[test]
8131    fn tcl_elseif_and_else_count_conditions() {
8132        // `if` / `elseif` / `else` clause productions each
8133        // contribute one condition. The leaf comparison inside the
8134        // predicate is counted independently.
8135        check_metrics::<TclParser>(
8136            "proc f {x} {\n\
8137                 if {$x > 0} {\n\
8138                     return 1\n\
8139                 } elseif {$x < 0} {\n\
8140                     return -1\n\
8141                 } else {\n\
8142                     return 0\n\
8143                 }\n\
8144             }",
8145            "foo.tcl",
8146            |metric| {
8147                // Branches: three `return` commands → 3.
8148                // Conditions: `>` (1), `<` (1), `elseif` (1), `else`
8149                // (1) → 4.
8150                assert_eq!(metric.abc.assignments_sum(), 0);
8151                assert_eq!(metric.abc.branches_sum(), 3);
8152                assert_eq!(metric.abc.conditions_sum(), 4);
8153                insta::assert_json_snapshot!(metric.abc);
8154            },
8155        );
8156    }
8157
8158    #[test]
8159    fn tcl_if_multiple_conditions() {
8160        // Fitzpatrick Rule 9 walker (issue #403). Tcl's `expr` slot
8161        // exposes `&&` / `||` operands as variable substitutions
8162        // (`$a`, `$b`, …) inside a `binop_expr`.
8163        check_metrics::<TclParser>(
8164            "proc f {a b c d} {\n\
8165                 if {[expr {$a || $b || $c || $d}]} { return 1 }    \n\
8166                 if {[expr {$a && $b && $c}]} { return 2 }          \n\
8167                 return 0\n\
8168             }",
8169            "foo.tcl",
8170            |metric| {
8171                // The two `expr` predicates feed the walker: 4 + 3 = 7.
8172                assert_eq!(metric.abc.conditions_sum(), 7);
8173                insta::assert_json_snapshot!(metric.abc);
8174            },
8175        );
8176    }
8177
8178    #[test]
8179    fn tcl_while_conditions() {
8180        // Tcl has no `do { ... } while(cond);` — `while {…} {…}` is
8181        // the standard loop. The walker fires on `&&` / `||` tokens
8182        // inside the `expr` predicate.
8183        check_metrics::<TclParser>(
8184            "proc f {a b} {\n\
8185                 while {[expr {$a || $b}]} { break }    \n\
8186                 while {[expr {$a && $b}]} { break }    \n\
8187             }",
8188            "foo.tcl",
8189            |metric| {
8190                assert_eq!(metric.abc.conditions_sum(), 4);
8191                insta::assert_json_snapshot!(metric.abc);
8192            },
8193        );
8194    }
8195
8196    #[test]
8197    fn tcl_short_circuit_with_boolean_literal_operand() {
8198        // `$a && 1` reports 2 conditions: a VariableSubstitution
8199        // operand plus a Number-literal operand. Confirms `Number`
8200        // is in the walker terminal set. `true` / `false` Tcl
8201        // keywords are not literal tokens in tree-sitter-tcl —
8202        // they're emitted as the operator-context word, which is
8203        // captured separately by the `Tcl::Boolean` kind for
8204        // dedicated `expr {true}` predicates but not as a `&&`
8205        // operand at this iteration; using a numeric literal keeps
8206        // the assertion grammar-stable.
8207        check_metrics::<TclParser>(
8208            "proc f {a} { return [expr {$a && 1}] }\n",
8209            "foo.tcl",
8210            |metric| {
8211                assert_eq!(metric.abc.conditions_sum(), 2);
8212                insta::assert_json_snapshot!(metric.abc);
8213            },
8214        );
8215    }
8216
8217    #[test]
8218    fn tcl_complex_function_abc() {
8219        // Mixed program covering every category. Tcl's grammar
8220        // re-parses braced content that looks command-shaped as a
8221        // nested `command` node, which inflates the branch count
8222        // relative to a naive read of the source — see breakdown.
8223        check_metrics::<TclParser>(
8224            "proc run {n} {\n\
8225                 set total 0\n\
8226                 for {set i 0} {$i < $n} {incr i} {\n\
8227                     if {$i % 2 == 0} {\n\
8228                         do_work $i\n\
8229                     } else {\n\
8230                         incr total $i\n\
8231                     }\n\
8232                 }\n\
8233                 puts \"done\"\n\
8234                 return $total\n\
8235             }",
8236            "foo.tcl",
8237            |metric| {
8238                // Assignments: `set total 0` (1), `set i 0` (1),
8239                // `incr i` (1), `incr total $i` (1) → A = 4.
8240                // Branches: the outer `for …` is one `command` node;
8241                // the `{$i < $n}` predicate ALSO re-parses as a
8242                // `command` node (tree-sitter-tcl treats braced
8243                // predicates as nested commands at the pinned
8244                // grammar version); plus `do_work $i`, `puts
8245                // "done"`, and `return $total`. The for-loop body's
8246                // `incr` and `incr total $i` are assignment commands
8247                // and don't add branches. Total B = 5.
8248                // Conditions: `==` (1) and `else` (1) → C = 2. The
8249                // `<` inside `{$i < $n}` is NOT `Tcl::LT`: because
8250                // that predicate re-parses as a `command`, the `<`
8251                // is emitted as `simple_word`. Only `<` inside a
8252                // real `expr` production becomes `Tcl::LT`.
8253                assert_eq!(metric.abc.assignments_sum(), 4);
8254                assert_eq!(metric.abc.branches_sum(), 5);
8255                assert_eq!(metric.abc.conditions_sum(), 2);
8256                insta::assert_json_snapshot!(metric.abc);
8257            },
8258        );
8259    }
8260
8261    /// The dedicated `set name value` production counts as one assignment.
8262    #[test]
8263    fn irules_abc_set_assignment() {
8264        check_metrics::<IrulesParser>("when X {\n    set x 1\n}\n", "foo.irule", |metric| {
8265            assert_eq!(metric.abc.assignments_sum(), 1);
8266            assert_eq!(metric.abc.branches_sum(), 0);
8267            assert_eq!(metric.abc.conditions_sum(), 0);
8268        });
8269    }
8270
8271    /// Mutator commands (`incr` / `append` / `lappend`) count as
8272    /// assignments, not branches — iRules has no assignment operators, so
8273    /// mutation is always a command invocation.
8274    #[test]
8275    fn irules_abc_mutator_commands() {
8276        check_metrics::<IrulesParser>(
8277            "when X {\n    incr x\n    append s \"y\"\n    lappend l 1\n}\n",
8278            "foo.irule",
8279            |metric| {
8280                assert_eq!(metric.abc.assignments_sum(), 3);
8281                assert_eq!(metric.abc.branches_sum(), 0);
8282            },
8283        );
8284    }
8285
8286    /// Generic (non-mutator) commands count as branches.
8287    #[test]
8288    fn irules_abc_branch_commands() {
8289        check_metrics::<IrulesParser>(
8290            "when X {\n    log local0. hi\n    pool p1\n}\n",
8291            "foo.irule",
8292            |metric| {
8293                assert_eq!(metric.abc.assignments_sum(), 0);
8294                assert_eq!(metric.abc.branches_sum(), 2);
8295                assert_eq!(metric.abc.conditions_sum(), 0);
8296            },
8297        );
8298    }
8299
8300    /// A numeric comparison (`==`) is one condition; the `log` inside the
8301    /// `if` body is one branch.
8302    #[test]
8303    fn irules_abc_comparison_condition() {
8304        check_metrics::<IrulesParser>(
8305            "when X {\n    if { $a == 1 } { log local0. hi }\n}\n",
8306            "foo.irule",
8307            |metric| {
8308                assert_eq!(metric.abc.branches_sum(), 1);
8309                assert_eq!(metric.abc.conditions_sum(), 1);
8310            },
8311        );
8312    }
8313
8314    /// A word-form string comparator (`contains`) is a condition just like
8315    /// `==` — iRules-specific (Tcl has only `eq`/`ne`/`in`/`ni`). If
8316    /// `contains` were dropped from the condition set this would report 0.
8317    #[test]
8318    fn irules_abc_string_op_condition() {
8319        check_metrics::<IrulesParser>(
8320            "when X {\n    if { $a contains \"x\" } { log local0. hi }\n}\n",
8321            "foo.irule",
8322            |metric| {
8323                assert_eq!(metric.abc.branches_sum(), 1);
8324                assert_eq!(metric.abc.conditions_sum(), 1);
8325            },
8326        );
8327    }
8328
8329    /// Each `elseif` / `else` clause is one condition; the three `set`s are
8330    /// assignments. The leading `if` is not itself a condition.
8331    #[test]
8332    fn irules_abc_elseif_else_conditions() {
8333        check_metrics::<IrulesParser>(
8334            "when X {\n    if { $a } { set r 1 } elseif { $b } { set r 2 } else { set r 3 }\n}\n",
8335            "foo.irule",
8336            |metric| {
8337                assert_eq!(metric.abc.assignments_sum(), 3);
8338                assert_eq!(metric.abc.conditions_sum(), 2);
8339            },
8340        );
8341    }
8342
8343    /// A ternary contributes its own condition plus the `>` comparison in
8344    /// its test: conditions 2; the `set` is one assignment.
8345    #[test]
8346    fn irules_abc_ternary_condition() {
8347        check_metrics::<IrulesParser>(
8348            "when X {\n    set y [expr { $a > 0 ? 1 : 0 }]\n}\n",
8349            "foo.irule",
8350            |metric| {
8351                assert_eq!(metric.abc.assignments_sum(), 1);
8352                assert_eq!(metric.abc.conditions_sum(), 2);
8353            },
8354        );
8355    }
8356
8357    /// Fitzpatrick Rule 9: the short-circuit `&&` is not itself a condition,
8358    /// but each negated bare operand (`!$a`, `!$b`) in the chain is. Guards
8359    /// the `irules_count_unary_conditions` / `irules_inspect_container`
8360    /// walker — conditions 2.
8361    #[test]
8362    fn irules_abc_negated_operands_in_chain() {
8363        check_metrics::<IrulesParser>(
8364            "when X {\n    if { !$a && !$b } { log local0. hi }\n}\n",
8365            "foo.irule",
8366            |metric| {
8367                assert_eq!(metric.abc.branches_sum(), 1);
8368                assert_eq!(metric.abc.conditions_sum(), 2);
8369            },
8370        );
8371    }
8372
8373    /// A bare-truthy `if {$a}` condition carries no comparison, ternary,
8374    /// or short-circuit operator, so the unary-conditional walker is
8375    /// never invoked and conditions stay 0 — iRules does not wire the
8376    /// broader Phase 2B slot routing that counts bare-truthy operands.
8377    /// The `log` command is the single branch. Pins the metrics-book
8378    /// claim in the ABC per-language deviation table.
8379    #[test]
8380    fn irules_abc_bare_truthy_reports_zero() {
8381        check_metrics::<IrulesParser>(
8382            "when X {\n    if { $a } { log local0. hi }\n}\n",
8383            "foo.irule",
8384            |metric| {
8385                assert_eq!(metric.abc.branches_sum(), 1);
8386                assert_eq!(metric.abc.conditions_sum(), 0);
8387            },
8388        );
8389    }
8390}