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