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 serde::Serialize;
25use serde::ser::{SerializeStruct, Serializer};
26use std::fmt;
27
28use crate::checker::Checker;
29use crate::macros::{
30    csharp_invocation_expr_kinds, csharp_paren_expr_kinds, csharp_prefix_unary_expr_kinds,
31    implement_metric_trait,
32};
33use crate::node::Node;
34use crate::*;
35
36/// The `ABC` metric.
37///
38/// The `ABC` metric measures the size of a source code by counting
39/// the number of Assignments (`A`), Branches (`B`) and Conditions (`C`).
40/// The metric defines an ABC score as a vector of three elements (`<A,B,C>`).
41/// The ABC score can be represented by its individual components (`A`, `B` and `C`)
42/// or by the magnitude of the vector (`|<A,B,C>| = sqrt(A^2 + B^2 + C^2)`).
43///
44/// Official paper and definition:
45///
46/// Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report.
47///
48/// <https://www.softwarerenovation.com/Articles.aspx>
49#[derive(Debug, Clone)]
50pub struct Stats {
51    assignments: f64,
52    assignments_sum: f64,
53    assignments_min: f64,
54    assignments_max: f64,
55    branches: f64,
56    branches_sum: f64,
57    branches_min: f64,
58    branches_max: f64,
59    conditions: f64,
60    conditions_sum: f64,
61    conditions_min: f64,
62    conditions_max: f64,
63    space_count: usize,
64    declaration: Vec<DeclKind>,
65}
66
67#[derive(Debug, Clone)]
68enum DeclKind {
69    Var,
70    Const,
71}
72
73impl Default for Stats {
74    fn default() -> Self {
75        Self {
76            assignments: 0.,
77            assignments_sum: 0.,
78            assignments_min: f64::MAX,
79            assignments_max: 0.,
80            branches: 0.,
81            branches_sum: 0.,
82            branches_min: f64::MAX,
83            branches_max: 0.,
84            conditions: 0.,
85            conditions_sum: 0.,
86            conditions_min: f64::MAX,
87            conditions_max: 0.,
88            space_count: 1,
89            declaration: Vec::new(),
90        }
91    }
92}
93
94impl Serialize for Stats {
95    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
96    where
97        S: Serializer,
98    {
99        let mut st = serializer.serialize_struct("abc", 13)?;
100        st.serialize_field("assignments", &self.assignments_sum())?;
101        st.serialize_field("branches", &self.branches_sum())?;
102        st.serialize_field("conditions", &self.conditions_sum())?;
103        st.serialize_field("magnitude", &self.magnitude_sum())?;
104        st.serialize_field("assignments_average", &self.assignments_average())?;
105        st.serialize_field("branches_average", &self.branches_average())?;
106        st.serialize_field("conditions_average", &self.conditions_average())?;
107        st.serialize_field("assignments_min", &self.assignments_min())?;
108        st.serialize_field("assignments_max", &self.assignments_max())?;
109        st.serialize_field("branches_min", &self.branches_min())?;
110        st.serialize_field("branches_max", &self.branches_max())?;
111        st.serialize_field("conditions_min", &self.conditions_min())?;
112        st.serialize_field("conditions_max", &self.conditions_max())?;
113        st.end()
114    }
115}
116
117impl fmt::Display for Stats {
118    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
119        write!(
120            f,
121            "assignments: {}, branches: {}, conditions: {}, magnitude: {}, \
122            assignments_average: {}, branches_average: {}, conditions_average: {}, \
123            assignments_min: {}, assignments_max: {}, \
124            branches_min: {}, branches_max: {}, \
125            conditions_min: {}, conditions_max: {}",
126            self.assignments_sum(),
127            self.branches_sum(),
128            self.conditions_sum(),
129            self.magnitude_sum(),
130            self.assignments_average(),
131            self.branches_average(),
132            self.conditions_average(),
133            self.assignments_min(),
134            self.assignments_max(),
135            self.branches_min(),
136            self.branches_max(),
137            self.conditions_min(),
138            self.conditions_max()
139        )
140    }
141}
142
143impl Stats {
144    /// Merges a second `Abc` metric into the first one.
145    pub fn merge(&mut self, other: &Stats) {
146        // Calculates minimum and maximum values
147        self.assignments_min = self.assignments_min.min(other.assignments_min);
148        self.assignments_max = self.assignments_max.max(other.assignments_max);
149        self.branches_min = self.branches_min.min(other.branches_min);
150        self.branches_max = self.branches_max.max(other.branches_max);
151        self.conditions_min = self.conditions_min.min(other.conditions_min);
152        self.conditions_max = self.conditions_max.max(other.conditions_max);
153
154        self.assignments_sum += other.assignments_sum;
155        self.branches_sum += other.branches_sum;
156        self.conditions_sum += other.conditions_sum;
157
158        self.space_count += other.space_count;
159    }
160
161    /// Returns the `Abc` assignments metric value.
162    #[must_use]
163    pub fn assignments(&self) -> f64 {
164        self.assignments
165    }
166
167    /// Returns the `Abc` assignments sum metric value.
168    #[must_use]
169    pub fn assignments_sum(&self) -> f64 {
170        self.assignments_sum
171    }
172
173    /// Returns the `Abc` assignments average value.
174    ///
175    /// This value is computed dividing the `Abc`
176    /// assignments value for the number of spaces.
177    #[must_use]
178    pub fn assignments_average(&self) -> f64 {
179        self.assignments_sum() / self.space_count as f64
180    }
181
182    /// Returns the `Abc` assignments minimum value.
183    ///
184    /// Collapses the `f64::MAX` sentinel that `Stats::default()` plants
185    /// into `assignments_min` to `0.0`, so a never-observed space
186    /// serializes to a meaningful number rather than `1.7976931e308`.
187    #[allow(clippy::float_cmp)]
188    #[must_use]
189    pub fn assignments_min(&self) -> f64 {
190        if self.assignments_min == f64::MAX {
191            0.0
192        } else {
193            self.assignments_min
194        }
195    }
196
197    /// Returns the `Abc` assignments maximum value.
198    #[must_use]
199    pub fn assignments_max(&self) -> f64 {
200        self.assignments_max
201    }
202
203    /// Returns the `Abc` branches metric value.
204    #[must_use]
205    pub fn branches(&self) -> f64 {
206        self.branches
207    }
208
209    /// Returns the `Abc` branches sum metric value.
210    #[must_use]
211    pub fn branches_sum(&self) -> f64 {
212        self.branches_sum
213    }
214
215    /// Returns the `Abc` branches average value.
216    ///
217    /// This value is computed dividing the `Abc`
218    /// branches value for the number of spaces.
219    #[must_use]
220    pub fn branches_average(&self) -> f64 {
221        self.branches_sum() / self.space_count as f64
222    }
223
224    /// Returns the `Abc` branches minimum value.
225    ///
226    /// Same `f64::MAX` sentinel collapse as `assignments_min`.
227    #[allow(clippy::float_cmp)]
228    #[must_use]
229    pub fn branches_min(&self) -> f64 {
230        if self.branches_min == f64::MAX {
231            0.0
232        } else {
233            self.branches_min
234        }
235    }
236
237    /// Returns the `Abc` branches maximum value.
238    #[must_use]
239    pub fn branches_max(&self) -> f64 {
240        self.branches_max
241    }
242
243    /// Returns the `Abc` conditions metric value.
244    #[must_use]
245    pub fn conditions(&self) -> f64 {
246        self.conditions
247    }
248
249    /// Returns the `Abc` conditions sum metric value.
250    #[must_use]
251    pub fn conditions_sum(&self) -> f64 {
252        self.conditions_sum
253    }
254
255    /// Returns the `Abc` conditions average value.
256    ///
257    /// This value is computed dividing the `Abc`
258    /// conditions value for the number of spaces.
259    #[must_use]
260    pub fn conditions_average(&self) -> f64 {
261        self.conditions_sum() / self.space_count as f64
262    }
263
264    /// Returns the `Abc` conditions minimum value.
265    ///
266    /// Same `f64::MAX` sentinel collapse as `assignments_min`.
267    #[allow(clippy::float_cmp)]
268    #[must_use]
269    pub fn conditions_min(&self) -> f64 {
270        if self.conditions_min == f64::MAX {
271            0.0
272        } else {
273            self.conditions_min
274        }
275    }
276
277    /// Returns the `Abc` conditions maximum value.
278    #[must_use]
279    pub fn conditions_max(&self) -> f64 {
280        self.conditions_max
281    }
282
283    /// Returns the `Abc` magnitude metric value.
284    #[must_use]
285    pub fn magnitude(&self) -> f64 {
286        (self.assignments.powi(2) + self.branches.powi(2) + self.conditions.powi(2)).sqrt()
287    }
288
289    /// Returns the `Abc` magnitude sum metric value.
290    #[must_use]
291    pub fn magnitude_sum(&self) -> f64 {
292        (self.assignments_sum.powi(2) + self.branches_sum.powi(2) + self.conditions_sum.powi(2))
293            .sqrt()
294    }
295
296    #[inline]
297    pub(crate) fn compute_sum(&mut self) {
298        self.assignments_sum += self.assignments;
299        self.branches_sum += self.branches;
300        self.conditions_sum += self.conditions;
301    }
302
303    #[inline]
304    pub(crate) fn compute_minmax(&mut self) {
305        self.assignments_min = self.assignments_min.min(self.assignments);
306        self.assignments_max = self.assignments_max.max(self.assignments);
307        self.branches_min = self.branches_min.min(self.branches);
308        self.branches_max = self.branches_max.max(self.branches);
309        self.conditions_min = self.conditions_min.min(self.conditions);
310        self.conditions_max = self.conditions_max.max(self.conditions);
311        self.compute_sum();
312    }
313}
314
315#[doc(hidden)]
316/// Per-language computation of the ABC metric.
317pub trait Abc
318where
319    Self: Checker,
320{
321    /// Walk `node` and update `stats` with this metric for the language
322    /// implementing the trait.
323    ///
324    /// `code` is the source bytes underlying the parsed tree. Most
325    /// languages ignore it: assignments, branches, and conditions all
326    /// surface as distinct grammar productions and a `kind_id()` match
327    /// is enough. Elixir is the exception — `case` / `cond` / `if` /
328    /// `with` / guard `when` arms surface as `Call` nodes whose keyword
329    /// target lives only in the source text. Matching the `Cyclomatic`
330    /// / `Halstead` / `Exit` / `Cognitive` pattern keeps the signature
331    /// uniform.
332    fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats);
333}
334
335// Inspects the content of Java parenthesized expressions
336// and `Not` operators to find unary conditional expressions
337fn java_inspect_container(container_node: &Node, conditions: &mut f64) {
338    use Java::*;
339
340    let mut node = *container_node;
341    let mut node_kind = node.kind_id().into();
342
343    // Initializes the flag to true if the container is known to contain a boolean value
344    let Some(parent) = node.parent() else { return };
345    let mut has_boolean_content = match parent.kind_id().into() {
346        BinaryExpression | IfStatement | WhileStatement | DoStatement | ForStatement => true,
347        TernaryExpression => node
348            .previous_sibling()
349            .is_none_or(|prev_node| !matches!(prev_node.kind_id().into(), QMARK | COLON)),
350        _ => false,
351    };
352
353    // Looks inside parenthesized expressions and `Not` operators to find what they contain
354    loop {
355        // Checks if the node is a parenthesized expression or a `Not` operator
356        // The child node of index 0 contains the unary expression operator (we look for the `!` operator)
357        let is_parenthesised_exp = matches!(node_kind, ParenthesizedExpression);
358        let is_not_operator = matches!(node_kind, UnaryExpression)
359            && node
360                .child(0)
361                .is_some_and(|c| matches!(c.kind_id().into(), BANG));
362
363        // Stops the exploration if the node is neither
364        // a parenthesized expression nor a `Not` operator
365        if !is_parenthesised_exp && !is_not_operator {
366            break;
367        }
368
369        // Sets the flag to true if a `Not` operator is found
370        // This is used to prove if a variable or a value returned by a method is actually boolean
371        // e.g. `return (!x);`
372        if !has_boolean_content && is_not_operator {
373            has_boolean_content = true;
374        }
375
376        // Parenthesized expressions and `Not` operators nodes
377        // always store their expressions in the children nodes of index one
378        // https://github.com/tree-sitter/tree-sitter-java/blob/master/src/grammar.json#L2472
379        // https://github.com/tree-sitter/tree-sitter-java/blob/master/src/grammar.json#L2150
380        let Some(child) = node.child(1) else { break };
381        node = child;
382        node_kind = node.kind_id().into();
383
384        // Stops the exploration when the content is found
385        if matches!(node_kind, MethodInvocation | Identifier | True | False) {
386            if has_boolean_content {
387                *conditions += 1.;
388            }
389            break;
390        }
391    }
392}
393
394// C# analogue of `java_inspect_container`: walks parenthesised expressions
395// and `!` (PrefixUnaryExpression) wrappers to surface a unary boolean
396// condition contained within.
397fn csharp_inspect_container(container_node: &Node, conditions: &mut f64) {
398    use Csharp::*;
399
400    let mut node = *container_node;
401    let mut node_kind = node.kind_id().into();
402
403    // Seed the boolean-context flag from the parent: known-boolean
404    // contexts (loop / if / binary expression) imply the contained
405    // expression evaluates as a condition.
406    let Some(parent) = node.parent() else { return };
407    let mut has_boolean_content = match parent.kind_id().into() {
408        BinaryExpression | IfStatement | WhileStatement | DoStatement | ForStatement => true,
409        ConditionalExpression => node
410            .previous_sibling()
411            .is_none_or(|prev| !matches!(prev.kind_id().into(), QMARK | COLON)),
412        _ => false,
413    };
414
415    // Walk down through `(...)` and `!...` wrappers until we either hit
416    // the underlying operand or run out of nesting. The C# grammar
417    // aliases each of these kinds across multiple `kind_id`s
418    // (lesson #2): match every numbered variant.
419    loop {
420        let is_parens = matches!(node_kind, csharp_paren_expr_kinds!());
421        let is_not = matches!(node_kind, csharp_prefix_unary_expr_kinds!())
422            && node
423                .child(0)
424                .is_some_and(|c| matches!(c.kind_id().into(), BANG));
425
426        if !is_parens && !is_not {
427            break;
428        }
429
430        // A `!` wrapper proves the contained value is boolean even
431        // when the parent context didn't (e.g. `return !x;`).
432        if !has_boolean_content && is_not {
433            has_boolean_content = true;
434        }
435
436        // Both `parenthesized_expression` and `prefix_unary_expression`
437        // store their inner expression at child index 1.
438        let Some(child) = node.child(1) else { break };
439        node = child;
440        node_kind = node.kind_id().into();
441
442        // Found the innermost operand; count it if a boolean context
443        // was established up the chain.
444        if matches!(
445            node_kind,
446            crate::Csharp::InvocationExpression
447                | crate::Csharp::InvocationExpression2
448                | crate::Csharp::InvocationExpression3
449                | Identifier
450                | True
451                | False
452        ) {
453            if has_boolean_content {
454                *conditions += 1.;
455            }
456            break;
457        }
458    }
459}
460
461fn csharp_count_unary_conditions(list_node: &Node, conditions: &mut f64) {
462    use Csharp::*;
463
464    let list_kind = list_node.kind_id().into();
465    let mut cursor = list_node.cursor();
466
467    if cursor.goto_first_child() {
468        loop {
469            let node = cursor.node();
470            let node_kind = node.kind_id().into();
471
472            if matches!(
473                node_kind,
474                crate::Csharp::InvocationExpression
475                    | crate::Csharp::InvocationExpression2
476                    | crate::Csharp::InvocationExpression3
477                    | Identifier
478                    | True
479                    | False
480            ) && matches!(list_kind, BinaryExpression)
481            {
482                *conditions += 1.;
483            } else {
484                csharp_inspect_container(&node, conditions);
485            }
486
487            if !cursor.goto_next_sibling() {
488                break;
489            }
490        }
491    }
492}
493
494// Inspects a list of elements and counts any unary conditional expression found
495fn java_count_unary_conditions(list_node: &Node, conditions: &mut f64) {
496    use Java::*;
497
498    let list_kind = list_node.kind_id().into();
499    let mut cursor = list_node.cursor();
500
501    // Scans the immediate children nodes of the argument node
502    if cursor.goto_first_child() {
503        loop {
504            // Gets the current child node and its kind
505            let node = cursor.node();
506            let node_kind = node.kind_id().into();
507
508            // Checks if the node is a unary condition
509            if matches!(node_kind, MethodInvocation | Identifier | True | False)
510                && matches!(list_kind, BinaryExpression)
511            {
512                *conditions += 1.;
513            } else {
514                // Checks if the node is a unary condition container
515                java_inspect_container(&node, conditions);
516            }
517
518            // Moves the cursor to the next sibling node of the current node
519            // Exits the scan if there is no next sibling node
520            if !cursor.goto_next_sibling() {
521                break;
522            }
523        }
524    }
525}
526
527// Groovy mirror of `java_inspect_container`. The dekobon Groovy grammar
528// inherits `parenthesized_expression`, `unary_expression`, and the
529// standard boolean-context kinds from tree-sitter-java verbatim, so the
530// body is structurally identical to Java's helper. The terminal set
531// includes `CommandChain` (the new grammar's distinct node for Groovy's
532// parens-less call form `println foo`), keeping it in sync with the
533// `impl Abc for GroovyCode` branches dispatch.
534fn groovy_inspect_container(container_node: &Node, conditions: &mut f64) {
535    use Groovy::*;
536
537    let mut node = *container_node;
538    let mut node_kind = node.kind_id().into();
539
540    let Some(parent) = node.parent() else { return };
541    let mut has_boolean_content = match parent.kind_id().into() {
542        BinaryExpression | IfStatement | WhileStatement | DoWhileStatement | ForStatement => true,
543        TernaryExpression => node
544            .previous_sibling()
545            .is_none_or(|prev_node| !matches!(prev_node.kind_id().into(), QMARK | COLON)),
546        _ => false,
547    };
548
549    loop {
550        let is_parenthesised_exp = matches!(node_kind, ParenthesizedExpression);
551        let is_not_operator = matches!(node_kind, UnaryExpression)
552            && node
553                .child(0)
554                .is_some_and(|c| matches!(c.kind_id().into(), BANG));
555
556        if !is_parenthesised_exp && !is_not_operator {
557            break;
558        }
559
560        if !has_boolean_content && is_not_operator {
561            has_boolean_content = true;
562        }
563
564        let Some(child) = node.child(1) else { break };
565        node = child;
566        node_kind = node.kind_id().into();
567
568        if matches!(
569            node_kind,
570            MethodInvocation | CommandChain | Identifier | True | False
571        ) {
572            if has_boolean_content {
573                *conditions += 1.;
574            }
575            break;
576        }
577    }
578}
579
580fn groovy_count_unary_conditions(list_node: &Node, conditions: &mut f64) {
581    use Groovy::*;
582
583    let list_kind = list_node.kind_id().into();
584    let mut cursor = list_node.cursor();
585
586    if cursor.goto_first_child() {
587        loop {
588            let node = cursor.node();
589            let node_kind = node.kind_id().into();
590
591            if matches!(
592                node_kind,
593                MethodInvocation | CommandChain | Identifier | True | False
594            ) && matches!(list_kind, BinaryExpression)
595            {
596                *conditions += 1.;
597            } else {
598                groovy_inspect_container(&node, conditions);
599            }
600
601            if !cursor.goto_next_sibling() {
602                break;
603            }
604        }
605    }
606}
607
608// Default no-op `Abc` impls. Audited in #188; the matrix below
609// records the rationale for every entry so the no-op default is a
610// deliberate choice, not scaffolding leftover.
611//
612// Real defaults (the language has no construct ABC measures, so the
613// metric is genuinely 0):
614//   - PreprocCode, CcommentCode: no executable code (comments /
615//     preprocessor lines only).
616implement_metric_trait!(Abc, PreprocCode, CcommentCode);
617
618// TypeScript / TSX share the same expression / statement vocabulary; the
619// `ts_abc_compute!` macro expands the same token-level Fitzpatrick rules
620// for both. Compared with the Java / C# impls we stay at the leaf-token
621// level rather than walking parenthesised / unary containers — TS source
622// rarely uses C-style `if (x)` conditions, so the
623// "unary-boolean-in-a-container" heuristic adds noise without catching
624// many real conditions. Conditions still capture every comparison and
625// control-flow arm.
626//
627// Declaration sentinel: `lexical_declaration` and `variable_declaration`
628// push a `Var` sentinel that suppresses counting the initializer `=` as
629// an assignment. The `Const` token promotes to `Const` (compile-time
630// constant — initializer is not a mutable assignment). `let` and `var`
631// keep the `Var` slot. Augmented assignments (`+=`) and update
632// expressions (`++`, `--`) always count.
633macro_rules! ts_abc_compute {
634    ($lang:ident) => {
635        fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
636            use $lang::*;
637
638            match node.kind_id().into() {
639                // Augmented assignments and pre/post increment/decrement
640                // always count.
641                PLUSEQ | DASHEQ | STAREQ | SLASHEQ | PERCENTEQ | STARSTAREQ | AMPEQ | PIPEEQ
642                | CARETEQ | LTLTEQ | GTGTEQ | GTGTGTEQ | AMPAMPEQ | PIPEPIPEEQ | QMARKQMARKEQ
643                | PLUSPLUS | DASHDASH => {
644                    stats.assignments += 1.;
645                }
646                // Variable declarations push a `Var` sentinel; the `Const`
647                // keyword promotes the top to `Const` so the initializer
648                // `=` is treated as a constant binding.
649                LexicalDeclaration | VariableDeclaration => {
650                    stats.declaration.push(DeclKind::Var);
651                }
652                Const => {
653                    if let Some(DeclKind::Var) = stats.declaration.last() {
654                        stats.declaration.push(DeclKind::Const);
655                    }
656                }
657                SEMI => {
658                    if let Some(DeclKind::Const | DeclKind::Var) = stats.declaration.last() {
659                        stats.declaration.clear();
660                    }
661                }
662                // Plain `=` outside `const` declarations is an assignment.
663                EQ if !matches!(stats.declaration.last(), Some(DeclKind::Const)) => {
664                    stats.assignments += 1.;
665                }
666                // Function invocation and object construction count as
667                // branches. Member calls and chained calls all surface
668                // as `CallExpression`.
669                CallExpression | NewExpression => {
670                    stats.branches += 1.;
671                }
672                // Comparison and equality operators, ternary `?`, `??`,
673                // `instanceof`, `else`, `case`, `default`, `catch`,
674                // `try`.
675                EQEQ | EQEQEQ | BANGEQ | BANGEQEQ | LTEQ | GTEQ | QMARK | QMARKQMARK
676                | Instanceof | Else | Case | Default | Try | Catch => {
677                    stats.conditions += 1.;
678                }
679                // `<` and `>` may also delimit type arguments / type
680                // parameters (`Array<number>`, `class Foo<T> {}`); skip
681                // those, count only comparison usage.
682                GT | LT
683                    if node.parent().is_some_and(|p| {
684                        !matches!(p.kind_id().into(), TypeArguments | TypeParameters)
685                    }) =>
686                {
687                    stats.conditions += 1.;
688                }
689                _ => {}
690            }
691        }
692    };
693}
694
695impl Abc for TypescriptCode {
696    ts_abc_compute!(Typescript);
697}
698
699impl Abc for TsxCode {
700    ts_abc_compute!(Tsx);
701}
702
703// JavaScript / Mozjs share TypeScript's expression / statement
704// vocabulary. The `js_abc_compute!` macro expands the same
705// token-level Fitzpatrick rules as `ts_abc_compute!`, with two
706// adjustments:
707//
708//   1. `LT` / `GT` are always comparison operators in plain JS — there
709//      are no `TypeArguments` / `TypeParameters` nodes to gate against.
710//   2. JS retains the same `LexicalDeclaration` / `VariableDeclaration`
711//      sentinel handling so `const x = 5` does not double-count the
712//      initializer `=` as an assignment. `let x = 5` and `var x = 5`
713//      DO count their initializer `=` as an assignment — only `const`
714//      suppresses, matching the TS impl above. This deliberately
715//      deviates from a strict reading of Fitzpatrick's "declaration
716//      initialiser is not an assignment" rule because `let`/`var`
717//      bindings can be reassigned and the initial value is the first
718//      assignment of the binding's lifetime.
719macro_rules! js_abc_compute {
720    ($lang:ident) => {
721        fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
722            use $lang::*;
723
724            match node.kind_id().into() {
725                PLUSEQ | DASHEQ | STAREQ | SLASHEQ | PERCENTEQ | STARSTAREQ | AMPEQ | PIPEEQ
726                | CARETEQ | LTLTEQ | GTGTEQ | GTGTGTEQ | AMPAMPEQ | PIPEPIPEEQ | QMARKQMARKEQ
727                | PLUSPLUS | DASHDASH => {
728                    stats.assignments += 1.;
729                }
730                LexicalDeclaration | VariableDeclaration => {
731                    stats.declaration.push(DeclKind::Var);
732                }
733                Const => {
734                    if let Some(DeclKind::Var) = stats.declaration.last() {
735                        stats.declaration.push(DeclKind::Const);
736                    }
737                }
738                SEMI => {
739                    if let Some(DeclKind::Const | DeclKind::Var) = stats.declaration.last() {
740                        stats.declaration.clear();
741                    }
742                }
743                EQ if !matches!(stats.declaration.last(), Some(DeclKind::Const)) => {
744                    stats.assignments += 1.;
745                }
746                CallExpression | NewExpression => {
747                    stats.branches += 1.;
748                }
749                EQEQ | EQEQEQ | BANGEQ | BANGEQEQ | LTEQ | GTEQ | LT | GT | QMARK | QMARKQMARK
750                | Instanceof | Else | Case | Default | Try | Catch => {
751                    stats.conditions += 1.;
752                }
753                _ => {}
754            }
755        }
756    };
757}
758
759impl Abc for JavascriptCode {
760    js_abc_compute!(Javascript);
761}
762
763impl Abc for MozjsCode {
764    js_abc_compute!(Mozjs);
765}
766
767// Fitzpatrick's ABC rules adapted for Kotlin syntax. Kotlin shares the
768// JVM and Java's spec roots: assignments count once per `=` / augmented
769// assignment / ++ / --, branches count once per function invocation or
770// object construction, conditions count comparison operators plus the
771// `else` / `when`-entry / `catch` arms. Compared with the Java impl we
772// stay token-level (matching the leaf kind_ids) rather than walking
773// `Modifiers` children; the Kotlin grammar exposes the relevant
774// operators directly as token nodes inside `binary_expression`,
775// `assignment`, `prefix_expression`, and `postfix_expression`.
776impl Abc for KotlinCode {
777    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
778        use Kotlin::*;
779
780        match node.kind_id().into() {
781            // Property / local-variable declaration and primary-constructor
782            // parameter property (`class C(val a: Int = 5)`) both push a
783            // sentinel so the `=` operator initialising the binding is NOT
784            // counted as a standalone assignment (Fitzpatrick:
785            // "initialisation is part of the declaration", mirroring Java).
786            // The `Val` keyword arm below promotes the sentinel to `Const`
787            // for immutable bindings.
788            PropertyDeclaration | ClassParameter => {
789                stats.declaration.push(DeclKind::Var);
790            }
791            // `val` introduces an immutable binding; promote the pending
792            // declaration to `Const` so the upcoming `=` is suppressed
793            // (constants are not assignments in ABC).
794            Val => {
795                if let Some(DeclKind::Var) = stats.declaration.last() {
796                    stats.declaration.push(DeclKind::Const);
797                }
798            }
799            // Statement terminator: the grammar emits an explicit `SEMI`
800            // only for explicit semicolons. Property declarations also
801            // terminate without one when the next token starts a new
802            // statement. We clear the sentinel on the explicit `SEMI`
803            // here; the implicit-terminator case is benign because the
804            // EQ arm reads only `last()`, which is the most recently
805            // pushed sentinel — any older entries left on the stack
806            // from preceding implicit terminators do not affect the
807            // assignment count.
808            SEMI => {
809                if let Some(DeclKind::Const | DeclKind::Var) = stats.declaration.last() {
810                    stats.declaration.clear();
811                }
812            }
813            // Augmented assignments and pre/post increment-decrement
814            // always count, regardless of declaration context.
815            PLUSEQ | DASHEQ | STAREQ | SLASHEQ | PERCENTEQ | PLUSPLUS | DASHDASH => {
816                stats.assignments += 1.;
817            }
818            // Plain `=` token. Skip when inside a `val` declaration; count
819            // when inside a `var` declaration (initialiser of mutable
820            // binding) or a standalone `Assignment`. The DeclKind stack is
821            // cleared at the property statement boundary above.
822            EQ if stats
823                .declaration
824                .last()
825                .is_none_or(|decl| matches!(decl, DeclKind::Var)) =>
826            {
827                stats.assignments += 1.;
828            }
829            // Branches: every call expression plus object construction.
830            // Kotlin's `new` is implicit — `Foo()` parses as
831            // `CallExpression` with a type-named receiver. The
832            // Halstead-side classification treats it uniformly. Indexed
833            // access (`arr[i]`) is NOT a branch (it's an operator on a
834            // sequence), matching the Java rule of "method invocation
835            // only".
836            CallExpression => {
837                stats.branches += 1.;
838            }
839            // Conditions: comparison operators, identity equality,
840            // ternary-elvis (`?:`), `as?` safe-cast, and the arms of
841            // control-flow constructs (`else`, `catch`, `when` entries).
842            // Kotlin's `if`-expression does not need an extra count for
843            // the `if` keyword itself — Fitzpatrick counts the
844            // *conditions*, and the unary condition is already implicit
845            // in the boolean operand. We add the `if` arm via the `Else`
846            // keyword for else-branches and via `WhenEntry` for `when`.
847            LTEQ | GTEQ | EQEQ | EQEQEQ | BANGEQ | BANGEQEQ | WhenEntry | CatchBlock
848            | QMARKCOLON | AsQMARK => {
849                stats.conditions += 1.;
850            }
851            // `else` is a keyword token used in both `if_expression`'s
852            // else-clause and `when`'s `else ->` entry. Only count it
853            // when it belongs to an `if_expression`; the `WhenEntry`
854            // wrapper above already covers the `when` case.
855            Else if node.parent().is_some_and(|p| p.kind_id() == IfExpression) => {
856                stats.conditions += 1.;
857            }
858            // `<` and `>` may appear as type-argument brackets
859            // (`List<Int>`); exclude those by checking the parent kind.
860            LT | GT
861                if node.parent().is_some_and(|p| {
862                    !matches!(p.kind_id().into(), TypeArguments | TypeParameters)
863                }) =>
864            {
865                stats.conditions += 1.;
866            }
867            _ => {}
868        }
869    }
870}
871
872impl Abc for PhpCode {
873    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
874        use Php::*;
875
876        match node.kind_id().into() {
877            // Assignments: explicit assignment expressions and augmented forms,
878            // plus pre/post increment and decrement. `const_declaration` and
879            // `enum_case` use their own `const_element` / value-assignment
880            // shapes, so they do not produce `AssignmentExpression` nodes —
881            // matching the assignment-expression kinds naturally excludes
882            // them.
883            AssignmentExpression
884            | AugmentedAssignmentExpression
885            | ReferenceAssignmentExpression
886            | PLUSPLUS
887            | DASHDASH => {
888                stats.assignments += 1.;
889            }
890            // Branches: every PHP call kind plus object construction.
891            FunctionCallExpression
892            | MemberCallExpression
893            | ScopedCallExpression
894            | NullsafeMemberCallExpression
895            | ObjectCreationExpression => {
896                stats.branches += 1.;
897            }
898            // Conditions: comparison and identity operators (anonymous tokens
899            // inside `binary_expression`), `instanceof`, ternary `?`, and
900            // control-flow arms.
901            EQEQ
902            | EQEQEQ
903            | BANGEQ
904            | BANGEQEQ
905            | LT
906            | GT
907            | LTEQ
908            | GTEQ
909            | LTEQGT
910            | LTGT
911            | Instanceof
912            | ConditionalExpression
913            | ElseClause
914            | ElseClause2
915            | ElseIfClause
916            | ElseIfClause2
917            | CaseStatement
918            | DefaultStatement
919            | MatchConditionalExpression
920            | MatchDefaultExpression
921            | CatchClause => {
922                stats.conditions += 1.;
923            }
924            _ => {}
925        }
926    }
927}
928
929// Ruby ABC rules follow the Fitzpatrick paper's spirit, adapted to
930// tree-sitter-ruby:
931// - Assignments: `assignment` (plain `=`) and `operator_assignment`
932//   (`+=`, `-=`, `||=`, `&&=`, …). Tree-sitter wraps both forms in a
933//   dedicated node, so we count one assignment per node and avoid
934//   double-counting the inner `=` / augmented token.
935// - Branches: every Ruby method invocation kind (`Call` / `Call2` /
936//   `Call3` / `Call4`) plus `super` and `yield`. `yield` is grammar-
937//   level a "block invocation" but ABC's branch bucket is "message
938//   pass / function call", so it belongs here. `attr_*` macros are
939//   `Call3` nodes and are counted as branches like any other call.
940// - Conditions: comparison and equality operator tokens emitted inside
941//   `binary` (`==`, `!=`, `===`, `<`, `>`, `<=`, `>=`, `<=>`,
942//   `=~`, `!~`), plus the control-flow arms that the Fitzpatrick rules
943//   list — the named clause nodes `Else` / `Elsif` / `When` and the
944//   `?` ternary marker, plus `Rescue` (the rescue clause) and rescue
945//   modifiers. `if` / `unless` themselves are not counted (the head
946//   condition appears as the inner comparison); the `Then` clause is
947//   an implicit grammar wrapper around every `if` / `elsif` body and
948//   is NOT counted as a separate arm.
949impl Abc for RubyCode {
950    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
951        use Ruby::*;
952
953        match node.kind_id().into() {
954            Assignment | Assignment2 | OperatorAssignment | OperatorAssignment2 => {
955                stats.assignments += 1.;
956            }
957            Call | Call2 | Call3 | Call4 | Super | Yield | Yield2 => {
958                stats.branches += 1.;
959            }
960            EQEQ | BANGEQ | EQEQEQ | LT | GT | LTEQ | GTEQ | LTEQGT | EQTILDE | BANGTILDE
961            | Else | Elsif | When | QMARK | Rescue | RescueModifier | RescueModifier2
962            | RescueModifier3 => {
963                stats.conditions += 1.;
964            }
965            _ => {}
966        }
967    }
968}
969
970// Fitzpatrick's ABC rules adapted for Python.
971//
972// - Assignments: every `Assignment` node that contains an explicit `=`
973//   token (plain assignment, walrus `:=` lives in `NamedExpression`,
974//   handled separately), plus every `AugmentedAssignment` (`+=`,
975//   `-=`, …) and every `NamedExpression` (walrus). Bare type-only
976//   annotations like `x: int` also parse as `Assignment` but have no
977//   `=` child — these are excluded so a class-level type annotation
978//   does not inflate the assignment count.
979// - Branches: every `Call` node. Python's "object construction" is
980//   syntactically a `Call` (`Foo()` parses as `call`), so the same
981//   arm covers it without a separate `New`-style case.
982// - Conditions: comparison operators (`ComparisonOperator` wraps
983//   `<`, `>`, `==`, `!=`, `is`, `is not`, `in`, `not in`, etc. as a
984//   single node), `BooleanOperator` (`and`/`or`), `ConditionalExpression`
985//   (ternary `a if c else b`), and the explicit arms of control flow:
986//   `ElifClause`, `ElseClause`, `ExceptClause`, `FinallyClause`,
987//   `CaseClause`. We do not separately count the `if` / `while`
988//   keyword: the condition expression itself is already covered by
989//   `ComparisonOperator` or `BooleanOperator`. This matches the
990//   token-level approach used for PHP / Bash.
991impl Abc for PythonCode {
992    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
993        use Python::*;
994
995        match node.kind_id().into() {
996            // Plain `=` assignment. tree-sitter-python emits an
997            // `Assignment` node for both `x = 1` (LHS, `=`, RHS) and
998            // bare annotations `x: int` (LHS, `:`, type, *no* `=`).
999            // Filtering on the presence of an `EQ` child keeps the
1000            // annotation-only case out of the count.
1001            Assignment if node.first_child(|id| id == EQ).is_some() => {
1002                stats.assignments += 1.;
1003            }
1004            // Augmented assignment (`+=`, `-=`, `*=`, …) always counts;
1005            // walrus `name := expr` is a PEP-572 `NamedExpression` and
1006            // also binds a value, so it counts as one assignment under
1007            // Fitzpatrick's rule.
1008            AugmentedAssignment | NamedExpression => {
1009                stats.assignments += 1.;
1010            }
1011            // Every call — function call, method call, type
1012            // construction — is one branch. Python parses `Foo()` as
1013            // `Call`, so object construction folds into this arm.
1014            Call => {
1015                stats.branches += 1.;
1016            }
1017            // `x < y`, `a == b`, `c is None`, `n in xs`, `m not in xs`
1018            // all parse as a single `ComparisonOperator` node — one
1019            // node, one condition, regardless of how many comparison
1020            // operators are chained.
1021            ComparisonOperator
1022            | BooleanOperator
1023            | ConditionalExpression
1024            | ElifClause
1025            | ElseClause
1026            | ExceptClause
1027            | FinallyClause
1028            | NotOperator => {
1029                // `NotOperator` is Python's unary `not`. Counting it
1030                // mirrors Java's `!x` / C#'s `!x` Abc condition rule
1031                // and closes the parity gap noted in #214 — without
1032                // it, `if not flag:` reports 0 conditions while
1033                // `if !flag` in Java reports 1. Nested combos like
1034                // `not (x > 0)` count both the unary and the
1035                // comparison once each (one logical "is-negation",
1036                // one logical "comparison"), matching Java's
1037                // `!(x > 0)`.
1038                stats.conditions += 1.;
1039            }
1040            // A non-wildcard `case` arm contributes one condition,
1041            // matching Rust's bare-`_` MatchArm filter and Java/C#'s
1042            // `default:` rule. The bare wildcard is detected by: (a)
1043            // `case_pattern` is `_`, AND (b) no `if_clause` sibling
1044            // on the `case_clause` — `case _ if g:` carries a guard
1045            // and still counts. The shared classifier lives in
1046            // `super::npa` next to `pattern_is_bare_underscore`.
1047            CaseClause if super::npa::python_case_clause_counts(node, UNDERSCORE as u16) => {
1048                stats.conditions += 1.;
1049            }
1050            _ => {}
1051        }
1052    }
1053}
1054
1055impl Abc for RustCode {
1056    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
1057        use Rust::*;
1058
1059        match node.kind_id().into() {
1060            // Plain `x = expr` (assignment_expression) and augmented
1061            // forms `+=`, `-=`, `*=`, `/=`, `%=`, `&=`, `|=`, `^=`,
1062            // `<<=`, `>>=` (compound_assignment_expr) both bind a
1063            // value; each counts as one assignment. Rust grammar
1064            // isolates both in distinct named nodes, so there is no
1065            // risk of double-counting the contained `EQ` token here.
1066            AssignmentExpression | CompoundAssignmentExpr => {
1067                stats.assignments += 1.;
1068            }
1069            // Every call expression — including method calls
1070            // (`a.b.c()` parses as `call_expression` whose callee is a
1071            // `field_expression`) — plus every `try_expression` (the
1072            // `?` operator, a short-circuit return on Result / Option)
1073            // contributes one branch. Macro invocations parse as
1074            // `macro_invocation`, NOT `call_expression`, so they are
1075            // intentionally NOT counted as branches.
1076            CallExpression | TryExpression => {
1077                stats.branches += 1.;
1078            }
1079            // Comparison operators emitted as token children of a
1080            // `binary_expression`, `if let` / `while let` conditions,
1081            // and the `else` keyword each count as one condition.
1082            // `let_condition` covers both `if let` and `while let`
1083            // (Rust's grammar uses the same node for both); inside a
1084            // `let_chain` each `let_condition` counts separately.
1085            // Java counts the `Else` token directly; Rust's grammar
1086            // exposes the same token and we follow that lead.
1087            LTEQ | GTEQ | EQEQ | BANGEQ | LetCondition | Else => {
1088                stats.conditions += 1.;
1089            }
1090            // `<` / `>` doubles as type-argument delimiter; the
1091            // `BinaryExpression` parent check disambiguates without
1092            // needing to inspect siblings.
1093            LT | GT
1094                if node
1095                    .parent()
1096                    .is_some_and(|p| matches!(p.kind_id().into(), BinaryExpression)) =>
1097            {
1098                stats.conditions += 1.;
1099            }
1100            // Every non-wildcard `match_arm` is one condition. A bare
1101            // `_ => ...` arm is the C / Java `default:` equivalent and
1102            // is excluded — mirrors the cyclomatic treatment and
1103            // Kotlin's `when` / Java's `case` rules. Patterns like
1104            // `Some(_)`, `(_, x)`, or `_ if guard` are not bare
1105            // wildcards and still count. The check scans only NAMED
1106            // children of `match_pattern` so anonymous tokens like a
1107            // leading `|` (allowed in or-patterns: `| _ => ...`) do
1108            // not throw off the detection. A guard (`_ if g`) adds a
1109            // second named child to `match_pattern` and so escapes
1110            // the bare-wildcard filter.
1111            MatchArm | MatchArm2 => {
1112                let is_bare_wildcard = node.child_by_field_name("pattern").is_some_and(|pat| {
1113                    super::npa::pattern_is_bare_underscore(&pat, UNDERSCORE as u16)
1114                });
1115                if !is_bare_wildcard {
1116                    stats.conditions += 1.;
1117                }
1118            }
1119            _ => {}
1120        }
1121    }
1122}
1123
1124impl Abc for GoCode {
1125    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
1126        // Aliased because `Go::Go` (the `go` keyword variant) collides
1127        // with the bare enum name in pattern position under
1128        // `use Go::*;` (same workaround as in cyclomatic / cognitive).
1129        use Go as G;
1130
1131        match node.kind_id().into() {
1132            // Plain `=`, augmented `+=`, `-=`, … all parse as
1133            // `assignment_statement`. `:=` is a short variable
1134            // declaration. `x++` / `x--` rebind too.
1135            G::AssignmentStatement | G::ShortVarDeclaration | G::IncStatement | G::DecStatement => {
1136                stats.assignments += 1.;
1137            }
1138            // Every call expression — including method calls
1139            // (`r.Method()` parses as `call_expression` whose callee is
1140            // a `selector_expression`) — contributes one branch.
1141            // Composite literals (`Point{X: 1}`) are NOT calls.
1142            G::CallExpression => {
1143                stats.branches += 1.;
1144            }
1145            // Comparison operators emitted as token children of a
1146            // `binary_expression`, `else`, and each non-default switch
1147            // / type-switch / select arm all contribute one condition.
1148            // `<` / `>` double as type-argument delimiters in generic
1149            // instantiations (`f[T any]`, `List[int]`); the
1150            // `BinaryExpression` parent guard filters those out
1151            // without inspecting siblings. `default_case` is
1152            // intentionally excluded — like Java / C# `default:`, it
1153            // does not introduce a new decision point.
1154            G::EQEQ
1155            | G::BANGEQ
1156            | G::LTEQ
1157            | G::GTEQ
1158            | G::Else
1159            | G::ExpressionCase
1160            | G::TypeCase
1161            | G::CommunicationCase => {
1162                stats.conditions += 1.;
1163            }
1164            G::LT | G::GT
1165                if node
1166                    .parent()
1167                    .is_some_and(|p| matches!(p.kind_id().into(), G::BinaryExpression)) =>
1168            {
1169                stats.conditions += 1.;
1170            }
1171            _ => {}
1172        }
1173    }
1174}
1175
1176impl Abc for CppCode {
1177    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
1178        use Cpp::*;
1179
1180        match node.kind_id().into() {
1181            // `assignment_expression` covers both plain `=` and every
1182            // compound form (`+=`, `-=`, `*=`, `/=`, `%=`, `&=`, `|=`,
1183            // `^=`, `<<=`, `>>=`); the grammar lifts them all into a
1184            // single named node so we count once per
1185            // `assignment_expression`. `update_expression` covers both
1186            // prefix and postfix `++` / `--`. Variable initialisers
1187            // (`int x = 0`) parse as `init_declarator` inside
1188            // `declaration` and never become `assignment_expression` —
1189            // they correctly stay out.
1190            AssignmentExpression | AssignmentExpression2 | UpdateExpression => {
1191                stats.assignments += 1.;
1192            }
1193            // Every call counts (method calls fold in as
1194            // `call_expression` with a `field_expression` callee). The
1195            // C++ grammar exposes two aliased `call_expression` ids.
1196            // `new T(...)` allocations count as a branch — they invoke
1197            // a constructor, mirroring Java's `New` and C#'s
1198            // `ObjectCreationExpression` rule.
1199            CallExpression | CallExpression2 | NewExpression => {
1200                stats.branches += 1.;
1201            }
1202            // Comparison operators emitted as token children of a
1203            // `binary_expression`. The C++20 spaceship `<=>` (`LTEQGT`)
1204            // is a comparison operator and counts once per use.
1205            // `&&` / `||` add one each per Fitzpatrick. `else` opens
1206            // an alternative branch path; `case` (non-default) adds
1207            // one per switch arm; `?` opens a ternary; `try` / `catch`
1208            // count per Fitzpatrick (and Java's rule). `Try2` is the
1209            // second token-id alias the C++ grammar emits for `try`
1210            // (it appears under structured-exception forms).
1211            LTEQ | GTEQ | EQEQ | BANGEQ | LTEQGT | AMPAMP | PIPEPIPE | Else | Case | QMARK
1212            | Try | Try2 | Catch => {
1213                stats.conditions += 1.;
1214            }
1215            // Plain `<` / `>` doubles as template-argument and
1216            // template-parameter delimiter (`std::vector<int>`,
1217            // `template <typename T>`). The `binary_expression` parent
1218            // check disambiguates without inspecting siblings — only
1219            // comparison uses of `<` / `>` count. Both kind-id aliases
1220            // (`BinaryExpression`, `BinaryExpression2`) are accepted
1221            // because the C++ grammar emits the same node under two
1222            // production-rule paths.
1223            LT | GT
1224                if node.parent().is_some_and(|p| {
1225                    matches!(p.kind_id().into(), BinaryExpression | BinaryExpression2)
1226                }) =>
1227            {
1228                stats.conditions += 1.;
1229            }
1230            _ => {}
1231        }
1232    }
1233}
1234
1235impl Abc for BashCode {
1236    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
1237        match node.kind_id().into() {
1238            // Each `variable_assignment` is one assignment regardless of
1239            // operator (`=`, `+=`, `-=`, …) — counting the parent node
1240            // avoids double-counting `Bash::EQ`, which is also produced
1241            // for the `=` inside `[ a = b ]` test expressions.
1242            Bash::VariableAssignment | Bash::VariableAssignment2 => {
1243                stats.assignments += 1.;
1244            }
1245            // Every command invocation is a branch in the ABC sense
1246            // (function-call / message-pass). `return` and `exit` builtins
1247            // are also `Bash::Command` nodes and count here too.
1248            Bash::Command => {
1249                stats.branches += 1.;
1250            }
1251            // Comparison operators inside `[[ … ]]` and `(( … ))`, plus
1252            // the prefix test operators `-z`, `-n`, `-eq`, `-lt`, … which
1253            // the grammar emits as `Bash::TestOperator`.
1254            Bash::EQEQ
1255            | Bash::BANGEQ
1256            | Bash::LT
1257            | Bash::GT
1258            | Bash::LTEQ
1259            | Bash::GTEQ
1260            | Bash::EQTILDE
1261            | Bash::TestOperator => {
1262                stats.conditions += 1.;
1263            }
1264            _ => {}
1265        }
1266    }
1267}
1268
1269// Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report.
1270// Source: https://www.softwarerenovation.com/Articles.aspx
1271// ABC Java rules: (page 8, figure 4)
1272// ABC Java example: (page 15, listing 4)
1273impl Abc for JavaCode {
1274    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
1275        use Java::*;
1276
1277        match node.kind_id().into() {
1278            STAREQ | SLASHEQ | PERCENTEQ | DASHEQ | PLUSEQ | LTLTEQ | GTGTEQ | AMPEQ | PIPEEQ
1279            | CARETEQ | GTGTGTEQ | PLUSPLUS | DASHDASH => {
1280                stats.assignments += 1.;
1281            }
1282            FieldDeclaration | LocalVariableDeclaration => {
1283                stats.declaration.push(DeclKind::Var);
1284            }
1285            Final => {
1286                if let Some(DeclKind::Var) = stats.declaration.last() {
1287                    stats.declaration.push(DeclKind::Const);
1288                }
1289            }
1290            SEMI => {
1291                if let Some(DeclKind::Const | DeclKind::Var) = stats.declaration.last() {
1292                    stats.declaration.clear();
1293                }
1294            }
1295            // Excludes constant declarations
1296            EQ if stats
1297                .declaration
1298                .last()
1299                .is_none_or(|decl| matches!(decl, DeclKind::Var)) =>
1300            {
1301                stats.assignments += 1.;
1302            }
1303            MethodInvocation | New => {
1304                stats.branches += 1.;
1305            }
1306            GTEQ | LTEQ | EQEQ | BANGEQ | Else | Case | Default | QMARK | Try | Catch => {
1307                stats.conditions += 1.;
1308            }
1309            GT | LT => {
1310                // Excludes `<` and `>` used for generic types
1311                if let Some(parent) = node.parent()
1312                    && !matches!(parent.kind_id().into(), TypeArguments)
1313                {
1314                    stats.conditions += 1.;
1315                }
1316            }
1317            // Counts unary conditions in elements separated by `&&` or `||` boolean operators
1318            AMPAMP | PIPEPIPE => {
1319                if let Some(parent) = node.parent() {
1320                    java_count_unary_conditions(&parent, &mut stats.conditions);
1321                }
1322            }
1323            // Counts unary conditions among method arguments
1324            ArgumentList => {
1325                java_count_unary_conditions(node, &mut stats.conditions);
1326            }
1327            // Counts unary conditions inside assignments
1328            VariableDeclarator | AssignmentExpression => {
1329                // The child node of index 2 contains the right operand of an assignment operation
1330                if let Some(right_operand) = node.child(2)
1331                    && matches!(
1332                        right_operand.kind_id().into(),
1333                        ParenthesizedExpression | UnaryExpression
1334                    )
1335                {
1336                    java_inspect_container(&right_operand, &mut stats.conditions);
1337                }
1338            }
1339            // Counts unary conditions inside if and while statements
1340            IfStatement | WhileStatement => {
1341                // The child node of index 1 contains the condition
1342                if let Some(condition) = node.child(1)
1343                    && matches!(condition.kind_id().into(), ParenthesizedExpression)
1344                {
1345                    java_inspect_container(&condition, &mut stats.conditions);
1346                }
1347            }
1348            // Counts unary conditions do-while statements
1349            DoStatement => {
1350                // The child node of index 3 contains the condition
1351                if let Some(condition) = node.child(3)
1352                    && matches!(condition.kind_id().into(), ParenthesizedExpression)
1353                {
1354                    java_inspect_container(&condition, &mut stats.conditions);
1355                }
1356            }
1357            // Counts unary conditions inside for statements
1358            ForStatement => {
1359                // The child node of index 3 contains the `condition` when
1360                // the initialization expression is a variable declaration
1361                // e.g. `for ( int i=0; `condition`; ... ) {}`
1362                if let Some(condition) = node.child(3) {
1363                    match condition.kind_id().into() {
1364                        SEMI => {
1365                            // The child node of index 4 contains the `condition` when
1366                            // the initialization expression is not a variable declaration
1367                            // e.g. `for ( i=0; `condition`; ... ) {}`
1368                            if let Some(cond) = node.child(4) {
1369                                match cond.kind_id().into() {
1370                                    MethodInvocation | Identifier | True | False | SEMI
1371                                    | RPAREN => {
1372                                        stats.conditions += 1.;
1373                                    }
1374                                    ParenthesizedExpression | UnaryExpression => {
1375                                        java_inspect_container(&cond, &mut stats.conditions);
1376                                    }
1377                                    _ => {}
1378                                }
1379                            }
1380                        }
1381                        MethodInvocation | Identifier | True | False => {
1382                            stats.conditions += 1.;
1383                        }
1384                        ParenthesizedExpression | UnaryExpression => {
1385                            java_inspect_container(&condition, &mut stats.conditions);
1386                        }
1387                        _ => {}
1388                    }
1389                }
1390            }
1391            // Counts unary conditions inside return statements
1392            ReturnStatement => {
1393                // The child node of index 1 contains the return value
1394                if let Some(value) = node.child(1)
1395                    && matches!(
1396                        value.kind_id().into(),
1397                        ParenthesizedExpression | UnaryExpression
1398                    )
1399                {
1400                    java_inspect_container(&value, &mut stats.conditions);
1401                }
1402            }
1403            // Counts unary conditions inside implicit return statements in lambda expressions
1404            LambdaExpression => {
1405                // The child node of index 2 contains the return value
1406                if let Some(value) = node.child(2)
1407                    && matches!(
1408                        value.kind_id().into(),
1409                        ParenthesizedExpression | UnaryExpression
1410                    )
1411                {
1412                    java_inspect_container(&value, &mut stats.conditions);
1413                }
1414            }
1415            // Counts unary conditions inside ternary expressions
1416            TernaryExpression => {
1417                // The child node of index 0 contains the condition
1418                if let Some(condition) = node.child(0) {
1419                    match condition.kind_id().into() {
1420                        MethodInvocation | Identifier | True | False => {
1421                            stats.conditions += 1.;
1422                        }
1423                        ParenthesizedExpression | UnaryExpression => {
1424                            java_inspect_container(&condition, &mut stats.conditions);
1425                        }
1426                        _ => {}
1427                    }
1428                }
1429                // The child node of index 2 contains the first expression
1430                if let Some(expression) = node.child(2)
1431                    && matches!(
1432                        expression.kind_id().into(),
1433                        ParenthesizedExpression | UnaryExpression
1434                    )
1435                {
1436                    java_inspect_container(&expression, &mut stats.conditions);
1437                }
1438                // The child node of index 4 contains the second expression
1439                if let Some(expression) = node.child(4)
1440                    && matches!(
1441                        expression.kind_id().into(),
1442                        ParenthesizedExpression | UnaryExpression
1443                    )
1444                {
1445                    java_inspect_container(&expression, &mut stats.conditions);
1446                }
1447            }
1448            _ => {}
1449        }
1450    }
1451}
1452
1453// Groovy's ABC mirrors Java's directly because the dekobon Groovy
1454// grammar shares Java's expression / statement vocabulary for the
1455// shapes ABC cares about (assignments, branches, conditions).
1456// Groovy-specific touches over Java:
1457//   - `CommandChain` (parens-less calls like `println foo`) counts as
1458//     a branch alongside `MethodInvocation` (#247).
1459//   - `DoWhileStatement` (the new grammar's name for `do { } while`)
1460//     replaces the prior `DoStatement` (which the amaanq grammar used).
1461//   - Closures (`{ x -> ... }`) have block bodies — no implicit-return
1462//     "single-expression arm" like a Java lambda, so the prior
1463//     `LambdaExpression` arm is intentionally absent.
1464//   - The dekobon grammar inlines the parens of `if (…)` / `while (…)`
1465//     / `do { … } while (…)` as `(` / `)` token children rather than
1466//     wrapping the condition in a `parenthesized_expression`, so the
1467//     condition is at a different child index than under Java's
1468//     grammar and must be inspected differently.
1469impl Abc for GroovyCode {
1470    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
1471        use Groovy::*;
1472
1473        match node.kind_id().into() {
1474            STAREQ | SLASHEQ | PERCENTEQ | DASHEQ | PLUSEQ | LTLTEQ | GTGTEQ | AMPEQ | PIPEEQ
1475            | CARETEQ | GTGTGTEQ | PLUSPLUS | DASHDASH => {
1476                stats.assignments += 1.;
1477            }
1478            FieldDeclaration | LocalVariableDeclaration => {
1479                stats.declaration.push(DeclKind::Var);
1480            }
1481            Final => {
1482                if let Some(DeclKind::Var) = stats.declaration.last() {
1483                    stats.declaration.push(DeclKind::Const);
1484                }
1485            }
1486            SEMI => {
1487                if let Some(DeclKind::Const | DeclKind::Var) = stats.declaration.last() {
1488                    stats.declaration.clear();
1489                }
1490            }
1491            EQ if stats
1492                .declaration
1493                .last()
1494                .is_none_or(|decl| matches!(decl, DeclKind::Var)) =>
1495            {
1496                stats.assignments += 1.;
1497            }
1498            MethodInvocation | CommandChain | New => {
1499                stats.branches += 1.;
1500            }
1501            GTEQ | LTEQ | EQEQ | BANGEQ | Else | Case | Default | QMARK | Try | Catch => {
1502                stats.conditions += 1.;
1503            }
1504            GT | LT => {
1505                // Excludes `<` / `>` used for generic types (e.g.
1506                // `List<String>`).
1507                if let Some(parent) = node.parent()
1508                    && !matches!(parent.kind_id().into(), TypeArguments)
1509                {
1510                    stats.conditions += 1.;
1511                }
1512            }
1513            AMPAMP | PIPEPIPE => {
1514                if let Some(parent) = node.parent() {
1515                    groovy_count_unary_conditions(&parent, &mut stats.conditions);
1516                }
1517            }
1518            ArgumentList => {
1519                groovy_count_unary_conditions(node, &mut stats.conditions);
1520            }
1521            VariableDeclarator | AssignmentExpression => {
1522                if let Some(right_operand) = node.child(2)
1523                    && matches!(
1524                        right_operand.kind_id().into(),
1525                        ParenthesizedExpression | UnaryExpression
1526                    )
1527                {
1528                    groovy_inspect_container(&right_operand, &mut stats.conditions);
1529                }
1530            }
1531            IfStatement | WhileStatement => {
1532                // dekobon `if_statement` / `while_statement` shape:
1533                // [keyword, `(`, condition, `)`, body, …]. Condition
1534                // lives at child index 2 (not 1 as under tree-sitter-
1535                // java, where the parens come wrapped in a
1536                // `parenthesized_expression`).
1537                if let Some(condition) = node.child(2) {
1538                    groovy_count_condition(&condition, &mut stats.conditions);
1539                }
1540            }
1541            DoWhileStatement => {
1542                // dekobon shape: [`do`, body, `while`, `(`, condition,
1543                // `)`]. Condition is at child index 4.
1544                if let Some(condition) = node.child(4) {
1545                    groovy_count_condition(&condition, &mut stats.conditions);
1546                }
1547            }
1548            ForStatement => {
1549                // Two shapes: a present condition lives at child(3);
1550                // an empty condition shows up as a bare `SEMI` token at
1551                // child(3) with the next slot (child(4)) holding either
1552                // the update expression or `;`/`)` for `for(;;)`-style
1553                // empty-condition loops, which still count as a single
1554                // condition slot.
1555                if let Some(condition) = node.child(3) {
1556                    if matches!(condition.kind_id().into(), SEMI) {
1557                        if let Some(cond) = node.child(4) {
1558                            if matches!(cond.kind_id().into(), SEMI | RPAREN) {
1559                                stats.conditions += 1.;
1560                            } else {
1561                                groovy_count_condition(&cond, &mut stats.conditions);
1562                            }
1563                        }
1564                    } else {
1565                        groovy_count_condition(&condition, &mut stats.conditions);
1566                    }
1567                }
1568            }
1569            ReturnStatement => {
1570                if let Some(value) = node.child(1)
1571                    && matches!(
1572                        value.kind_id().into(),
1573                        ParenthesizedExpression | UnaryExpression
1574                    )
1575                {
1576                    groovy_inspect_container(&value, &mut stats.conditions);
1577                }
1578            }
1579            TernaryExpression => {
1580                if let Some(condition) = node.child(0) {
1581                    groovy_count_condition(&condition, &mut stats.conditions);
1582                }
1583                for branch_idx in [2, 4] {
1584                    if let Some(expression) = node.child(branch_idx)
1585                        && matches!(
1586                            expression.kind_id().into(),
1587                            ParenthesizedExpression | UnaryExpression
1588                        )
1589                    {
1590                        groovy_inspect_container(&expression, &mut stats.conditions);
1591                    }
1592                }
1593            }
1594            _ => {}
1595        }
1596    }
1597}
1598
1599// Counts a single boolean-context condition expression for the dekobon
1600// Groovy grammar. The grammar inlines `(` / `)` as token children of
1601// `if_statement` / `while_statement` / `do_while_statement` rather than
1602// wrapping the condition in a `parenthesized_expression`, so the
1603// condition shows up bare. A bare identifier / boolean / call / command
1604// chain contributes one condition directly; parenthesised and unary
1605// containers delegate to `groovy_inspect_container`; binary / ternary
1606// expressions are picked up by their own arms.
1607fn groovy_count_condition(condition: &Node, conditions: &mut f64) {
1608    use Groovy::*;
1609    match condition.kind_id().into() {
1610        MethodInvocation | CommandChain | Identifier | True | False => {
1611            *conditions += 1.;
1612        }
1613        ParenthesizedExpression | UnaryExpression => {
1614            groovy_inspect_container(condition, conditions);
1615        }
1616        _ => {}
1617    }
1618}
1619
1620impl Abc for CsharpCode {
1621    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
1622        use Csharp::*;
1623
1624        match node.kind_id().into() {
1625            STAREQ | SLASHEQ | PERCENTEQ | DASHEQ | PLUSEQ | LTLTEQ | GTGTEQ | GTGTGTEQ | AMPEQ
1626            | PIPEEQ | CARETEQ | QMARKQMARKEQ | PLUSPLUS | DASHDASH => {
1627                stats.assignments += 1.;
1628            }
1629            FieldDeclaration | LocalDeclarationStatement => {
1630                stats.declaration.push(DeclKind::Var);
1631            }
1632            // C# `const` modifier marks a compile-time constant — exclude
1633            // its initializer from the assignment count (matches Java's
1634            // treatment of `final`).
1635            Const => {
1636                if let Some(DeclKind::Var) = stats.declaration.last() {
1637                    stats.declaration.push(DeclKind::Const);
1638                }
1639            }
1640            SEMI => {
1641                if let Some(DeclKind::Const | DeclKind::Var) = stats.declaration.last() {
1642                    stats.declaration.clear();
1643                }
1644            }
1645            // Count `=` as an assignment unless it's the initializer of a
1646            // `const` declaration (those are constant bindings, not mutable
1647            // assignments). `None` means we're outside any declaration —
1648            // still count.
1649            EQ if !matches!(stats.declaration.last(), Some(DeclKind::Const)) => {
1650                stats.assignments += 1.;
1651            }
1652            crate::Csharp::InvocationExpression
1653            | crate::Csharp::InvocationExpression2
1654            | crate::Csharp::InvocationExpression3
1655            | ObjectCreationExpression => {
1656                stats.branches += 1.;
1657            }
1658            GTEQ | LTEQ | EQEQ | BANGEQ | Else | Case | Default | QMARK | Try | Catch => {
1659                stats.conditions += 1.;
1660            }
1661            GT | LT => {
1662                // Excludes `<` and `>` used as type-syntax delimiters:
1663                // generic type arguments (`Dictionary<K, V>`), type
1664                // parameter declarations (`class Foo<T> { }`), and the
1665                // parameter-list delimiters of unsafe function-pointer
1666                // types (`delegate*<int, int>`).
1667                if let Some(parent) = node.parent()
1668                    && !matches!(
1669                        parent.kind_id().into(),
1670                        TypeArgumentList | TypeParameterList | FunctionPointerType
1671                    )
1672                {
1673                    stats.conditions += 1.;
1674                }
1675            }
1676            AMPAMP | PIPEPIPE => {
1677                if let Some(parent) = node.parent() {
1678                    csharp_count_unary_conditions(&parent, &mut stats.conditions);
1679                }
1680            }
1681            ArgumentList => {
1682                csharp_count_unary_conditions(node, &mut stats.conditions);
1683            }
1684            crate::Csharp::VariableDeclarator
1685            | crate::Csharp::VariableDeclarator2
1686            | AssignmentExpression => {
1687                // Child 2 is the RHS of `lhs = rhs`.
1688                inspect_csharp_child(node, 2, &mut stats.conditions);
1689            }
1690            IfStatement | WhileStatement => {
1691                // Child 1 is the parenthesised condition: `if (cond) ...`.
1692                if let Some(condition) = node.child(1)
1693                    && matches!(condition.kind_id().into(), csharp_paren_expr_kinds!())
1694                {
1695                    csharp_inspect_container(&condition, &mut stats.conditions);
1696                }
1697            }
1698            DoStatement => {
1699                // `do { ... } while (cond);` — condition sits at child 3
1700                // (children: `do`, body, `while`, `(cond)`, `;`).
1701                if let Some(condition) = node.child(3)
1702                    && matches!(condition.kind_id().into(), csharp_paren_expr_kinds!())
1703                {
1704                    csharp_inspect_container(&condition, &mut stats.conditions);
1705                }
1706            }
1707            ReturnStatement => {
1708                // Child 1 is the returned expression (child 0 is `return`).
1709                inspect_csharp_child(node, 1, &mut stats.conditions);
1710            }
1711            LambdaExpression => {
1712                // Child 2 is the lambda body for `params => body`.
1713                inspect_csharp_child(node, 2, &mut stats.conditions);
1714            }
1715            ConditionalExpression => {
1716                // `cond ? a : b` — children are [cond, ?, a, :, b].
1717                if let Some(condition) = node.child(0) {
1718                    match condition.kind_id().into() {
1719                        crate::Csharp::InvocationExpression
1720                        | crate::Csharp::InvocationExpression2
1721                        | crate::Csharp::InvocationExpression3
1722                        | Identifier
1723                        | True
1724                        | False => {
1725                            stats.conditions += 1.;
1726                        }
1727                        crate::Csharp::ParenthesizedExpression
1728                        | crate::Csharp::ParenthesizedExpression2
1729                        | crate::Csharp::ParenthesizedExpression3
1730                        | crate::Csharp::PrefixUnaryExpression
1731                        | crate::Csharp::PrefixUnaryExpression2 => {
1732                            csharp_inspect_container(&condition, &mut stats.conditions);
1733                        }
1734                        _ => {}
1735                    }
1736                }
1737                inspect_csharp_child(node, 2, &mut stats.conditions);
1738                inspect_csharp_child(node, 4, &mut stats.conditions);
1739            }
1740            // Counts unary / single-token conditions inside `for`
1741            // statements. The C# grammar exposes the loop condition via
1742            // the named `condition` field on `for_statement`, so we look
1743            // it up by name rather than positional index (Java's arm
1744            // relies on positional indices because its grammar does not
1745            // name the field). Comparison-operator conditions like
1746            // `i < n` are still counted by the standard `GT | LT | ...`
1747            // arms — this arm only fires when the condition is a bare
1748            // identifier, invocation, boolean literal, parenthesised
1749            // expression, or `!`-prefixed unary expression.
1750            ForStatement => {
1751                if let Some(condition) = node.child_by_field_name("condition") {
1752                    let kind = condition.kind_id().into();
1753                    if matches!(kind, csharp_invocation_expr_kinds!())
1754                        || matches!(kind, Identifier | BooleanLiteral)
1755                    {
1756                        stats.conditions += 1.;
1757                    } else if matches!(kind, csharp_paren_expr_kinds!())
1758                        || matches!(kind, csharp_prefix_unary_expr_kinds!())
1759                    {
1760                        csharp_inspect_container(&condition, &mut stats.conditions);
1761                    }
1762                }
1763            }
1764            _ => {}
1765        }
1766    }
1767}
1768
1769impl Abc for ElixirCode {
1770    // Elixir's pattern-match `=` is a `BinaryOperator` whose middle
1771    // child is an `EQ` token. The same wrapper node also hosts `+=`-
1772    // style augmented assignments, but Elixir is purely functional —
1773    // augmented assignment does not exist in the grammar; `EQ` is the
1774    // only assignment-shaped operator. `|>` (`PIPEGT`) is a
1775    // BinaryOperator too but its operator token differs, so the EQ
1776    // child check is what filters assignments from pipelines and from
1777    // comparison operators that share the wrapper.
1778    //
1779    // Branches cover `|>` (the pipe operator dispatches one call per
1780    // step) and every `Call` node (function / method / macro
1781    // invocation). `RemoteCallWithParentheses` and `LocalCallWith*`
1782    // variants are subordinate nodes to `Call`, so the single `Call`
1783    // match captures every dispatch site.
1784    //
1785    // Conditions cover `when` (guard token `Elixir::When`), the six
1786    // comparison operator tokens (`==`, `===`, `!=`, `!==`, `<`, `>`,
1787    // `<=`, `>=`), and the keyword-shaped `Call`s that introduce a
1788    // decision point (`if`, `unless`, `case`, `cond`, `with`).
1789    // `for` / `while` are looping forms — not condition-shaped per
1790    // the issue body's literal list — so we omit them.
1791    //
1792    // Limitations:
1793    // - `case` is counted once on the container, not once per arm
1794    //   (`stab_clause`). The issue body says "conditions = case,
1795    //   cond, if, with, guard when" — i.e. one condition per
1796    //   construct, not per arm. Matches the Rust impl's "MatchExpression
1797    //   once" rule.
1798    // - Higher-order calls like `Enum.reduce` are `RemoteCallWithParentheses`
1799    //   nodes; they are still `Call` nodes and so contribute one branch
1800    //   each, matching the issue's "branches = `|>`, function calls"
1801    //   instruction.
1802    fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats) {
1803        use Elixir as E;
1804
1805        match node.kind_id().into() {
1806            // A `BinaryOperator` whose operator token is `EQ` is a
1807            // pattern-match assignment. The grammar shape is
1808            // `(left, operator, right)`, so the operator token is
1809            // always at child index 1 — looking it up directly is
1810            // O(1) vs. an `any()` scan of all children. This arm
1811            // fires on every Elixir binary op (comparisons, pipes,
1812            // boolean ops, arithmetic) so the constant-time check
1813            // matters.
1814            E::BinaryOperator | E::BinaryOperator2 | E::BinaryOperator3
1815                if node
1816                    .child(1)
1817                    .is_some_and(|c| c.kind_id() == E::EQ as u16) =>
1818            {
1819                stats.assignments += 1.;
1820            }
1821            // `|>` pipeline operator: every step in `foo |> bar |> baz`
1822            // is one branch (the pipe dispatches one call per step).
1823            E::PIPEGT => {
1824                stats.branches += 1.;
1825            }
1826            // Every Call (function, method, macro, sigil-call) is one
1827            // branch — `RemoteCallWith*`, `LocalCallWith*`,
1828            // `AnonymousCall`, and `DoubleCall` are all subordinate
1829            // node kinds underneath the top-level `Call` wrapper, so
1830            // matching `Call` alone captures every dispatch site.
1831            //
1832            // Method-defining macros (`def`/`defp`/`defmacro`/`defmacrop`)
1833            // and module/struct/protocol declarations (`defmodule`/
1834            // `defstruct`/`defprotocol`/`defimpl`) are *not* runtime
1835            // dispatch and must not inflate `branches` — they parse as
1836            // `Call` nodes because Elixir's grammar uses the same
1837            // shape for all keyword-introduced forms. Aliasing/import
1838            // directives (`alias`, `import`, `require`, `use`) are
1839            // similarly declarative and excluded.
1840            //
1841            // Cognitive's `elixir_call_keyword` lookup is reused to
1842            // identify the target keyword. Note: Cognitive only acts
1843            // on a subset of these keywords (the four method-definers
1844            // for nesting reset, plus the 7 control-flow keywords for
1845            // +nesting); Abc's broader filter additionally drops the
1846            // module/struct/protocol declarators and aliasing
1847            // directives that Cognitive ignores entirely. Filter sets
1848            // are intentionally different — both impls use the same
1849            // helper to look up the keyword, but apply different
1850            // policies on top.
1851            E::Call => {
1852                let keyword = super::cognitive::elixir_call_keyword(node, code);
1853                let is_definition_or_directive = matches!(
1854                    keyword,
1855                    Some(
1856                        "def" | "defp" | "defmacro" | "defmacrop"
1857                        | "defmodule" | "defstruct" | "defprotocol" | "defimpl"
1858                        | "alias" | "import" | "require" | "use"
1859                    )
1860                );
1861                if !is_definition_or_directive {
1862                    stats.branches += 1.;
1863                }
1864                // Keyword-shaped control-flow Calls also contribute
1865                // one condition.
1866                if matches!(keyword, Some("if" | "unless" | "case" | "cond" | "with")) {
1867                    stats.conditions += 1.;
1868                }
1869            }
1870            // Comparison operator tokens. `Elixir::LT` / `Elixir::GT`
1871            // are unambiguously comparison ops here — unlike Go's
1872            // generic-instantiation `<` / `>`, Elixir has no type
1873            // parameter brackets that share the token.
1874            E::EQEQ | E::EQEQEQ | E::BANGEQ | E::BANGEQEQ
1875            | E::LT | E::GT | E::LTEQ | E::GTEQ
1876            // Guard `when` token: introduces the guard clause of a
1877            // function head or `case` arm.
1878            | E::When => {
1879                stats.conditions += 1.;
1880            }
1881            _ => {}
1882        }
1883    }
1884}
1885
1886// Fitzpatrick's ABC rules adapted for Perl.
1887//
1888// - Assignments: every assignment operator token — plain `=` plus the
1889//   compound forms `+=`, `-=`, `*=`, `/=`, `%=`, `**=`, `.=`, `x=`,
1890//   `&=`, `|=`, `^=`, `<<=`, `>>=`, `&&=`, `||=`, `//=`, and the
1891//   bitstring forms `&.=`, `|.=`, `^.=`. Each token fires exactly
1892//   once per textual occurrence inside a `binary_expression`.
1893// - Branches: every call expression dispatch — `call_expression_with_*`
1894//   (bareword / spaced args / args-with-brackets / sub / variable /
1895//   recursive) plus `method_invocation`. The grammar nests an inner
1896//   `call_expression_with_bareword` (just the function name)
1897//   underneath the wrapper kinds carrying argument lists, so we only
1898//   count `CallExpressionWithBareword` when it stands on its own;
1899//   when its parent is another call form, the outer wrapper has
1900//   already contributed the branch.
1901// - Conditions: numeric and string comparison operators (`==`, `!=`,
1902//   `<`, `>`, `<=`, `>=`, `<=>`, `eq`, `ne`, `lt`, `gt`, `le`, `ge`,
1903//   `cmp`, `=~`, `!~`), short-circuit / logical operators (`&&`,
1904//   `||`, `//`, `and`, `or`, `xor`), the ternary operator
1905//   (`TernaryExpression`), and each `elsif` / `else` clause of an
1906//   `if` / `unless` statement. Bare predicates that have no
1907//   comparison (e.g. `if ($x)`) are not separately counted; we let
1908//   the comparison tokens carry the metric, mirroring the Bash /
1909//   Python token-level approach.
1910impl Abc for PerlCode {
1911    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
1912        use Perl as P;
1913
1914        match node.kind_id().into() {
1915            // Plain `=` and every compound assignment operator. The
1916            // grammar tokenises each operator separately, so one
1917            // textual `+=` produces exactly one token and there is no
1918            // double-counting via a wrapper.
1919            P::EQ
1920            | P::PLUSEQ
1921            | P::DASHEQ
1922            | P::STAREQ
1923            | P::SLASHEQ
1924            | P::PERCENTEQ
1925            | P::STARSTAREQ
1926            | P::DOTEQ
1927            | P::XEQ
1928            | P::AMPEQ
1929            | P::PIPEEQ
1930            | P::CARETEQ
1931            | P::LTLTEQ
1932            | P::GTGTEQ
1933            | P::AMPAMPEQ
1934            | P::PIPEPIPEEQ
1935            | P::SLASHSLASHEQ
1936            | P::AMPDOTEQ
1937            | P::PIPEDOTEQ
1938            | P::CARETDOTEQ => {
1939                stats.assignments += 1.;
1940            }
1941            // Argument-bearing call wrappers always count.
1942            P::CallExpressionWithSpacedArgs
1943            | P::CallExpressionWithSub
1944            | P::CallExpressionWithArgsWithBrackets
1945            | P::CallExpressionWithVariable
1946            | P::CallExpressionRecursive
1947            | P::MethodInvocation => {
1948                stats.branches += 1.;
1949            }
1950            // Bareword-only call (`shift`, `time`, …) — count only
1951            // when this node is the outermost dispatch site. When the
1952            // bareword sits inside one of the wrappers above, the
1953            // outer node has already been counted and this child
1954            // would double the branch tally.
1955            P::CallExpressionWithBareword
1956                if !node.parent().is_some_and(|p| {
1957                    matches!(
1958                        p.kind_id().into(),
1959                        P::CallExpressionWithSpacedArgs
1960                            | P::CallExpressionWithSub
1961                            | P::CallExpressionWithArgsWithBrackets
1962                            | P::CallExpressionWithVariable
1963                            | P::CallExpressionRecursive
1964                    )
1965                }) =>
1966            {
1967                stats.branches += 1.;
1968            }
1969            // Numeric, string, and pattern-match comparison operators
1970            // plus the spaceship / `cmp` three-way comparisons.
1971            P::EQEQ | P::BANGEQ | P::LT | P::GT | P::LTEQ | P::GTEQ | P::LTEQGT
1972            | P::Eq | P::Ne | P::Lt | P::Gt | P::Le | P::Ge | P::Cmp
1973            | P::EQTILDE | P::BANGTILDE
1974            // Short-circuit / logical operators (high- and low-
1975            // precedence forms).
1976            | P::AMPAMP | P::PIPEPIPE | P::SLASHSLASH
1977            | P::And | P::Or | P::Xor
1978            // Ternary `a ? b : c` and each `elsif` / `else` clause of
1979            // an `if` / `unless` chain.
1980            | P::TernaryExpression
1981            | P::ElsifClause
1982            | P::ElseClause => {
1983                stats.conditions += 1.;
1984            }
1985            _ => {}
1986        }
1987    }
1988}
1989
1990// Fitzpatrick's ABC rules adapted for Lua.
1991//
1992// - Assignments: every `assignment_statement` node. Lua has no
1993//   compound assignment operators (`+=` and friends do not exist in
1994//   the grammar), so the wrapper kind is the sole assignment node
1995//   and there is no per-operator alternative to track. `local x = 1`
1996//   wraps an `assignment_statement` under a `variable_declaration`,
1997//   so initialisers count the same as later mutations.
1998// - Branches: every `function_call`. The Lua grammar collapses
1999//   `obj.method(args)`, `obj:method(args)`, and `f(args)` into the
2000//   same `function_call` node, so one arm covers all dispatch forms.
2001// - Conditions: comparison operators (`==`, `~=`, `<`, `>`, `<=`,
2002//   `>=`), short-circuit operators (`and`, `or`), each elseif / else
2003//   arm of an `if`. Lua has no ternary operator (`cond and a or b`
2004//   is the idiom, captured by the `and` / `or` tokens above).
2005impl Abc for LuaCode {
2006    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
2007        match node.kind_id().into() {
2008            Lua::AssignmentStatement | Lua::AssignmentStatement2 => {
2009                stats.assignments += 1.;
2010            }
2011            Lua::FunctionCall => {
2012                stats.branches += 1.;
2013            }
2014            Lua::EQEQ
2015            | Lua::TILDEEQ
2016            | Lua::LT
2017            | Lua::GT
2018            | Lua::LTEQ
2019            | Lua::GTEQ
2020            | Lua::And
2021            | Lua::Or
2022            | Lua::ElseifStatement
2023            | Lua::ElseStatement => {
2024                stats.conditions += 1.;
2025            }
2026            _ => {}
2027        }
2028    }
2029}
2030
2031// Names of Tcl commands that mutate a variable. Each invocation of
2032// one of these commands counts as an assignment, not a branch — the
2033// command is acting as an assignment operator, not as a generic
2034// dispatch. The list is intentionally narrow: only commands that
2035// every Tcl programmer recognises as primary mutators. Less-common
2036// mutators (`dict set`, `array set`, `lset`, `regsub … name`) are
2037// left as branches; treating them as assignments would require
2038// inspecting the command's second word, and the additional
2039// fidelity is not worth the complexity for the ABC magnitude.
2040const TCL_ASSIGNMENT_COMMANDS: &[&[u8]] = &[b"incr", b"append", b"lappend"];
2041
2042// Fitzpatrick's ABC rules adapted for Tcl.
2043//
2044// - Assignments: every `set` production (`set name value`) plus
2045//   every `command` whose first word is one of the recognised
2046//   mutator commands in `TCL_ASSIGNMENT_COMMANDS`. Tcl has no
2047//   assignment operators — variable mutation is always a command
2048//   invocation, so we filter on the command name. The `set` form
2049//   has its own grammar production (`Tcl::Set`) and counts directly
2050//   without any source-text inspection.
2051// - Branches: every other `command` node. Like Bash, `return` and
2052//   `error` builtins parse as plain `command` nodes and count here
2053//   too — Tcl treats every dispatch the same regardless of whether
2054//   the command is a procedure call, a control-flow primitive, or a
2055//   builtin. The grammar productions for `if`, `while`, `foreach`,
2056//   etc. live separately from `command` and do not double-count.
2057// - Conditions: numeric (`==`, `!=`, `<`, `>`, `<=`, `>=`) and
2058//   string (`eq`, `ne`, `in`, `ni`) comparison tokens, short-circuit
2059//   operators (`&&`, `||`), the ternary expression production, and
2060//   each `elseif` / `else` clause of an `if`.
2061impl Abc for TclCode {
2062    fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats) {
2063        match node.kind_id().into() {
2064            // The `set` production wraps `set name value` as a
2065            // first-class node distinct from generic commands.
2066            Tcl::Set => {
2067                stats.assignments += 1.;
2068            }
2069            // Generic command: branch by default, assignment when
2070            // the first word names a known mutator. The first word
2071            // can be either a `simple_word` or a wrapped form; both
2072            // surface their literal text via `utf8_text`.
2073            Tcl::Command => {
2074                if tcl_command_is_assignment(node, code) {
2075                    stats.assignments += 1.;
2076                } else {
2077                    stats.branches += 1.;
2078                }
2079            }
2080            Tcl::EQEQ
2081            | Tcl::BANGEQ
2082            | Tcl::LT
2083            | Tcl::GT
2084            | Tcl::LTEQ
2085            | Tcl::GTEQ
2086            | Tcl::Eq
2087            | Tcl::Ne
2088            | Tcl::In
2089            | Tcl::Ni
2090            | Tcl::AMPAMP
2091            | Tcl::PIPEPIPE
2092            | Tcl::TernaryExpr
2093            | Tcl::Elseif
2094            | Tcl::Else => {
2095                stats.conditions += 1.;
2096            }
2097            _ => {}
2098        }
2099    }
2100}
2101
2102// Returns true when the `command` node's first word is one of the
2103// recognised Tcl assignment commands. The first word is the leftmost
2104// non-comment child; we slice the source bytes directly using the
2105// child node's byte range, which is robust to `simple_word` wrappers
2106// and avoids depending on a particular grammar shape.
2107fn tcl_command_is_assignment(node: &Node, code: &[u8]) -> bool {
2108    let Some(first) = node.child(0) else {
2109        return false;
2110    };
2111    let start = first.start_byte();
2112    let end = first.end_byte();
2113    if end > code.len() || start >= end {
2114        return false;
2115    }
2116    let word = &code[start..end];
2117    TCL_ASSIGNMENT_COMMANDS.contains(&word)
2118}
2119
2120// Shared helper: if `node.child(idx)` is a parenthesised or `!`-prefixed
2121// expression, descend into it to count any unary boolean condition.
2122// Used by every C# Abc match arm whose condition sits at a known child
2123// index (assignments, returns, lambdas, ternaries).
2124fn inspect_csharp_child(node: &Node, idx: usize, conditions: &mut f64) {
2125    if let Some(child) = node.child(idx)
2126        && matches!(
2127            child.kind_id().into(),
2128            crate::Csharp::ParenthesizedExpression
2129                | crate::Csharp::ParenthesizedExpression2
2130                | crate::Csharp::ParenthesizedExpression3
2131                | crate::Csharp::PrefixUnaryExpression
2132                | crate::Csharp::PrefixUnaryExpression2
2133        )
2134    {
2135        csharp_inspect_container(&child, conditions);
2136    }
2137}
2138
2139#[cfg(test)]
2140#[allow(
2141    clippy::float_cmp,
2142    clippy::cast_precision_loss,
2143    clippy::cast_possible_truncation,
2144    clippy::cast_sign_loss,
2145    clippy::similar_names,
2146    clippy::doc_markdown,
2147    clippy::needless_raw_string_hashes,
2148    clippy::too_many_lines
2149)]
2150mod tests {
2151    use crate::tools::check_metrics;
2152
2153    use super::*;
2154
2155    /// Regression for #227: a `Stats::default()` that never sees an
2156    /// observation must not leak the `f64::MAX` sentinel for
2157    /// `assignments_min`, `branches_min`, or `conditions_min`. All
2158    /// three getters collapse the sentinel to `0.0` so JSON never
2159    /// emits `1.7976931e308`.
2160    #[test]
2161    fn abc_empty_file_min_is_zero() {
2162        let stats = Stats::default();
2163        assert_eq!(stats.assignments_min(), 0.0);
2164        assert_eq!(stats.branches_min(), 0.0);
2165        assert_eq!(stats.conditions_min(), 0.0);
2166    }
2167
2168    // Regression test for the `EQ` arm guard in `JavaCode::compute`:
2169    // the rewrite from `.map().unwrap_or_else()` to
2170    // `is_none_or(|decl| matches!(decl, DeclKind::Var))` must preserve
2171    // the three-way truth table — None → ++, Some(Var) → ++,
2172    // Some(Const) → no-op.
2173    #[test]
2174    fn java_eq_arm_increments_when_declaration_stack_is_empty() {
2175        // No surrounding `int x = ...` / `Final` token → declaration
2176        // stack is empty when the `EQ` token is visited, so the None
2177        // branch must increment `assignments`.
2178        check_metrics::<JavaParser>(
2179            "class A { void m() { int x = 0; x = 1; x = 2; x = 3; } }",
2180            "foo.java",
2181            |metric| {
2182                // `int x = 0;` adds 1 (Some(Var) branch),
2183                // each subsequent `x = N;` adds 1 (None branch).
2184                assert_eq!(metric.abc.assignments_sum(), 4.0);
2185            },
2186        );
2187    }
2188
2189    #[test]
2190    fn java_eq_arm_skips_when_declaration_stack_top_is_const() {
2191        // `final` pushes `DeclKind::Const` on top of the active `Var`
2192        // entry, so the Some(non-Var) branch must skip the increment.
2193        check_metrics::<JavaParser>(
2194            "class A {
2195                final int X = 1;
2196                final int Y = 2;
2197                void m() { final int Z = 3; }
2198            }",
2199            "foo.java",
2200            |metric| {
2201                // All three `=` tokens land under a `Const` top, so
2202                // assignments should be 0 across all spaces.
2203                assert_eq!(metric.abc.assignments_sum(), 0.0);
2204            },
2205        );
2206    }
2207
2208    // Constant declarations are not counted as assignments
2209    #[test]
2210    fn java_constant_declarations() {
2211        check_metrics::<JavaParser>(
2212            "class A {
2213                private final int X1 = 0, Y1 = 0;
2214                public final float PI = 3.14f;
2215                final static String HELLO = \"Hello,\";
2216                protected String world = \" world!\";   // +1a
2217                public float e = 2.718f;                // +1a
2218                private int x2 = 1, y2 = 2;             // +2a
2219
2220                void m() {
2221                    final int Z1 = 0, Z2 = 0, Z3 = 0;
2222                    final float T = 0.0f;
2223                    int z1 = 1, z2 = 2, z3 = 3;         // +3a
2224                    float t = 60.0f;                    // +1a
2225                }
2226            }",
2227            "foo.java",
2228            |metric| {
2229                // magnitude: sqrt(64 + 0 + 0) = sqrt(64)
2230                // space count: 3 (1 unit, 1 class and 1 method)
2231                insta::assert_json_snapshot!(
2232                    metric.abc,
2233                    @r###"
2234                    {
2235                      "assignments": 8.0,
2236                      "branches": 0.0,
2237                      "conditions": 0.0,
2238                      "magnitude": 8.0,
2239                      "assignments_average": 2.6666666666666665,
2240                      "branches_average": 0.0,
2241                      "conditions_average": 0.0,
2242                      "assignments_min": 0.0,
2243                      "assignments_max": 4.0,
2244                      "branches_min": 0.0,
2245                      "branches_max": 0.0,
2246                      "conditions_min": 0.0,
2247                      "conditions_max": 0.0
2248                    }"###
2249                );
2250            },
2251        );
2252    }
2253
2254    // "In computer science, conditionals (that is, conditional statements, conditional expressions
2255    // and conditional constructs,) are programming language commands for handling decisions."
2256    // Source: https://en.wikipedia.org/wiki/Conditional_(computer_programming)
2257    // According to this definition, boolean expressions that are evaluated to make a decision are considered as conditions
2258    // Variables, method invocations and true or false values used inside
2259    // variable declarations and assignment expressions are not counted as conditions
2260    #[test]
2261    fn java_declarations_with_conditions() {
2262        check_metrics::<JavaParser>(
2263            "
2264            boolean a = (1 > 2);            // +1a +1c
2265            boolean b = 3 > 4;              // +1a +1c
2266            boolean c = (1 > 2) && 3 > 4;   // +1a +2c
2267            boolean d = b && (x > 5) || c;  // +1a +3c
2268            boolean e = !d;                 // +1a +1c
2269            boolean f = ((!false));         // +1a +1c
2270            boolean g = !(!(true));         // +1a +1c
2271            boolean h = true;               // +1a
2272            boolean i = (false);            // +1a
2273            boolean j = (((((true)))));     // +1a
2274            boolean k = (((((m())))));      // +1a +1b
2275            boolean l = (((((!m())))));     // +1a +1b +1c
2276            boolean m = (!(!((m()))));      // +1a +1b +1c
2277            List<String> n = null;          // +1a (< and > used for generic types are not counted as conditions)
2278            ",
2279            "foo.java",
2280          |metric| {
2281                // magnitude: sqrt(196 + 9 + 144) = sqrt(349)
2282                // space count: 1 (1 unit)
2283                insta::assert_json_snapshot!(
2284                    metric.abc,
2285                    @r###"
2286                    {
2287                      "assignments": 14.0,
2288                      "branches": 3.0,
2289                      "conditions": 12.0,
2290                      "magnitude": 18.681541692269406,
2291                      "assignments_average": 14.0,
2292                      "branches_average": 3.0,
2293                      "conditions_average": 12.0,
2294                      "assignments_min": 14.0,
2295                      "assignments_max": 14.0,
2296                      "branches_min": 3.0,
2297                      "branches_max": 3.0,
2298                      "conditions_min": 12.0,
2299                      "conditions_max": 12.0
2300                    }"###
2301                );
2302            },
2303        );
2304    }
2305
2306    // Conditions can be found in assignment expressions
2307    #[test]
2308    fn java_assignments_with_conditions() {
2309        check_metrics::<JavaParser>(
2310            "
2311            a = 2 < 1;                  // +1a +1c
2312            b = (4 >= 3) && 2 <= 1;     // +1a +2c
2313            c = a || (x != 10) && b;    // +1a +3c
2314            d = !false;                 // +1a +1c
2315            e = (!false);               // +1a +1c
2316            f = !(false);               // +1a +1c
2317            g = (!(((true))));          // +1a +1c
2318            h = ((true));               // +1a
2319            i = !m();                   // +1a +1b +1c
2320            j = !((m()));               // +1a +1b +1c
2321            k = (!(m()));               // +1a +1b +1c
2322            l = ((!(m())));             // +1a +1b +1c
2323            m = !B.<Integer>m(2);       // +1a +1b +1c
2324            n = !((B.<Integer>m(4)));   // +1a +1b +1c
2325            ",
2326            "foo.java",
2327            |metric| {
2328                // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
2329                // space count: 1 (1 unit)
2330                insta::assert_json_snapshot!(
2331                    metric.abc,
2332                    @r###"
2333                    {
2334                      "assignments": 14.0,
2335                      "branches": 6.0,
2336                      "conditions": 16.0,
2337                      "magnitude": 22.090722034374522,
2338                      "assignments_average": 14.0,
2339                      "branches_average": 6.0,
2340                      "conditions_average": 16.0,
2341                      "assignments_min": 14.0,
2342                      "assignments_max": 14.0,
2343                      "branches_min": 6.0,
2344                      "branches_max": 6.0,
2345                      "conditions_min": 16.0,
2346                      "conditions_max": 16.0
2347                    }"###
2348                );
2349            },
2350        );
2351    }
2352
2353    // Conditions can be found in method arguments
2354    #[test]
2355    fn java_methods_arguments_with_conditions() {
2356        check_metrics::<JavaParser>(
2357            "
2358            m1(a);                                  // +1b
2359            m2(a, b);                               // +1b
2360            m3(true, (false), (((true))));          // +1b
2361            m3(m1(false), m1(true), m1(false));     // +4b
2362            m1(!a);                                 // +1b +1c
2363            m2((((a))), (!b));                      // +1b +1c
2364            m3(!(a), b, !!!c);                      // +1b +2c
2365            m3(a, !b, m2(!a, !m2(!b, !m1(!c))));    // +4b +6c
2366            ",
2367            "foo.java",
2368            |metric| {
2369                // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
2370                // space count: 1 (1 unit)
2371                insta::assert_json_snapshot!(
2372                    metric.abc,
2373                    @r###"
2374                    {
2375                      "assignments": 0.0,
2376                      "branches": 14.0,
2377                      "conditions": 10.0,
2378                      "magnitude": 17.204650534085253,
2379                      "assignments_average": 0.0,
2380                      "branches_average": 14.0,
2381                      "conditions_average": 10.0,
2382                      "assignments_min": 0.0,
2383                      "assignments_max": 0.0,
2384                      "branches_min": 14.0,
2385                      "branches_max": 14.0,
2386                      "conditions_min": 10.0,
2387                      "conditions_max": 10.0
2388                    }"###
2389                );
2390            },
2391        );
2392    }
2393
2394    // "A unary conditional expression is an implicit condition that uses no relational operators."
2395    // Source: Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report.
2396    // https://www.softwarerenovation.com/Articles.aspx (page 5)
2397    #[test]
2398    fn java_if_single_conditions() {
2399        check_metrics::<JavaParser>(
2400            "
2401            if ( a < 0 ) {}             // +1c
2402            if ( ((a != 0)) ) {}        // +1c
2403            if ( !(a > 0) ) {}          // +1c
2404            if ( !(((a == 0))) ) {}     // +1c
2405            if ( b.m1() ) {}            // +1b +1c
2406            if ( !b.m1() ) {}           // +1b +1c
2407            if ( !!b.m2() ) {}          // +1b +1c
2408            if ( (!(b.m1())) ) {}       // +1b +1c
2409            if ( (!(!b.m1())) ) {}      // +1b +1c
2410            if ( ((b.m2())) ) {}        // +1b +1c
2411            if ( ((b.m().m1())) ) {}    // +2b +1c
2412            if ( c ) {}                 // +1c
2413            if ( !c ) {}                // +1c
2414            if ( !!!!!!!!!!c ) {}       // +1c
2415            if ( (((c))) ) {}           // +1c
2416            if ( (((!c))) ) {}          // +1c
2417            if ( ((!(c))) ) {}          // +1c
2418            if ( true ) {}              // +1c
2419            if ( !true ) {}             // +1c
2420            if ( ((false)) ) {}         // +1c
2421            if ( !(!(false)) ) {}       // +1c
2422            if ( !!!false ) {}          // +1c
2423            ",
2424            "foo.java",
2425            |metric| {
2426                // magnitude: sqrt(0 + 64 + 484) = sqrt(548)
2427                // space count: 1 (1 unit)
2428                insta::assert_json_snapshot!(
2429                    metric.abc,
2430                    @r###"
2431                    {
2432                      "assignments": 0.0,
2433                      "branches": 8.0,
2434                      "conditions": 22.0,
2435                      "magnitude": 23.40939982143925,
2436                      "assignments_average": 0.0,
2437                      "branches_average": 8.0,
2438                      "conditions_average": 22.0,
2439                      "assignments_min": 0.0,
2440                      "assignments_max": 0.0,
2441                      "branches_min": 8.0,
2442                      "branches_max": 8.0,
2443                      "conditions_min": 22.0,
2444                      "conditions_max": 22.0
2445                    }"###
2446                );
2447            },
2448        );
2449    }
2450
2451    #[test]
2452    fn java_if_multiple_conditions() {
2453        check_metrics::<JavaParser>(
2454            "
2455            if ( a || b || c || d ) {}              // +4c
2456            if ( a || b && c && d ) {}              // +4c
2457            if ( x < y && a == b ) {}               // +2c
2458            if ( ((z < (x + y))) ) {}               // +1c
2459            if ( a || ((((b))) && c) ) {}           // +3c
2460            if ( a && ((((a == b))) && c) ) {}      // +3c
2461            if ( a || ((((a == b))) || ((c))) ) {}  // +3c
2462            if ( x < y && B.m() ) {}                // +1b +2c
2463            if ( x < y && !(((B.m()))) ) {}         // +1b +2c
2464            if ( !(x < y) && !B.m() ) {}            // +1b +2c
2465            if ( !!!(!!!(a)) && B.m() ||            // +1b +2c
2466                 !B.m() && (((x > 4))) ) {}         // +1b +2c
2467            ",
2468            "foo.java",
2469            |metric| {
2470                // magnitude: sqrt(0 + 25 + 900) = sqrt(925)
2471                // space count: 1 (1 unit)
2472                insta::assert_json_snapshot!(
2473                    metric.abc,
2474                    @r###"
2475                    {
2476                      "assignments": 0.0,
2477                      "branches": 5.0,
2478                      "conditions": 30.0,
2479                      "magnitude": 30.4138126514911,
2480                      "assignments_average": 0.0,
2481                      "branches_average": 5.0,
2482                      "conditions_average": 30.0,
2483                      "assignments_min": 0.0,
2484                      "assignments_max": 0.0,
2485                      "branches_min": 5.0,
2486                      "branches_max": 5.0,
2487                      "conditions_min": 30.0,
2488                      "conditions_max": 30.0
2489                    }"###
2490                );
2491            },
2492        );
2493    }
2494
2495    #[test]
2496    fn java_while_and_do_while_conditions() {
2497        check_metrics::<JavaParser>(
2498            "
2499            while ( (!(!(!(a)))) ) {}                   // +1c
2500            while ( b || 1 > 2 ) {}                     // +2c
2501            while ( x.m() && (((c))) ) {}               // +1b +2c
2502            do {} while ( !!!(((!!!a))) );              // +1c
2503            do {} while ( a || (b && c) );              // +3c
2504            do {} while ( !x.m() && 1 > 2 || !true );   // +1b +3c
2505            ",
2506            "foo.java",
2507            |metric| {
2508                // magnitude: sqrt(0 + 4 + 144) = sqrt(148)
2509                // space count: 1 (1 unit)
2510                insta::assert_json_snapshot!(
2511                    metric.abc,
2512                    @r###"
2513                    {
2514                      "assignments": 0.0,
2515                      "branches": 2.0,
2516                      "conditions": 12.0,
2517                      "magnitude": 12.165525060596439,
2518                      "assignments_average": 0.0,
2519                      "branches_average": 2.0,
2520                      "conditions_average": 12.0,
2521                      "assignments_min": 0.0,
2522                      "assignments_max": 0.0,
2523                      "branches_min": 2.0,
2524                      "branches_max": 2.0,
2525                      "conditions_min": 12.0,
2526                      "conditions_max": 12.0
2527                    }"###
2528                );
2529            },
2530        );
2531    }
2532
2533    // GMetrics, a Groovy source code analyzer, provides the following definition of unary conditional expression:
2534    // "These are cases where a single variable/field/value is treated as a boolean value.
2535    // Examples include `if (x)` and `return !ready`."
2536    // According to this definition, unary conditional expressions are counted also in function return values.
2537    // Source: https://dx42.github.io/gmetrics/metrics/AbcMetric.html
2538    // Examples: https://github.com/dx42/gmetrics/blob/master/src/test/groovy/org/gmetrics/metric/abc/AbcMetric_MethodTest.groovy
2539    #[test]
2540    fn java_return_with_conditions() {
2541        check_metrics::<JavaParser>(
2542            "class A {
2543                boolean m1() {
2544                    return !(z >= 0);       // +1c
2545                }
2546                boolean m2() {
2547                    return (((!x)));        // +1c
2548                }
2549                boolean m3() {
2550                    return x && y;          // +2c
2551                }
2552                boolean m4() {
2553                    return y || (z < 0);    // +2c
2554                }
2555                boolean m5() {
2556                    return x || y ?         // +3c (two unary conditions and one ?)
2557                        true : false;
2558                }
2559            }",
2560            "foo.java",
2561            |metric| {
2562                // magnitude: sqrt(0 + 0 + 81) = sqrt(81)
2563                // space count: 7 (1 unit, 1 class and 5 methods)
2564                insta::assert_json_snapshot!(
2565                    metric.abc,
2566                    @r###"
2567                    {
2568                      "assignments": 0.0,
2569                      "branches": 0.0,
2570                      "conditions": 9.0,
2571                      "magnitude": 9.0,
2572                      "assignments_average": 0.0,
2573                      "branches_average": 0.0,
2574                      "conditions_average": 1.2857142857142858,
2575                      "assignments_min": 0.0,
2576                      "assignments_max": 0.0,
2577                      "branches_min": 0.0,
2578                      "branches_max": 0.0,
2579                      "conditions_min": 0.0,
2580                      "conditions_max": 3.0
2581                    }"###
2582                );
2583            },
2584        );
2585    }
2586
2587    // Variables, method invocations, and true or false values
2588    // inside return statements are not counted as conditions
2589    #[test]
2590    fn java_return_without_conditions() {
2591        check_metrics::<JavaParser>(
2592            "class A {
2593                boolean m1() {
2594                    return x;
2595                }
2596                boolean m2() {
2597                    return (x);
2598                }
2599                boolean m3() {
2600                    return y.m();   // +1b
2601                }
2602                boolean m4() {
2603                    return false;
2604                }
2605                void m5() {
2606                    return;
2607                }
2608            }",
2609            "foo.java",
2610            |metric| {
2611                // magnitude: sqrt(0 + 1 + 0) = sqrt(1)
2612                // space count: 7 (1 unit, 1 class and 5 methods)
2613                insta::assert_json_snapshot!(
2614                    metric.abc,
2615                    @r###"
2616                    {
2617                      "assignments": 0.0,
2618                      "branches": 1.0,
2619                      "conditions": 0.0,
2620                      "magnitude": 1.0,
2621                      "assignments_average": 0.0,
2622                      "branches_average": 0.14285714285714285,
2623                      "conditions_average": 0.0,
2624                      "assignments_min": 0.0,
2625                      "assignments_max": 0.0,
2626                      "branches_min": 0.0,
2627                      "branches_max": 1.0,
2628                      "conditions_min": 0.0,
2629                      "conditions_max": 0.0
2630                    }"###
2631                );
2632            },
2633        );
2634    }
2635
2636    // Variables, method invocations, and true or false values
2637    // in lambda expression return values are not counted as conditions
2638    #[test]
2639    fn java_lambda_expressions_return_with_conditions() {
2640        check_metrics::<JavaParser>(
2641            "
2642            Predicate<Boolean> p1 = a -> a;                         // +1a
2643            Predicate<Boolean> p2 = b -> true;                      // +1a
2644            Predicate<Boolean> p3 = c -> m();                       // +1a
2645            Predicate<Integer> p4 = d -> d > 10;                    // +1a +1c
2646            Predicate<Boolean> p5 = (e) -> !e;                      // +1a +1c
2647            Predicate<Boolean> p6 = (f) -> !((!f));                 // +1a +1c
2648            Predicate<Boolean> p7 = (g) -> !g && true;              // +1a +2c
2649            BiPredicate<Boolean, Boolean> bp1 = (h, i) -> !h && !i; // +1a +2c
2650            BiPredicate<Boolean, Boolean> bp2 = (j, k) -> {
2651                return j || k;                                      // +1a +2c
2652            };
2653            ",
2654            "foo.java",
2655            |metric| {
2656                // magnitude: sqrt(81 + 1 + 81) = sqrt(163)
2657                // space count: 1 (1 unit)
2658                insta::assert_json_snapshot!(
2659                    metric.abc,
2660                    @r###"
2661                    {
2662                      "assignments": 9.0,
2663                      "branches": 1.0,
2664                      "conditions": 9.0,
2665                      "magnitude": 12.767145334803704,
2666                      "assignments_average": 9.0,
2667                      "branches_average": 1.0,
2668                      "conditions_average": 9.0,
2669                      "assignments_min": 9.0,
2670                      "assignments_max": 9.0,
2671                      "branches_min": 1.0,
2672                      "branches_max": 1.0,
2673                      "conditions_min": 9.0,
2674                      "conditions_max": 9.0
2675                    }"###
2676                );
2677            },
2678        );
2679    }
2680
2681    #[test]
2682    fn java_for_with_variable_declaration() {
2683        check_metrics::<JavaParser>(
2684            "
2685            for ( int i1 = 0; !(!(!(!a))); i1++ ) {}                // +2a +1c
2686            for ( int i2 = 0; !B.m(); i2++ ) {}                     // +2a +1b +1c
2687            for ( int i3 = 0; a || false; i3++ ) {}                 // +2a +2c
2688            for ( int i4 = 0; a && B.m() ? true : false; i4++ ) {}  // +2a +1b +3c
2689            for ( int i5 = 0; true; i5++ ) {}                       // +2a +1c
2690            ",
2691            "foo.java",
2692            |metric| {
2693                // magnitude: sqrt(100 + 4 + 64) = sqrt(168)
2694                // space count: 1 (1 unit)
2695                insta::assert_json_snapshot!(
2696                    metric.abc,
2697                    @r###"
2698                    {
2699                      "assignments": 10.0,
2700                      "branches": 2.0,
2701                      "conditions": 8.0,
2702                      "magnitude": 12.96148139681572,
2703                      "assignments_average": 10.0,
2704                      "branches_average": 2.0,
2705                      "conditions_average": 8.0,
2706                      "assignments_min": 10.0,
2707                      "assignments_max": 10.0,
2708                      "branches_min": 2.0,
2709                      "branches_max": 2.0,
2710                      "conditions_min": 8.0,
2711                      "conditions_max": 8.0
2712                    }"###
2713                );
2714            },
2715        );
2716    }
2717
2718    #[test]
2719    fn java_for_without_variable_declaration() {
2720        check_metrics::<JavaParser>(
2721            "class A{
2722                void m1() {
2723                    for (i = 0; x < y; i++) {}          // +2a +1c
2724                    for (i = 0; ((x < y)); i++) {}      // +2a +1c
2725                    for (i = 0; !(!(x < y)); i++) {}    // +2a +1c
2726                    for (i = 0; true; i++) {}           // +2a +1c
2727                }
2728                void m2() {
2729                    for ( ; true; ) {}  // +1c
2730                }
2731                void m3() {
2732                    for ( ; ; ) {}      // +1c (one implicit unary condition set to true)
2733                }
2734            }",
2735            "foo.java",
2736            |metric| {
2737                // magnitude: sqrt(64 + 0 + 36) = sqrt(100)
2738                // space count: 5 (1 unit, 1 class and 3 methods)
2739                insta::assert_json_snapshot!(
2740                    metric.abc,
2741                    @r###"
2742                    {
2743                      "assignments": 8.0,
2744                      "branches": 0.0,
2745                      "conditions": 6.0,
2746                      "magnitude": 10.0,
2747                      "assignments_average": 1.6,
2748                      "branches_average": 0.0,
2749                      "conditions_average": 1.2,
2750                      "assignments_min": 0.0,
2751                      "assignments_max": 8.0,
2752                      "branches_min": 0.0,
2753                      "branches_max": 0.0,
2754                      "conditions_min": 0.0,
2755                      "conditions_max": 4.0
2756                    }"###
2757                );
2758            },
2759        );
2760    }
2761
2762    // Variables, method invocations, and true or false values
2763    // in ternary expression return values are not counted as conditions
2764    #[test]
2765    fn java_ternary_conditions() {
2766        check_metrics::<JavaParser>(
2767            "
2768            a = true;                                   // +1a
2769            b = a ? true : false;                       // +1a +2c
2770            c = ((((a)))) ? !false : !b;                // +1a +4c
2771            d = !this.m() ? !!a : (false);              // +1a +1b +3c
2772            e = !(a) && b ? ((c)) : !d;                 // +1a +4c
2773            if ( this.m() ? a : !this.m() ) {}          // +2b +3c
2774            if ( x > 0 ? !(false) : this.m() ) {}       // +1b +3c
2775            if ( x > 0 && x != 3 ? !(a) : (!(b)) ) {}   // +5c
2776            ",
2777            "foo.java",
2778            |metric| {
2779                // magnitude: sqrt(25 + 16 + 576) = sqrt(617)
2780                //  space count: 1 (1 unit)
2781                insta::assert_json_snapshot!(
2782                    metric.abc,
2783                    @r###"
2784                    {
2785                      "assignments": 5.0,
2786                      "branches": 4.0,
2787                      "conditions": 24.0,
2788                      "magnitude": 24.839484696748443,
2789                      "assignments_average": 5.0,
2790                      "branches_average": 4.0,
2791                      "conditions_average": 24.0,
2792                      "assignments_min": 5.0,
2793                      "assignments_max": 5.0,
2794                      "branches_min": 4.0,
2795                      "branches_max": 4.0,
2796                      "conditions_min": 24.0,
2797                      "conditions_max": 24.0
2798                    }"###
2799                );
2800            },
2801        );
2802    }
2803
2804    #[test]
2805    fn bash_assignments_only() {
2806        check_metrics::<BashParser>(
2807            "f() {
2808                 a=1
2809                 b=2
2810                 c+=3
2811             }",
2812            "foo.sh",
2813            |metric| {
2814                insta::assert_json_snapshot!(
2815                    metric.abc,
2816                    @r###"
2817                    {
2818                      "assignments": 3.0,
2819                      "branches": 0.0,
2820                      "conditions": 0.0,
2821                      "magnitude": 3.0,
2822                      "assignments_average": 1.5,
2823                      "branches_average": 0.0,
2824                      "conditions_average": 0.0,
2825                      "assignments_min": 0.0,
2826                      "assignments_max": 3.0,
2827                      "branches_min": 0.0,
2828                      "branches_max": 0.0,
2829                      "conditions_min": 0.0,
2830                      "conditions_max": 0.0
2831                    }"###
2832                );
2833            },
2834        );
2835    }
2836
2837    #[test]
2838    fn bash_commands_only() {
2839        check_metrics::<BashParser>(
2840            "f() {
2841                 echo a
2842                 ls
2843             }",
2844            "foo.sh",
2845            |metric| {
2846                insta::assert_json_snapshot!(
2847                    metric.abc,
2848                    @r###"
2849                    {
2850                      "assignments": 0.0,
2851                      "branches": 2.0,
2852                      "conditions": 0.0,
2853                      "magnitude": 2.0,
2854                      "assignments_average": 0.0,
2855                      "branches_average": 1.0,
2856                      "conditions_average": 0.0,
2857                      "assignments_min": 0.0,
2858                      "assignments_max": 0.0,
2859                      "branches_min": 0.0,
2860                      "branches_max": 2.0,
2861                      "conditions_min": 0.0,
2862                      "conditions_max": 0.0
2863                    }"###
2864                );
2865            },
2866        );
2867    }
2868
2869    #[test]
2870    fn bash_conditions_mix() {
2871        // Exercises every condition path: `==` and `!=` inside `[[ ]]`,
2872        // arithmetic `<` inside `(( ))`, and the prefix `-z` test operator
2873        // inside `[ ]`. Each `if` body's `echo` contributes a branch.
2874        check_metrics::<BashParser>(
2875            "f() {
2876                 if [[ \"$a\" == \"$b\" ]]; then
2877                     echo eq
2878                 fi
2879                 if [[ \"$x\" != \"$y\" ]]; then
2880                     echo ne
2881                 fi
2882                 if (( $a < $b )); then
2883                     echo lt
2884                 fi
2885                 if [ -z \"$x\" ]; then
2886                     echo empty
2887                 fi
2888             }",
2889            "foo.sh",
2890            |metric| {
2891                insta::assert_json_snapshot!(
2892                    metric.abc,
2893                    @r###"
2894                    {
2895                      "assignments": 0.0,
2896                      "branches": 4.0,
2897                      "conditions": 4.0,
2898                      "magnitude": 5.656854249492381,
2899                      "assignments_average": 0.0,
2900                      "branches_average": 2.0,
2901                      "conditions_average": 2.0,
2902                      "assignments_min": 0.0,
2903                      "assignments_max": 0.0,
2904                      "branches_min": 0.0,
2905                      "branches_max": 4.0,
2906                      "conditions_min": 0.0,
2907                      "conditions_max": 4.0
2908                    }"###
2909                );
2910            },
2911        );
2912    }
2913
2914    #[test]
2915    fn bash_magnitude() {
2916        // Combined assignments + branches + conditions: magnitude must
2917        // equal sqrt(2² + 1² + 1²) = sqrt(6).
2918        check_metrics::<BashParser>(
2919            "f() {
2920                 a=1
2921                 b=2
2922                 if [[ \"$a\" == \"$b\" ]]; then
2923                     echo eq
2924                 fi
2925             }",
2926            "foo.sh",
2927            |metric| {
2928                insta::assert_json_snapshot!(
2929                    metric.abc,
2930                    @r###"
2931                    {
2932                      "assignments": 2.0,
2933                      "branches": 1.0,
2934                      "conditions": 1.0,
2935                      "magnitude": 2.449489742783178,
2936                      "assignments_average": 1.0,
2937                      "branches_average": 0.5,
2938                      "conditions_average": 0.5,
2939                      "assignments_min": 0.0,
2940                      "assignments_max": 2.0,
2941                      "branches_min": 0.0,
2942                      "branches_max": 1.0,
2943                      "conditions_min": 0.0,
2944                      "conditions_max": 1.0
2945                    }"###
2946                );
2947            },
2948        );
2949    }
2950
2951    #[test]
2952    fn java_malformed_parenthesized_no_panic() {
2953        check_metrics::<JavaParser>("class A { void m() { if (( }) }", "foo.java", |metric| {
2954            // tree-sitter emits ERROR nodes for this malformed source, so no
2955            // IfStatement, branch, or condition is recognised — all counts are 0.
2956            // Primary goal: the unwrap-free path does not panic.
2957            assert_eq!(metric.abc.assignments(), 0.0);
2958            assert_eq!(metric.abc.branches(), 0.0);
2959            assert_eq!(metric.abc.conditions(), 0.0);
2960            assert_eq!(metric.abc.magnitude(), 0.0);
2961        });
2962    }
2963
2964    #[test]
2965    fn groovy_no_abc() {
2966        // Comment-only file has no executable code → all-zero ABC.
2967        check_metrics::<GroovyParser>(
2968            "// just a comment, no executable code",
2969            "foo.groovy",
2970            |metric| {
2971                assert_eq!(metric.abc.assignments_sum(), 0.0);
2972                assert_eq!(metric.abc.branches_sum(), 0.0);
2973                assert_eq!(metric.abc.conditions_sum(), 0.0);
2974            },
2975        );
2976    }
2977
2978    #[test]
2979    fn groovy_single_assignment() {
2980        // `int x = 1` is a local-variable declaration whose `=` counts
2981        // as one assignment (matches Java's semantics).
2982        check_metrics::<GroovyParser>("int x = 1", "foo.groovy", |metric| {
2983            assert_eq!(metric.abc.assignments_sum(), 1.0);
2984            assert_eq!(metric.abc.branches_sum(), 0.0);
2985            assert_eq!(metric.abc.conditions_sum(), 0.0);
2986        });
2987    }
2988
2989    #[test]
2990    fn groovy_assignments() {
2991        check_metrics::<GroovyParser>(
2992            "void f() {
2993                int a = 1
2994                int b = 2
2995                a = 3
2996                b = 4
2997                a += 1
2998                b -= 1
2999            }",
3000            "foo.groovy",
3001            |metric| {
3002                // Six `=` tokens total. The two `Final`-less local
3003                // var-decls (`int a = 1`, `int b = 2`) and the two
3004                // bare assignments (`a = 3`, `b = 4`) each contribute
3005                // one assignment via the `EQ` arm; the `+=` / `-=`
3006                // each contribute one via the compound-assign arm.
3007                assert_eq!(metric.abc.assignments_sum(), 6.0);
3008            },
3009        );
3010    }
3011
3012    #[test]
3013    fn groovy_branches() {
3014        check_metrics::<GroovyParser>(
3015            "void f() {
3016                doStuff()
3017                helper.invoke()
3018                new Worker()
3019            }",
3020            "foo.groovy",
3021            |metric| {
3022                // 2 method invocations + 1 object creation = 3 branches
3023                assert_eq!(metric.abc.branches_sum(), 3.0);
3024            },
3025        );
3026    }
3027
3028    #[test]
3029    fn groovy_conditions_in_if() {
3030        check_metrics::<GroovyParser>(
3031            "void f(int a) {
3032                if (a == 0) { println(a) }
3033                if (a >= 1) { println(a) }
3034                if (a != 2) { println(a) }
3035            }",
3036            "foo.groovy",
3037            |metric| {
3038                // Three relational ops = 3 conditions
3039                assert_eq!(metric.abc.conditions_sum(), 3.0);
3040            },
3041        );
3042    }
3043
3044    #[test]
3045    fn groovy_branches_with_juxt_call() {
3046        // Groovy's parens-less call form `println foo` must be counted
3047        // as a branch (`JuxtFunctionCall`).
3048        check_metrics::<GroovyParser>(
3049            "void f() {
3050                println 'hi'
3051                println 'bye'
3052            }",
3053            "foo.groovy",
3054            |metric| {
3055                // 2 juxt calls = 2 branches.
3056                assert_eq!(metric.abc.branches_sum(), 2.0);
3057            },
3058        );
3059    }
3060
3061    #[test]
3062    fn groovy_try_catch_conditions() {
3063        // Each `try` and `catch` keyword token contributes +1 to
3064        // conditions (mirrors Java).
3065        check_metrics::<GroovyParser>(
3066            "void f() {
3067                try {
3068                    risky()
3069                } catch (Exception e) {
3070                    handle(e)
3071                }
3072            }",
3073            "foo.groovy",
3074            |metric| {
3075                // try + catch = 2 conditions
3076                assert_eq!(metric.abc.conditions_sum(), 2.0);
3077            },
3078        );
3079    }
3080
3081    #[test]
3082    fn groovy_ternary_conditions() {
3083        check_metrics::<GroovyParser>(
3084            "void f(int x) {
3085                def y = x > 0 ? 1 : 2
3086            }",
3087            "foo.groovy",
3088            |metric| {
3089                // QMARK alone is +1 condition, plus the `>` condition = 2.
3090                assert_eq!(metric.abc.conditions_sum(), 2.0);
3091            },
3092        );
3093    }
3094
3095    #[test]
3096    fn groovy_constant_excluded_from_assignments() {
3097        // `final` declarations are not counted as assignments
3098        // (mirrors Java's `Final` handling).
3099        check_metrics::<GroovyParser>(
3100            "class A {
3101                final int CONST = 42
3102                int field = 0
3103            }",
3104            "foo.groovy",
3105            |metric| {
3106                // The `=` on `final int CONST = 42` is a constant
3107                // initialiser (skipped). Only `field = 0` counts.
3108                assert_eq!(metric.abc.assignments_sum(), 1.0);
3109            },
3110        );
3111    }
3112
3113    #[test]
3114    fn groovy_malformed_parenthesized_no_panic() {
3115        // Regression: malformed Groovy input must not panic the ABC
3116        // walker; the `spaces.rs` Unit fallback (lesson 9) covers
3117        // structural recovery. amaanq's grammar treats `def x = (((`
3118        // as a `local_variable_declaration` whose initialiser is the
3119        // first opening paren — the `=` still fires the assignment
3120        // arm.
3121        check_metrics::<GroovyParser>("def x = (((", "foo.groovy", |metric| {
3122            assert_eq!(metric.abc.assignments_sum(), 1.0);
3123        });
3124    }
3125
3126    #[test]
3127    fn groovy_if_multiple_conditions() {
3128        // Mirrors `java_if_multiple_conditions`: `&&` / `||` chains
3129        // and parenthesised unary forms each contribute one
3130        // condition per primitive comparison; the inspect-container
3131        // pass picks up the unary `!a` / `!b` arguments inside the
3132        // `BinaryExpression` and counts them too.
3133        check_metrics::<GroovyParser>(
3134            "void f(boolean a, boolean b, boolean c) {
3135                if (a || b || c) { println(a) }
3136                if (a && b && c) { println(a) }
3137                if (!a && !b) { println(a) }
3138            }",
3139            "foo.groovy",
3140            |metric| {
3141                // Conditions counted via the AMPAMP/PIPEPIPE arms
3142                // (one count per identifier in the chain — three
3143                // for `||`, three for `&&`, two for the unary chain)
3144                // = 8.
3145                assert_eq!(metric.abc.conditions_sum(), 8.0);
3146                // Three `println a` juxt calls — each is a branch.
3147                assert_eq!(metric.abc.branches_sum(), 3.0);
3148            },
3149        );
3150    }
3151
3152    #[test]
3153    fn groovy_while_and_do_while_conditions() {
3154        // Covers the WhileStatement and DoStatement arms in
3155        // `impl Abc for GroovyCode`. Each `while` / `do-while` has
3156        // its condition inspected through `groovy_inspect_container`.
3157        check_metrics::<GroovyParser>(
3158            "void f(boolean a, boolean b) {
3159                while (a) {
3160                    a = false
3161                }
3162                do {
3163                    b = !b
3164                } while (b)
3165            }",
3166            "foo.groovy",
3167            |metric| {
3168                // `while(a)` + `while(b)` each contribute one condition;
3169                // the unary `!b` on the do body's right-hand side adds
3170                // one more via the assignment-arm inspection = 3.
3171                assert_eq!(metric.abc.conditions_sum(), 3.0);
3172                // Two assignments to existing variables (`a = false`,
3173                // `b = !b`).
3174                assert_eq!(metric.abc.assignments_sum(), 2.0);
3175            },
3176        );
3177    }
3178
3179    #[test]
3180    fn groovy_methods_arguments_with_conditions() {
3181        // Mirror of `java_methods_arguments_with_conditions`: a
3182        // unary `!x` inside an argument list must count both the
3183        // method invocation as a branch AND the unary as a
3184        // condition. The `ArgumentList | ArgumentList2` arm in
3185        // `impl Abc for GroovyCode` is what exercises this.
3186        check_metrics::<GroovyParser>(
3187            "void f(boolean a, boolean b, boolean c) {
3188                m1(a)
3189                m1(!a)
3190                m2(!a, !b)
3191            }",
3192            "foo.groovy",
3193            |metric| {
3194                // 3 method invocations (m1, m1, m2) — each fires the
3195                // branches arm.
3196                assert_eq!(metric.abc.branches_sum(), 3.0);
3197                // Three `!` unaries — `m1(!a)` and the two args of
3198                // `m2(!a, !b)` — each contribute one condition via
3199                // the ArgumentList inspection.
3200                assert_eq!(metric.abc.conditions_sum(), 3.0);
3201            },
3202        );
3203    }
3204
3205    #[test]
3206    fn groovy_return_with_conditions() {
3207        // Mirror of `java_return_with_conditions`: a parenthesised
3208        // or unary expression inside `return` flows through the
3209        // `ReturnStatement` arm to `groovy_inspect_container`.
3210        check_metrics::<GroovyParser>(
3211            "boolean f(boolean a) {
3212                return (a)
3213            }
3214            boolean g(boolean a) {
3215                return !a
3216            }",
3217            "foo.groovy",
3218            |metric| {
3219                // Only one of the two return forms surfaces a
3220                // condition: `return !a` hits the UnaryExpression
3221                // path and adds one; `return (a)` reaches
3222                // `groovy_inspect_container` but the inner
3223                // identifier `a` is not in a boolean-context-firing
3224                // parent, so no condition is added.
3225                assert_eq!(metric.abc.conditions_sum(), 1.0);
3226            },
3227        );
3228    }
3229
3230    #[test]
3231    fn groovy_for_with_variable_declaration() {
3232        // Classical `for (int i = 0; cond; i++)` form. The init
3233        // slot's `int i = 0` is suppressed from assignments by the
3234        // `LocalVariableDeclaration` push/pop dance; the `i++` in
3235        // the update slot contributes one assignment via the
3236        // `PLUSPLUS` arm. The condition `i < 10` flows through the
3237        // `ForStatement` arm.
3238        check_metrics::<GroovyParser>(
3239            "void f() {
3240                for (int i = 0; i < 10; i++) {
3241                    println(i)
3242                }
3243            }",
3244            "foo.groovy",
3245            |metric| {
3246                // `int i = 0` fires the EQ arm + `i++` fires the
3247                // PLUSPLUS arm = 2 assignments.
3248                assert_eq!(metric.abc.assignments_sum(), 2.0);
3249                // `i < 10` is one condition (the LT arm).
3250                assert_eq!(metric.abc.conditions_sum(), 1.0);
3251            },
3252        );
3253    }
3254
3255    #[test]
3256    fn groovy_eq_arm_increments_when_no_declaration() {
3257        // Bare reassignment of an already-declared variable: the
3258        // `EQ` arm fires when the declaration stack is empty
3259        // (`stats.declaration.last().is_none()`), so the `=` counts
3260        // as one assignment. Mirrors `java_eq_arm_increments_when_
3261        // declaration_stack_is_empty`.
3262        check_metrics::<GroovyParser>(
3263            "void f(int x) {
3264                x = 42
3265            }",
3266            "foo.groovy",
3267            |metric| {
3268                assert_eq!(metric.abc.assignments_sum(), 1.0);
3269                assert_eq!(metric.abc.branches_sum(), 0.0);
3270                assert_eq!(metric.abc.conditions_sum(), 0.0);
3271            },
3272        );
3273    }
3274
3275    #[test]
3276    fn csharp_constant_declarations() {
3277        check_metrics::<CsharpParser>(
3278            "class A {
3279                private const int X1 = 0, Y1 = 0;
3280                public const float PI = 3.14f;
3281                const string HELLO = \"Hello,\";
3282                protected string world = \" world!\";
3283                public float e = 2.718f;
3284                private int x2 = 1, y2 = 2;
3285                void M() {
3286                    const int Z1 = 0, Z2 = 0, Z3 = 0;
3287                    const float T = 0.0f;
3288                    int z1 = 1, z2 = 2, z3 = 3;
3289                }
3290            }",
3291            "foo.cs",
3292            |metric| insta::assert_json_snapshot!(metric.abc),
3293        );
3294    }
3295
3296    #[test]
3297    fn csharp_declarations_with_conditions() {
3298        check_metrics::<CsharpParser>(
3299            "class A {
3300                bool a = (1 == 2);
3301                bool b = (1 < 2);
3302                bool c = !true;
3303                bool d = !false;
3304            }",
3305            "foo.cs",
3306            |metric| insta::assert_json_snapshot!(metric.abc),
3307        );
3308    }
3309
3310    #[test]
3311    fn csharp_assignments_with_conditions() {
3312        check_metrics::<CsharpParser>(
3313            "class A {
3314                void M() {
3315                    int a = 0;
3316                    a += 1;
3317                    a -= 2;
3318                    a *= 3;
3319                    a /= 4;
3320                    a %= 5;
3321                    a++;
3322                    a--;
3323                }
3324            }",
3325            "foo.cs",
3326            |metric| insta::assert_json_snapshot!(metric.abc),
3327        );
3328    }
3329
3330    #[test]
3331    fn csharp_methods_arguments_with_conditions() {
3332        check_metrics::<CsharpParser>(
3333            "class A {
3334                void M(int x, int y) {
3335                    F(x == y, x < y, !x.Equals(y));
3336                }
3337                void F(bool a, bool b, bool c) {}
3338            }",
3339            "foo.cs",
3340            |metric| insta::assert_json_snapshot!(metric.abc),
3341        );
3342    }
3343
3344    #[test]
3345    fn csharp_if_single_conditions() {
3346        check_metrics::<CsharpParser>(
3347            "class A {
3348                void M(int x) {
3349                    if (x > 0) { System.Console.WriteLine(\"a\"); }
3350                    if (x < 0) { System.Console.WriteLine(\"b\"); }
3351                    if (x == 0) { System.Console.WriteLine(\"c\"); }
3352                }
3353            }",
3354            "foo.cs",
3355            |metric| insta::assert_json_snapshot!(metric.abc),
3356        );
3357    }
3358
3359    #[test]
3360    fn csharp_if_multiple_conditions() {
3361        check_metrics::<CsharpParser>(
3362            "class A {
3363                void M(int x, int y) {
3364                    if (x > 0 && y > 0) { System.Console.WriteLine(\"a\"); }
3365                    if (x < 0 || y < 0) { System.Console.WriteLine(\"b\"); }
3366                }
3367            }",
3368            "foo.cs",
3369            |metric| insta::assert_json_snapshot!(metric.abc),
3370        );
3371    }
3372
3373    #[test]
3374    fn csharp_while_and_do_while_conditions() {
3375        check_metrics::<CsharpParser>(
3376            "class A {
3377                void M(int x) {
3378                    while (x > 0) { x--; }
3379                    do { x++; } while (x < 10);
3380                }
3381            }",
3382            "foo.cs",
3383            |metric| insta::assert_json_snapshot!(metric.abc),
3384        );
3385    }
3386
3387    #[test]
3388    fn csharp_return_with_conditions() {
3389        check_metrics::<CsharpParser>(
3390            "class A {
3391                bool M(int x) {
3392                    return (x > 0);
3393                }
3394                bool N(int x) {
3395                    return !(x < 0);
3396                }
3397            }",
3398            "foo.cs",
3399            |metric| insta::assert_json_snapshot!(metric.abc),
3400        );
3401    }
3402
3403    #[test]
3404    fn csharp_return_without_conditions() {
3405        check_metrics::<CsharpParser>(
3406            "class A {
3407                int M() { return 42; }
3408                string N() { return \"hi\"; }
3409            }",
3410            "foo.cs",
3411            |metric| insta::assert_json_snapshot!(metric.abc),
3412        );
3413    }
3414
3415    #[test]
3416    fn csharp_lambda_expressions_return_with_conditions() {
3417        check_metrics::<CsharpParser>(
3418            "class A {
3419                public void M() {
3420                    System.Func<int, bool> f = x => (x > 0);
3421                    System.Func<int, bool> g = x => !(x < 0);
3422                }
3423            }",
3424            "foo.cs",
3425            |metric| insta::assert_json_snapshot!(metric.abc),
3426        );
3427    }
3428
3429    #[test]
3430    fn csharp_for_with_variable_declaration() {
3431        check_metrics::<CsharpParser>(
3432            "class A {
3433                void M() {
3434                    for (int i = 0; i < 10; i++) {
3435                        System.Console.WriteLine(i);
3436                    }
3437                }
3438            }",
3439            "foo.cs",
3440            |metric| insta::assert_json_snapshot!(metric.abc),
3441        );
3442    }
3443
3444    #[test]
3445    fn csharp_for_without_variable_declaration() {
3446        check_metrics::<CsharpParser>(
3447            "class A {
3448                void M() {
3449                    int i;
3450                    for (i = 0; i < 10; i++) {
3451                        System.Console.WriteLine(i);
3452                    }
3453                }
3454            }",
3455            "foo.cs",
3456            |metric| insta::assert_json_snapshot!(metric.abc),
3457        );
3458    }
3459
3460    #[test]
3461    fn csharp_for_identifier_condition() {
3462        check_metrics::<CsharpParser>(
3463            "class A {
3464                void M(bool ready) {
3465                    for (; ready ;) { }
3466                }
3467            }",
3468            "foo.cs",
3469            |metric| {
3470                // expected: assignments=0 (no `=` / `++` / `--`),
3471                // branches=0 (no invocation / object creation),
3472                // conditions=1 (bare-identifier for-loop condition).
3473                // Averages divide by 3 spaces (top-level + class + method).
3474                insta::assert_json_snapshot!(
3475                    metric.abc,
3476                    @r###"
3477                {
3478                  "assignments": 0.0,
3479                  "branches": 0.0,
3480                  "conditions": 1.0,
3481                  "magnitude": 1.0,
3482                  "assignments_average": 0.0,
3483                  "branches_average": 0.0,
3484                  "conditions_average": 0.3333333333333333,
3485                  "assignments_min": 0.0,
3486                  "assignments_max": 0.0,
3487                  "branches_min": 0.0,
3488                  "branches_max": 0.0,
3489                  "conditions_min": 0.0,
3490                  "conditions_max": 1.0
3491                }
3492                "###
3493                );
3494            },
3495        );
3496    }
3497
3498    #[test]
3499    fn csharp_for_invocation_condition() {
3500        check_metrics::<CsharpParser>(
3501            "class A {
3502                bool Ok() { return true; }
3503                void M() {
3504                    for (; Ok() ;) { }
3505                }
3506            }",
3507            "foo.cs",
3508            |metric| {
3509                // expected: assignments=0, branches=1 (the `Ok()` call),
3510                // conditions=1 (invocation as for-loop condition).
3511                // Averages divide by 4 spaces (top-level + class + two
3512                // methods).
3513                insta::assert_json_snapshot!(
3514                    metric.abc,
3515                    @r###"
3516                {
3517                  "assignments": 0.0,
3518                  "branches": 1.0,
3519                  "conditions": 1.0,
3520                  "magnitude": 1.4142135623730951,
3521                  "assignments_average": 0.0,
3522                  "branches_average": 0.25,
3523                  "conditions_average": 0.25,
3524                  "assignments_min": 0.0,
3525                  "assignments_max": 0.0,
3526                  "branches_min": 0.0,
3527                  "branches_max": 1.0,
3528                  "conditions_min": 0.0,
3529                  "conditions_max": 1.0
3530                }
3531                "###
3532                );
3533            },
3534        );
3535    }
3536
3537    // Regression coverage for #279: the C# grammar wraps a literal
3538    // `true` / `false` for-loop condition in a `boolean_literal` node.
3539    // The `BooleanLiteral` arm in the `ForStatement` dispatch must
3540    // attribute one condition; without it, `for (; true ;)` would
3541    // contribute 0 (the bug fixed by this commit also affected this
3542    // shape).
3543    #[test]
3544    fn csharp_for_boolean_literal_condition() {
3545        check_metrics::<CsharpParser>(
3546            "class A {
3547                void M() {
3548                    for (; true ;) { }
3549                }
3550            }",
3551            "foo.cs",
3552            |metric| {
3553                // expected: assignments=0, branches=0,
3554                // conditions=1 (the `true` literal as condition).
3555                assert_eq!(metric.abc.conditions_sum(), 1.0);
3556                assert_eq!(metric.abc.assignments_sum(), 0.0);
3557                assert_eq!(metric.abc.branches_sum(), 0.0);
3558            },
3559        );
3560    }
3561
3562    // Regression coverage for #279: an empty for-loop condition such as
3563    // `for (; ;) {}` must contribute 0 to conditions — there is no
3564    // condition node to count.
3565    #[test]
3566    fn csharp_for_empty_condition() {
3567        check_metrics::<CsharpParser>(
3568            "class A {
3569                void M() {
3570                    for (; ;) { }
3571                }
3572            }",
3573            "foo.cs",
3574            |metric| {
3575                // expected: assignments=0, branches=0, conditions=0
3576                // (no condition expression in `for (; ;)`).
3577                insta::assert_json_snapshot!(
3578                    metric.abc,
3579                    @r###"
3580                {
3581                  "assignments": 0.0,
3582                  "branches": 0.0,
3583                  "conditions": 0.0,
3584                  "magnitude": 0.0,
3585                  "assignments_average": 0.0,
3586                  "branches_average": 0.0,
3587                  "conditions_average": 0.0,
3588                  "assignments_min": 0.0,
3589                  "assignments_max": 0.0,
3590                  "branches_min": 0.0,
3591                  "branches_max": 0.0,
3592                  "conditions_min": 0.0,
3593                  "conditions_max": 0.0
3594                }
3595                "###
3596                );
3597            },
3598        );
3599    }
3600
3601    #[test]
3602    fn csharp_ternary_conditions() {
3603        check_metrics::<CsharpParser>(
3604            "class A {
3605                int Sign(int x) {
3606                    return (x > 0) ? 1 : (x < 0 ? -1 : 0);
3607                }
3608            }",
3609            "foo.cs",
3610            |metric| insta::assert_json_snapshot!(metric.abc),
3611        );
3612    }
3613
3614    #[test]
3615    fn csharp_malformed_parenthesized_no_panic() {
3616        check_metrics::<CsharpParser>("class A { void M() { if (( }) }", "foo.cs", |metric| {
3617            // Don't panic on malformed source.
3618            assert_eq!(metric.abc.assignments(), 0.0);
3619            assert_eq!(metric.abc.branches(), 0.0);
3620        });
3621    }
3622
3623    #[test]
3624    fn csharp_function_pointer_type_no_double_count() {
3625        // EC1 extension — `<` and `>` are also parameter-list delimiters
3626        // for unsafe function-pointer types. `FunctionPointerType` must
3627        // be in the LT/GT exclusion list, otherwise these brackets
3628        // accumulate spurious `conditions` counts.
3629        check_metrics::<CsharpParser>(
3630            "unsafe class A {
3631                public delegate*<int, int, int> Adder;
3632                public delegate*<string, void> Logger;
3633            }",
3634            "foo.cs",
3635            |metric| {
3636                assert_eq!(
3637                    metric.abc.conditions(),
3638                    0.0,
3639                    "function-pointer-type angle brackets must not count"
3640                );
3641            },
3642        );
3643    }
3644
3645    #[test]
3646    fn csharp_generic_type_args_no_double_count() {
3647        // EC1 — `<` and `>` inside TypeArgumentList must not count as
3648        // boolean conditions.
3649        check_metrics::<CsharpParser>(
3650            "class A {
3651                void M(System.Collections.Generic.Dictionary<string, System.Collections.Generic.List<int>> d) {
3652                    System.Console.WriteLine(d);
3653                }
3654            }",
3655            "foo.cs",
3656            |metric| insta::assert_json_snapshot!(metric.abc),
3657        );
3658    }
3659
3660    #[test]
3661    fn csharp_aliased_invocation_expression_branches() {
3662        // Regression for issue #94 (lesson #2): the C# grammar emits three
3663        // aliased `kind_id`s for `invocation_expression`. Code that matches
3664        // only the unsuffixed `Csharp::InvocationExpression` undercounts ABC
3665        // branches whenever the AST emits an aliased variant. The three
3666        // method calls live in `M`, so the per-method maximum (visible at
3667        // the unit-space aggregate as `branches_max`) must be 3.
3668        check_metrics::<CsharpParser>(
3669            "class A {
3670                void M() {
3671                    System.Console.WriteLine(1);
3672                    System.Console.WriteLine(2);
3673                    System.Console.WriteLine(3);
3674                }
3675            }",
3676            "foo.cs",
3677            |metric| {
3678                assert_eq!(metric.abc.branches_max(), 3.0);
3679                assert_eq!(metric.abc.conditions_max(), 0.0);
3680            },
3681        );
3682    }
3683
3684    #[test]
3685    fn php_zero_abc() {
3686        check_metrics::<PhpParser>("<?php\n", "foo.php", |metric| {
3687            assert_eq!(metric.abc.assignments_sum(), 0.0);
3688            assert_eq!(metric.abc.branches_sum(), 0.0);
3689            assert_eq!(metric.abc.conditions_sum(), 0.0);
3690            insta::assert_json_snapshot!(metric.abc);
3691        });
3692    }
3693
3694    #[test]
3695    fn php_simple_assignment() {
3696        check_metrics::<PhpParser>(
3697            "<?php
3698function f(): void {
3699    $a = 1;
3700    $b = 2;
3701}",
3702            "foo.php",
3703            |metric| insta::assert_json_snapshot!(metric.abc),
3704        );
3705    }
3706
3707    #[test]
3708    fn php_augmented_assignment() {
3709        check_metrics::<PhpParser>(
3710            "<?php
3711function f(int $x): int {
3712    $a = 0;
3713    $a += $x;
3714    $a -= 1;
3715    $a *= 2;
3716    return $a;
3717}",
3718            "foo.php",
3719            |metric| insta::assert_json_snapshot!(metric.abc),
3720        );
3721    }
3722
3723    #[test]
3724    fn php_const_excluded() {
3725        // Constant declarations and enum cases are NOT counted as
3726        // assignments — they declare immutable values.
3727        check_metrics::<PhpParser>(
3728            "<?php
3729class A {
3730    const PI = 3.14;
3731    const E = 2.71;
3732}
3733enum Color {
3734    case Red;
3735    case Green;
3736}",
3737            "foo.php",
3738            |metric| insta::assert_json_snapshot!(metric.abc),
3739        );
3740    }
3741
3742    #[test]
3743    fn php_function_call() {
3744        check_metrics::<PhpParser>(
3745            "<?php
3746function f(): void {
3747    foo();
3748    bar(1, 2);
3749}",
3750            "foo.php",
3751            |metric| insta::assert_json_snapshot!(metric.abc),
3752        );
3753    }
3754
3755    #[test]
3756    fn php_method_call() {
3757        check_metrics::<PhpParser>(
3758            "<?php
3759function f($obj): void {
3760    $obj->m1();
3761    $obj->m2(1);
3762}",
3763            "foo.php",
3764            |metric| insta::assert_json_snapshot!(metric.abc),
3765        );
3766    }
3767
3768    #[test]
3769    fn php_static_call() {
3770        check_metrics::<PhpParser>(
3771            "<?php
3772function f(): void {
3773    Foo::bar();
3774    Foo::baz(1);
3775}",
3776            "foo.php",
3777            |metric| insta::assert_json_snapshot!(metric.abc),
3778        );
3779    }
3780
3781    #[test]
3782    fn php_nullsafe_call() {
3783        check_metrics::<PhpParser>(
3784            "<?php
3785function f($obj): void {
3786    $obj?->m1();
3787    $obj?->m2(1);
3788}",
3789            "foo.php",
3790            |metric| insta::assert_json_snapshot!(metric.abc),
3791        );
3792    }
3793
3794    #[test]
3795    fn php_object_creation() {
3796        check_metrics::<PhpParser>(
3797            "<?php
3798function f(): void {
3799    new Foo();
3800    new Bar(1);
3801}",
3802            "foo.php",
3803            |metric| insta::assert_json_snapshot!(metric.abc),
3804        );
3805    }
3806
3807    #[test]
3808    fn php_comparison_eq() {
3809        check_metrics::<PhpParser>(
3810            "<?php
3811function f(int $a, int $b): bool {
3812    return $a == $b || $a != $b;
3813}",
3814            "foo.php",
3815            |metric| insta::assert_json_snapshot!(metric.abc),
3816        );
3817    }
3818
3819    #[test]
3820    fn php_comparison_strict() {
3821        check_metrics::<PhpParser>(
3822            "<?php
3823function f(int $a, int $b): bool {
3824    return $a === $b || $a !== $b;
3825}",
3826            "foo.php",
3827            |metric| insta::assert_json_snapshot!(metric.abc),
3828        );
3829    }
3830
3831    #[test]
3832    fn php_spaceship() {
3833        check_metrics::<PhpParser>(
3834            "<?php
3835function f(int $a, int $b): int {
3836    return $a <=> $b;
3837}",
3838            "foo.php",
3839            |metric| insta::assert_json_snapshot!(metric.abc),
3840        );
3841    }
3842
3843    #[test]
3844    fn php_instanceof() {
3845        check_metrics::<PhpParser>(
3846            "<?php
3847function f($x): bool {
3848    return $x instanceof Foo;
3849}",
3850            "foo.php",
3851            |metric| insta::assert_json_snapshot!(metric.abc),
3852        );
3853    }
3854
3855    #[test]
3856    fn php_complex_function() {
3857        // One snippet exercising A, B, C buckets together.
3858        check_metrics::<PhpParser>(
3859            "<?php
3860function f(int $a, int $b): int {
3861    $sum = $a + $b;
3862    $prod = $a * $b;
3863    if ($sum > 0 && $prod === 0) {
3864        return foo($sum);
3865    }
3866    return bar()->double();
3867}",
3868            "foo.php",
3869            |metric| insta::assert_json_snapshot!(metric.abc),
3870        );
3871    }
3872
3873    // --- Kotlin ABC tests -------------------------------------------------
3874
3875    #[test]
3876    fn kotlin_empty_class() {
3877        check_metrics::<KotlinParser>("class C {}", "foo.kt", |metric| {
3878            assert_eq!(metric.abc.assignments_sum(), 0.0);
3879            assert_eq!(metric.abc.branches_sum(), 0.0);
3880            assert_eq!(metric.abc.conditions_sum(), 0.0);
3881            insta::assert_json_snapshot!(metric.abc);
3882        });
3883    }
3884
3885    #[test]
3886    fn kotlin_val_declarations_are_not_assignments() {
3887        // `val` introduces an immutable binding — the `=` initialising it
3888        // is not an assignment in the ABC sense.
3889        check_metrics::<KotlinParser>(
3890            "class C {
3891                val a: Int = 1
3892                val b: Int = 2
3893                val c: Int = 3
3894            }",
3895            "foo.kt",
3896            |metric| {
3897                assert_eq!(metric.abc.assignments_sum(), 0.0);
3898                assert_eq!(metric.abc.branches_sum(), 0.0);
3899                insta::assert_json_snapshot!(metric.abc);
3900            },
3901        );
3902    }
3903
3904    #[test]
3905    fn kotlin_var_declarations_count_assignment() {
3906        // `var` initialisers count as assignments (mutable binding).
3907        check_metrics::<KotlinParser>(
3908            "class C {
3909                var a: Int = 1
3910                var b: Int = 2
3911            }",
3912            "foo.kt",
3913            |metric| {
3914                assert_eq!(metric.abc.assignments_sum(), 2.0);
3915                insta::assert_json_snapshot!(metric.abc);
3916            },
3917        );
3918    }
3919
3920    #[test]
3921    fn kotlin_augmented_assignments_count() {
3922        // Augmented operators (+=, -=, etc.) and ++/-- always count.
3923        check_metrics::<KotlinParser>(
3924            "fun m() {
3925                var x = 0
3926                x += 1
3927                x -= 2
3928                x *= 3
3929                x++
3930                --x
3931            }",
3932            "foo.kt",
3933            |metric| {
3934                // var declaration (var x = 0): +1
3935                // x += 1, x -= 2, x *= 3, x++, --x: +5
3936                assert_eq!(metric.abc.assignments_sum(), 6.0);
3937                insta::assert_json_snapshot!(metric.abc);
3938            },
3939        );
3940    }
3941
3942    #[test]
3943    fn kotlin_branches_call_expression() {
3944        check_metrics::<KotlinParser>(
3945            "fun m() {
3946                println(\"a\")
3947                println(\"b\")
3948                println(\"c\")
3949            }",
3950            "foo.kt",
3951            |metric| {
3952                assert_eq!(metric.abc.branches_sum(), 3.0);
3953                insta::assert_json_snapshot!(metric.abc);
3954            },
3955        );
3956    }
3957
3958    #[test]
3959    fn kotlin_object_construction_branch() {
3960        // Kotlin's object construction is just `Foo()` — a `CallExpression`.
3961        check_metrics::<KotlinParser>(
3962            "class P(val x: Int)
3963            fun m(): P = P(1)",
3964            "foo.kt",
3965            |metric| {
3966                assert_eq!(metric.abc.branches_sum(), 1.0);
3967                insta::assert_json_snapshot!(metric.abc);
3968            },
3969        );
3970    }
3971
3972    #[test]
3973    fn kotlin_comparisons_count_conditions() {
3974        check_metrics::<KotlinParser>(
3975            "fun m(a: Int, b: Int): Boolean {
3976                val r1 = a < b
3977                val r2 = a > b
3978                val r3 = a <= b
3979                val r4 = a >= b
3980                val r5 = a == b
3981                val r6 = a != b
3982                return r1 || r2 || r3 || r4 || r5 || r6
3983            }",
3984            "foo.kt",
3985            |metric| {
3986                // Six binary operators: <, >, <=, >=, ==, != → 6 conditions.
3987                assert_eq!(metric.abc.conditions_sum(), 6.0);
3988                insta::assert_json_snapshot!(metric.abc);
3989            },
3990        );
3991    }
3992
3993    #[test]
3994    fn kotlin_identity_equality_conditions() {
3995        // `===` / `!==` are referential equality in Kotlin; they count too.
3996        check_metrics::<KotlinParser>(
3997            "fun m(a: Any, b: Any): Boolean {
3998                return a === b || a !== b
3999            }",
4000            "foo.kt",
4001            |metric| {
4002                assert_eq!(metric.abc.conditions_sum(), 2.0);
4003                insta::assert_json_snapshot!(metric.abc);
4004            },
4005        );
4006    }
4007
4008    #[test]
4009    fn kotlin_else_branch_counts() {
4010        check_metrics::<KotlinParser>(
4011            "fun m(x: Int): Int {
4012                return if (x > 0) 1 else -1
4013            }",
4014            "foo.kt",
4015            |metric| {
4016                // condition: > (1) + else (1) = 2
4017                assert_eq!(metric.abc.conditions_sum(), 2.0);
4018                insta::assert_json_snapshot!(metric.abc);
4019            },
4020        );
4021    }
4022
4023    #[test]
4024    fn kotlin_when_entries_count() {
4025        check_metrics::<KotlinParser>(
4026            "fun m(x: Int): Int {
4027                return when (x) {
4028                    1 -> 10
4029                    2 -> 20
4030                    else -> 0
4031                }
4032            }",
4033            "foo.kt",
4034            |metric| {
4035                // Each WhenEntry counts once (including `else`).
4036                assert_eq!(metric.abc.conditions_sum(), 3.0);
4037                insta::assert_json_snapshot!(metric.abc);
4038            },
4039        );
4040    }
4041
4042    #[test]
4043    fn kotlin_catch_block_counts() {
4044        check_metrics::<KotlinParser>(
4045            "fun m() {
4046                try {
4047                    println(\"ok\")
4048                } catch (e: Exception) {
4049                    println(\"err\")
4050                }
4051            }",
4052            "foo.kt",
4053            |metric| {
4054                // CatchBlock contributes 1 condition.
4055                assert_eq!(metric.abc.conditions_sum(), 1.0);
4056                insta::assert_json_snapshot!(metric.abc);
4057            },
4058        );
4059    }
4060
4061    #[test]
4062    fn kotlin_elvis_and_safe_cast() {
4063        // `?:` (elvis) and `as?` (safe cast) are condition-like.
4064        check_metrics::<KotlinParser>(
4065            "fun m(s: String?): Int {
4066                val n = (s as? Int) ?: 0
4067                return n
4068            }",
4069            "foo.kt",
4070            |metric| {
4071                // as? (+1) + ?: (+1) = 2 conditions.
4072                assert_eq!(metric.abc.conditions_sum(), 2.0);
4073                insta::assert_json_snapshot!(metric.abc);
4074            },
4075        );
4076    }
4077
4078    #[test]
4079    fn kotlin_generic_brackets_not_conditions() {
4080        // `<` / `>` used as type-parameter brackets must not be counted.
4081        check_metrics::<KotlinParser>(
4082            "class Box<T>(val v: T)
4083            fun <T> wrap(x: T): Box<T> = Box(x)",
4084            "foo.kt",
4085            |metric| {
4086                // No comparisons — only generic brackets.
4087                assert_eq!(metric.abc.conditions_sum(), 0.0);
4088                insta::assert_json_snapshot!(metric.abc);
4089            },
4090        );
4091    }
4092
4093    #[test]
4094    fn kotlin_class_with_methods_and_branches() {
4095        check_metrics::<KotlinParser>(
4096            "class C {
4097                var counter: Int = 0
4098                fun bump() {
4099                    counter += 1
4100                    println(counter)
4101                }
4102            }",
4103            "foo.kt",
4104            |metric| {
4105                // assignments: var counter = 0 (+1), counter += 1 (+1) = 2
4106                // branches: println(counter) = 1
4107                assert_eq!(metric.abc.assignments_sum(), 2.0);
4108                assert_eq!(metric.abc.branches_sum(), 1.0);
4109                assert_eq!(metric.abc.conditions_sum(), 0.0);
4110                insta::assert_json_snapshot!(metric.abc);
4111            },
4112        );
4113    }
4114
4115    #[test]
4116    fn kotlin_object_singleton_abc() {
4117        check_metrics::<KotlinParser>(
4118            "object Util {
4119                fun work(x: Int): Int {
4120                    var y = x
4121                    y += 1
4122                    if (y > 0) {
4123                        return y
4124                    }
4125                    return -1
4126                }
4127            }",
4128            "foo.kt",
4129            |metric| {
4130                // assignments: var y = x (+1), y += 1 (+1) = 2
4131                // branches: 0 (return is not a call)
4132                // conditions: y > 0 (+1) = 1
4133                assert_eq!(metric.abc.assignments_sum(), 2.0);
4134                assert_eq!(metric.abc.branches_sum(), 0.0);
4135                assert_eq!(metric.abc.conditions_sum(), 1.0);
4136                insta::assert_json_snapshot!(metric.abc);
4137            },
4138        );
4139    }
4140
4141    #[test]
4142    fn kotlin_interface_abc() {
4143        // Pure-abstract interface with no bodies — all-zero.
4144        check_metrics::<KotlinParser>(
4145            "interface I {
4146                fun work(): Int
4147                fun describe(): String
4148            }",
4149            "foo.kt",
4150            |metric| {
4151                assert_eq!(metric.abc.assignments_sum(), 0.0);
4152                assert_eq!(metric.abc.branches_sum(), 0.0);
4153                assert_eq!(metric.abc.conditions_sum(), 0.0);
4154                insta::assert_json_snapshot!(metric.abc);
4155            },
4156        );
4157    }
4158
4159    #[test]
4160    fn kotlin_nested_class_abc() {
4161        check_metrics::<KotlinParser>(
4162            "class Outer {
4163                var o: Int = 0
4164                class Nested {
4165                    var n: Int = 0
4166                    fun bump() { n += 1 }
4167                }
4168            }",
4169            "foo.kt",
4170            |metric| {
4171                // Outer: var o = 0 (+1)
4172                // Nested: var n = 0 (+1), n += 1 (+1) = 2
4173                // total assignments = 3
4174                assert_eq!(metric.abc.assignments_sum(), 3.0);
4175                insta::assert_json_snapshot!(metric.abc);
4176            },
4177        );
4178    }
4179
4180    #[test]
4181    fn kotlin_data_class_abc() {
4182        // `data class` with primary-constructor `val`s — no assignments
4183        // (vals don't count) and no body conditions.
4184        check_metrics::<KotlinParser>(
4185            "data class Point(val x: Int, val y: Int)",
4186            "foo.kt",
4187            |metric| {
4188                assert_eq!(metric.abc.assignments_sum(), 0.0);
4189                assert_eq!(metric.abc.branches_sum(), 0.0);
4190                assert_eq!(metric.abc.conditions_sum(), 0.0);
4191                insta::assert_json_snapshot!(metric.abc);
4192            },
4193        );
4194    }
4195
4196    #[test]
4197    fn kotlin_primary_constructor_default_value_not_assignment() {
4198        // Regression: default values on primary-constructor `val`
4199        // parameters are initialisers, not assignments. Without
4200        // `ClassParameter` pushing a declaration sentinel, the `=` token
4201        // here would be counted unconditionally as a standalone
4202        // assignment.
4203        check_metrics::<KotlinParser>("class C(val a: Int = 5)", "foo.kt", |metric| {
4204            // `val a = 5` → suppressed (Const sentinel).
4205            assert_eq!(metric.abc.assignments_sum(), 0.0);
4206            insta::assert_json_snapshot!(metric.abc);
4207        });
4208    }
4209
4210    // --- TypeScript / TSX ABC tests --------------------------------------
4211    //
4212    // Assignment, branch, condition counting per Fitzpatrick:
4213    // - Augmented assignment / `++` / `--` always count.
4214    // - Plain `=` counts unless inside `const` declaration.
4215    // - `call_expression` / `new_expression` count as branches.
4216    // - Comparison / equality operators, ternary `?`, `??`, control-flow
4217    //   arms (`else`, `case`, `default`, `catch`, `try`, `instanceof`),
4218    //   and `<`/`>` (outside `type_arguments` / `type_parameters`) count
4219    //   as conditions.
4220
4221    #[test]
4222    fn typescript_assignments_basic() {
4223        check_metrics::<TypescriptParser>(
4224            "class C {
4225                m(): void {
4226                    let x = 0;          // const-sentinel suppressed since `let`, but x is Var → +1
4227                    x = 1;              // +1
4228                    x += 2;             // +1
4229                    x++;                // +1
4230                }
4231            }",
4232            "foo.ts",
4233            |metric| {
4234                assert_eq!(metric.abc.assignments_sum(), 4.0);
4235                insta::assert_json_snapshot!(metric.abc);
4236            },
4237        );
4238    }
4239
4240    #[test]
4241    fn typescript_const_excluded_from_assignments() {
4242        check_metrics::<TypescriptParser>(
4243            "class C {
4244                m(): void {
4245                    const a = 1;        // suppressed (Const sentinel)
4246                    const b = 2;        // suppressed
4247                    let c = 3;          // +1 (Var sentinel)
4248                }
4249            }",
4250            "foo.ts",
4251            |metric| {
4252                assert_eq!(metric.abc.assignments_sum(), 1.0);
4253                insta::assert_json_snapshot!(metric.abc);
4254            },
4255        );
4256    }
4257
4258    #[test]
4259    fn typescript_branches_function_calls() {
4260        check_metrics::<TypescriptParser>(
4261            "class C {
4262                m(): void {
4263                    foo();              // +1
4264                    bar(1, 2);          // +1
4265                    new Date();         // +1
4266                }
4267            }",
4268            "foo.ts",
4269            |metric| {
4270                assert_eq!(metric.abc.branches_sum(), 3.0);
4271                insta::assert_json_snapshot!(metric.abc);
4272            },
4273        );
4274    }
4275
4276    #[test]
4277    fn typescript_conditions_comparison_operators() {
4278        check_metrics::<TypescriptParser>(
4279            "class C {
4280                m(x: number, y: number): boolean {
4281                    return x == y       // +1
4282                        || x === y      // +1
4283                        || x != y       // +1
4284                        || x !== y      // +1
4285                        || x < y        // +1
4286                        || x <= y       // +1
4287                        || x > y        // +1
4288                        || x >= y;      // +1
4289                }
4290            }",
4291            "foo.ts",
4292            |metric| {
4293                assert_eq!(metric.abc.conditions_sum(), 8.0);
4294                insta::assert_json_snapshot!(metric.abc);
4295            },
4296        );
4297    }
4298
4299    #[test]
4300    fn typescript_conditions_control_flow_arms() {
4301        check_metrics::<TypescriptParser>(
4302            "class C {
4303                m(x: number): number {
4304                    try {                       // +1 (try)
4305                        if (x > 0) {            // +1 (>)
4306                            return 1;
4307                        } else {                // +1 (else)
4308                            return -1;
4309                        }
4310                    } catch (e) {               // +1 (catch)
4311                        return 0;
4312                    }
4313                }
4314            }",
4315            "foo.ts",
4316            |metric| {
4317                assert_eq!(metric.abc.conditions_sum(), 4.0);
4318                insta::assert_json_snapshot!(metric.abc);
4319            },
4320        );
4321    }
4322
4323    #[test]
4324    fn typescript_conditions_switch_case() {
4325        check_metrics::<TypescriptParser>(
4326            "class C {
4327                m(x: number): number {
4328                    switch (x) {
4329                        case 1:                 // +1
4330                            return 1;
4331                        case 2:                 // +1
4332                            return 2;
4333                        default:                // +1
4334                            return 0;
4335                    }
4336                }
4337            }",
4338            "foo.ts",
4339            |metric| {
4340                assert_eq!(metric.abc.conditions_sum(), 3.0);
4341                insta::assert_json_snapshot!(metric.abc);
4342            },
4343        );
4344    }
4345
4346    #[test]
4347    fn typescript_ternary_and_nullish() {
4348        check_metrics::<TypescriptParser>(
4349            "class C {
4350                m(x: number | null): number {
4351                    return x !== null           // +1 (!==)
4352                        ? x                     // +1 (ternary ?)
4353                        : 0;
4354                }
4355                n(x: number | null): number {
4356                    return x ?? 0;              // +1 (??)
4357                }
4358            }",
4359            "foo.ts",
4360            |metric| {
4361                assert_eq!(metric.abc.conditions_sum(), 3.0);
4362                insta::assert_json_snapshot!(metric.abc);
4363            },
4364        );
4365    }
4366
4367    #[test]
4368    fn typescript_instanceof_counts_as_condition() {
4369        check_metrics::<TypescriptParser>(
4370            "class C {
4371                m(o: unknown): boolean {
4372                    return o instanceof C;      // +1
4373                }
4374            }",
4375            "foo.ts",
4376            |metric| {
4377                assert_eq!(metric.abc.conditions_sum(), 1.0);
4378                insta::assert_json_snapshot!(metric.abc);
4379            },
4380        );
4381    }
4382
4383    #[test]
4384    fn typescript_generic_lt_gt_not_a_condition() {
4385        // `<T>` in `class C<T>` and `Array<number>` should not contribute
4386        // to conditions even though the tokens are `<` and `>`.
4387        check_metrics::<TypescriptParser>(
4388            "class C<T> {
4389                xs: Array<number> = [];
4390                m(): void {
4391                    const arr: Array<string> = [];   // suppressed const
4392                    void arr;
4393                }
4394            }",
4395            "foo.ts",
4396            |metric| {
4397                assert_eq!(metric.abc.conditions_sum(), 0.0);
4398                insta::assert_json_snapshot!(metric.abc);
4399            },
4400        );
4401    }
4402
4403    #[test]
4404    fn typescript_abstract_class_abc() {
4405        // Abstract methods have no body — they contribute nothing.
4406        check_metrics::<TypescriptParser>(
4407            "abstract class C {
4408                abstract a(): void;
4409                m(x: number): number {
4410                    if (x > 0) return 1;        // +1 condition
4411                    return 0;
4412                }
4413            }",
4414            "foo.ts",
4415            |metric| {
4416                assert_eq!(metric.abc.conditions_sum(), 1.0);
4417                assert_eq!(metric.abc.branches_sum(), 0.0);
4418                insta::assert_json_snapshot!(metric.abc);
4419            },
4420        );
4421    }
4422
4423    #[test]
4424    fn typescript_interface_abc_zero() {
4425        check_metrics::<TypescriptParser>(
4426            "interface I {
4427                a(): void;
4428                b(): number;
4429                p: string;
4430            }",
4431            "foo.ts",
4432            |metric| {
4433                assert_eq!(metric.abc.assignments_sum(), 0.0);
4434                assert_eq!(metric.abc.branches_sum(), 0.0);
4435                assert_eq!(metric.abc.conditions_sum(), 0.0);
4436                insta::assert_json_snapshot!(metric.abc);
4437            },
4438        );
4439    }
4440
4441    #[test]
4442    fn typescript_arrow_field_contributes_abc() {
4443        // Arrow function class members are function spaces; their
4444        // assignments/branches/conditions are counted.
4445        check_metrics::<TypescriptParser>(
4446            "class C {
4447                arrow = (x: number) => {
4448                    if (x > 0) {                // +1 condition
4449                        return foo();           // +1 branch
4450                    }
4451                    return 0;
4452                };
4453            }",
4454            "foo.ts",
4455            |metric| {
4456                assert_eq!(metric.abc.conditions_sum(), 1.0);
4457                assert_eq!(metric.abc.branches_sum(), 1.0);
4458                insta::assert_json_snapshot!(metric.abc);
4459            },
4460        );
4461    }
4462
4463    #[test]
4464    fn typescript_parameter_property_init_not_assignment() {
4465        // Parameter properties don't introduce a `=` token themselves;
4466        // only the explicit `let z = 0` body assignment is counted.
4467        // The class field initializer `f: number = 0` likewise has a `=`
4468        // that DOES count (matches `typescript_assignments_basic`).
4469        check_metrics::<TypescriptParser>(
4470            "class C {
4471                f: number = 0;
4472                constructor(public x: number, private y: string) {
4473                    let z = 0;
4474                }
4475            }",
4476            "foo.ts",
4477            |metric| {
4478                // f's initializer + `let z = 0` = 2 assignments; the
4479                // parameter properties contribute zero.
4480                assert_eq!(metric.abc.assignments_sum(), 2.0);
4481                insta::assert_json_snapshot!(metric.abc);
4482            },
4483        );
4484    }
4485
4486    // TSX parity
4487
4488    #[test]
4489    fn tsx_assignments_basic() {
4490        check_metrics::<TsxParser>(
4491            "class C {
4492                m(): void {
4493                    let x = 0;
4494                    x = 1;
4495                    x += 2;
4496                    x++;
4497                }
4498            }",
4499            "foo.tsx",
4500            |metric| {
4501                assert_eq!(metric.abc.assignments_sum(), 4.0);
4502                insta::assert_json_snapshot!(metric.abc);
4503            },
4504        );
4505    }
4506
4507    #[test]
4508    fn tsx_const_excluded_from_assignments() {
4509        check_metrics::<TsxParser>(
4510            "class C {
4511                m(): void {
4512                    const a = 1;
4513                    let b = 2;
4514                }
4515            }",
4516            "foo.tsx",
4517            |metric| {
4518                assert_eq!(metric.abc.assignments_sum(), 1.0);
4519                insta::assert_json_snapshot!(metric.abc);
4520            },
4521        );
4522    }
4523
4524    #[test]
4525    fn tsx_branches_function_calls() {
4526        check_metrics::<TsxParser>(
4527            "class C {
4528                m(): void {
4529                    foo();
4530                    new Date();
4531                }
4532            }",
4533            "foo.tsx",
4534            |metric| {
4535                assert_eq!(metric.abc.branches_sum(), 2.0);
4536                insta::assert_json_snapshot!(metric.abc);
4537            },
4538        );
4539    }
4540
4541    #[test]
4542    fn tsx_conditions_comparison_operators() {
4543        check_metrics::<TsxParser>(
4544            "class C {
4545                m(x: number, y: number): boolean {
4546                    return x == y || x < y || x >= y;
4547                }
4548            }",
4549            "foo.tsx",
4550            |metric| {
4551                assert_eq!(metric.abc.conditions_sum(), 3.0);
4552                insta::assert_json_snapshot!(metric.abc);
4553            },
4554        );
4555    }
4556
4557    #[test]
4558    fn tsx_conditions_control_flow_arms() {
4559        check_metrics::<TsxParser>(
4560            "class C {
4561                m(x: number): number {
4562                    try {
4563                        if (x > 0) return 1;
4564                        else return -1;
4565                    } catch (e) {
4566                        return 0;
4567                    }
4568                }
4569            }",
4570            "foo.tsx",
4571            |metric| {
4572                assert_eq!(metric.abc.conditions_sum(), 4.0);
4573                insta::assert_json_snapshot!(metric.abc);
4574            },
4575        );
4576    }
4577
4578    #[test]
4579    fn tsx_conditions_switch_case() {
4580        check_metrics::<TsxParser>(
4581            "class C {
4582                m(x: number): number {
4583                    switch (x) {
4584                        case 1: return 1;
4585                        case 2: return 2;
4586                        default: return 0;
4587                    }
4588                }
4589            }",
4590            "foo.tsx",
4591            |metric| {
4592                assert_eq!(metric.abc.conditions_sum(), 3.0);
4593                insta::assert_json_snapshot!(metric.abc);
4594            },
4595        );
4596    }
4597
4598    #[test]
4599    fn tsx_ternary_and_nullish() {
4600        check_metrics::<TsxParser>(
4601            "class C {
4602                m(x: number | null): number {
4603                    return x !== null ? x : 0;
4604                }
4605                n(x: number | null): number { return x ?? 0; }
4606            }",
4607            "foo.tsx",
4608            |metric| {
4609                assert_eq!(metric.abc.conditions_sum(), 3.0);
4610                insta::assert_json_snapshot!(metric.abc);
4611            },
4612        );
4613    }
4614
4615    #[test]
4616    fn tsx_instanceof_counts_as_condition() {
4617        check_metrics::<TsxParser>(
4618            "class C { m(o: unknown): boolean { return o instanceof C; } }",
4619            "foo.tsx",
4620            |metric| {
4621                assert_eq!(metric.abc.conditions_sum(), 1.0);
4622                insta::assert_json_snapshot!(metric.abc);
4623            },
4624        );
4625    }
4626
4627    #[test]
4628    fn tsx_generic_lt_gt_not_a_condition() {
4629        check_metrics::<TsxParser>(
4630            "class C<T> { xs: Array<number> = []; }",
4631            "foo.tsx",
4632            |metric| {
4633                assert_eq!(metric.abc.conditions_sum(), 0.0);
4634                insta::assert_json_snapshot!(metric.abc);
4635            },
4636        );
4637    }
4638
4639    #[test]
4640    fn tsx_abstract_class_abc() {
4641        check_metrics::<TsxParser>(
4642            "abstract class C {
4643                abstract a(): void;
4644                m(x: number): number {
4645                    if (x > 0) return 1;
4646                    return 0;
4647                }
4648            }",
4649            "foo.tsx",
4650            |metric| {
4651                assert_eq!(metric.abc.conditions_sum(), 1.0);
4652                assert_eq!(metric.abc.branches_sum(), 0.0);
4653                insta::assert_json_snapshot!(metric.abc);
4654            },
4655        );
4656    }
4657
4658    #[test]
4659    fn tsx_interface_abc_zero() {
4660        check_metrics::<TsxParser>(
4661            "interface I { a(): void; p: string; }",
4662            "foo.tsx",
4663            |metric| {
4664                assert_eq!(metric.abc.assignments_sum(), 0.0);
4665                assert_eq!(metric.abc.branches_sum(), 0.0);
4666                assert_eq!(metric.abc.conditions_sum(), 0.0);
4667                insta::assert_json_snapshot!(metric.abc);
4668            },
4669        );
4670    }
4671
4672    #[test]
4673    fn tsx_arrow_field_contributes_abc() {
4674        check_metrics::<TsxParser>(
4675            "class C {
4676                arrow = (x: number) => {
4677                    if (x > 0) return foo();
4678                    return 0;
4679                };
4680            }",
4681            "foo.tsx",
4682            |metric| {
4683                assert_eq!(metric.abc.conditions_sum(), 1.0);
4684                assert_eq!(metric.abc.branches_sum(), 1.0);
4685                insta::assert_json_snapshot!(metric.abc);
4686            },
4687        );
4688    }
4689
4690    #[test]
4691    fn tsx_parameter_property_init_not_assignment() {
4692        // Parameter properties contribute no `=`; the body's `let z = 0`
4693        // and the field initializer do.
4694        check_metrics::<TsxParser>(
4695            "class C {
4696                f: number = 0;
4697                constructor(public x: number) { let z = 0; }
4698            }",
4699            "foo.tsx",
4700            |metric| {
4701                assert_eq!(metric.abc.assignments_sum(), 2.0);
4702                insta::assert_json_snapshot!(metric.abc);
4703            },
4704        );
4705    }
4706
4707    // --- Ruby ABC tests ---------------------------------------------------
4708    //
4709    // Each Ruby `assignment` / `operator_assignment` is one assignment
4710    // regardless of whether the LHS is a local, instance, or class
4711    // variable. Every `call` / `super` / `yield` is one branch. Every
4712    // comparison-operator token inside a `binary` node plus each
4713    // `else` / `elsif` / `when` / `then` / `?` / `rescue` clause is
4714    // one condition.
4715
4716    #[test]
4717    fn ruby_zero_abc() {
4718        check_metrics::<RubyParser>("\n", "foo.rb", |metric| {
4719            assert_eq!(metric.abc.assignments_sum(), 0.0);
4720            assert_eq!(metric.abc.branches_sum(), 0.0);
4721            assert_eq!(metric.abc.conditions_sum(), 0.0);
4722            insta::assert_json_snapshot!(metric.abc);
4723        });
4724    }
4725
4726    #[test]
4727    fn ruby_simple_assignment() {
4728        check_metrics::<RubyParser>("def f\n  a = 1\n  b = 2\nend\n", "foo.rb", |metric| {
4729            assert_eq!(metric.abc.assignments_sum(), 2.0);
4730            assert_eq!(metric.abc.branches_sum(), 0.0);
4731            assert_eq!(metric.abc.conditions_sum(), 0.0);
4732            insta::assert_json_snapshot!(metric.abc);
4733        });
4734    }
4735
4736    #[test]
4737    fn ruby_augmented_assignment() {
4738        // `+=`, `-=`, `*=` are `operator_assignment` nodes — each is
4739        // one assignment. Plain `=` to set the initial value adds one
4740        // more.
4741        check_metrics::<RubyParser>(
4742            "def f(x)\n  a = 0\n  a += x\n  a -= 1\n  a *= 2\nend\n",
4743            "foo.rb",
4744            |metric| {
4745                assert_eq!(metric.abc.assignments_sum(), 4.0);
4746                insta::assert_json_snapshot!(metric.abc);
4747            },
4748        );
4749    }
4750
4751    #[test]
4752    fn ruby_logical_augmented_assignment() {
4753        // `||=` and `&&=` are also `operator_assignment` nodes.
4754        check_metrics::<RubyParser>("def f\n  @x ||= 0\n  @x &&= 1\nend\n", "foo.rb", |metric| {
4755            assert_eq!(metric.abc.assignments_sum(), 2.0);
4756            insta::assert_json_snapshot!(metric.abc);
4757        });
4758    }
4759
4760    #[test]
4761    fn ruby_method_call_branch() {
4762        // Each method invocation is one branch.
4763        check_metrics::<RubyParser>(
4764            "def f(obj)\n  foo()\n  obj.bar(1)\nend\n",
4765            "foo.rb",
4766            |metric| {
4767                assert_eq!(metric.abc.branches_sum(), 2.0);
4768                insta::assert_json_snapshot!(metric.abc);
4769            },
4770        );
4771    }
4772
4773    #[test]
4774    fn ruby_super_and_yield_branches() {
4775        // `super` and `yield` both count as branches (control-pass).
4776        check_metrics::<RubyParser>("def f\n  super\n  yield\nend\n", "foo.rb", |metric| {
4777            assert_eq!(metric.abc.branches_sum(), 2.0);
4778            assert_eq!(metric.abc.assignments_sum(), 0.0);
4779            insta::assert_json_snapshot!(metric.abc);
4780        });
4781    }
4782
4783    #[test]
4784    fn ruby_attr_macro_is_branch() {
4785        // `attr_accessor` is a `Call3` node and registers as a branch
4786        // like any method invocation.
4787        check_metrics::<RubyParser>("class A\n  attr_accessor :x\nend\n", "foo.rb", |metric| {
4788            assert_eq!(metric.abc.branches_sum(), 1.0);
4789            insta::assert_json_snapshot!(metric.abc);
4790        });
4791    }
4792
4793    #[test]
4794    fn ruby_comparison_conditions() {
4795        // Each comparison operator is one condition.
4796        check_metrics::<RubyParser>(
4797            "def f(a, b)\n  a == b\n  a != b\n  a < b\n  a > b\n  a <= b\n  a >= b\nend\n",
4798            "foo.rb",
4799            |metric| {
4800                assert_eq!(metric.abc.conditions_sum(), 6.0);
4801                insta::assert_json_snapshot!(metric.abc);
4802            },
4803        );
4804    }
4805
4806    #[test]
4807    fn ruby_spaceship_and_case_equality() {
4808        // `<=>` and `===` are comparison operators (conditions).
4809        check_metrics::<RubyParser>(
4810            "def f(a, b)\n  a <=> b\n  a === b\nend\n",
4811            "foo.rb",
4812            |metric| {
4813                assert_eq!(metric.abc.conditions_sum(), 2.0);
4814                insta::assert_json_snapshot!(metric.abc);
4815            },
4816        );
4817    }
4818
4819    #[test]
4820    fn ruby_ternary_condition() {
4821        // The `?` ternary marker is one condition; the inner `==` is
4822        // another.
4823        check_metrics::<RubyParser>("def f(x)\n  x == 0 ? :z : :nz\nend\n", "foo.rb", |metric| {
4824            assert_eq!(metric.abc.conditions_sum(), 2.0);
4825            insta::assert_json_snapshot!(metric.abc);
4826        });
4827    }
4828
4829    #[test]
4830    fn ruby_case_when_arms() {
4831        // Each `when` named clause and the `else` clause count as one
4832        // condition each; the `case` head and the implicit `then`
4833        // wrappers do not.
4834        check_metrics::<RubyParser>(
4835            "def f(x)\n  case x\n  when 1 then 'one'\n  when 2 then 'two'\n  else 'other'\n  end\nend\n",
4836            "foo.rb",
4837            |metric| {
4838                // 2 `when` + 1 `else` = 3 conditions.
4839                assert_eq!(metric.abc.conditions_sum(), 3.0);
4840                insta::assert_json_snapshot!(metric.abc);
4841            },
4842        );
4843    }
4844
4845    #[test]
4846    fn ruby_elsif_and_else() {
4847        // `elsif` and `else` named clauses are conditions; their inner
4848        // `then` wrappers are not.
4849        check_metrics::<RubyParser>(
4850            "def f(x)\n  if x > 0\n    1\n  elsif x < 0\n    -1\n  else\n    0\n  end\nend\n",
4851            "foo.rb",
4852            |metric| {
4853                // `>`(1) + `elsif`(1) + `<`(1) + `else`(1) = 4.
4854                assert_eq!(metric.abc.conditions_sum(), 4.0);
4855                insta::assert_json_snapshot!(metric.abc);
4856            },
4857        );
4858    }
4859
4860    #[test]
4861    fn ruby_rescue_clause_condition() {
4862        // The `rescue` named clause is one condition; the `rescue`
4863        // keyword token (`Rescue2`) is not counted on its own.
4864        // `do_it` without parens is an `identifier`, not a `call`, so
4865        // it contributes no branch. `handle(e)` is a `call` (1 branch).
4866        check_metrics::<RubyParser>(
4867            "def f\n  begin\n    do_it\n  rescue StandardError => e\n    handle(e)\n  end\nend\n",
4868            "foo.rb",
4869            |metric| {
4870                assert_eq!(metric.abc.conditions_sum(), 1.0);
4871                assert_eq!(metric.abc.branches_sum(), 1.0);
4872                insta::assert_json_snapshot!(metric.abc);
4873            },
4874        );
4875    }
4876
4877    #[test]
4878    fn ruby_class_complex_function() {
4879        // Mixed: assignment(=), branch(call), conditions(`>` and `==`).
4880        check_metrics::<RubyParser>(
4881            "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",
4882            "foo.rb",
4883            |metric| {
4884                assert_eq!(metric.abc.assignments_sum(), 1.0);
4885                assert_eq!(metric.abc.branches_sum(), 1.0);
4886                // `>`(1) + `==`(1) = 2 conditions. `if` is not a token;
4887                // `&&` is `AMPAMP` which is NOT a Fitzpatrick condition
4888                // in our Ruby impl (it's a logical operator, not a
4889                // comparison). The Fitzpatrick paper allows either
4890                // choice; we follow the comparison-only rule like
4891                // Java/PHP.
4892                assert_eq!(metric.abc.conditions_sum(), 2.0);
4893                insta::assert_json_snapshot!(metric.abc);
4894            },
4895        );
4896    }
4897
4898    // ---------------------------------------------------------------
4899    // Default-impl placeholder smoke tests (audited in #188).
4900    //
4901    // These tests assert that the *current* default-impl languages
4902    // return ABC = 0/0/0 for source that DOES contain branches,
4903    // conditions, and assignments. When the real impl lands for any
4904    // of these languages, the corresponding assertion below will fire
4905    // — the implementer must update the expected values, which is the
4906    // gate. Tag the follow-up issue in each test.
4907    // ---------------------------------------------------------------
4908
4909    // --- Python ABC ---------------------------------------------------
4910
4911    #[test]
4912    fn python_empty_module_zero() {
4913        check_metrics::<PythonParser>("", "empty.py", |metric| {
4914            assert_eq!(metric.abc.assignments_sum(), 0.0);
4915            assert_eq!(metric.abc.branches_sum(), 0.0);
4916            assert_eq!(metric.abc.conditions_sum(), 0.0);
4917            insta::assert_json_snapshot!(metric.abc);
4918        });
4919    }
4920
4921    #[test]
4922    fn python_plain_assignments_count() {
4923        // Three plain `=` assignments → A=3. No branches, no conditions.
4924        check_metrics::<PythonParser>("x = 1\ny = 2\nz = x\n", "foo.py", |metric| {
4925            assert_eq!(metric.abc.assignments_sum(), 3.0);
4926            assert_eq!(metric.abc.branches_sum(), 0.0);
4927            assert_eq!(metric.abc.conditions_sum(), 0.0);
4928            insta::assert_json_snapshot!(metric.abc);
4929        });
4930    }
4931
4932    #[test]
4933    fn python_typed_assignment_counts_bare_annotation_does_not() {
4934        // `x: int = 1` carries an `=`, so it counts.
4935        // `y: int` is a bare annotation (no `=`) — declares a type but
4936        // binds nothing; it must NOT inflate the assignment count.
4937        check_metrics::<PythonParser>("x: int = 1\ny: int\n", "foo.py", |metric| {
4938            assert_eq!(metric.abc.assignments_sum(), 1.0);
4939            insta::assert_json_snapshot!(metric.abc);
4940        });
4941    }
4942
4943    #[test]
4944    fn python_augmented_assignments_count() {
4945        // Each augmented op counts once.
4946        check_metrics::<PythonParser>("x = 0\nx += 1\nx -= 1\nx *= 2\n", "foo.py", |metric| {
4947            // 1 plain `=` + 3 augmented = 4 assignments.
4948            assert_eq!(metric.abc.assignments_sum(), 4.0);
4949            insta::assert_json_snapshot!(metric.abc);
4950        });
4951    }
4952
4953    #[test]
4954    fn python_walrus_counts_as_assignment() {
4955        // `x := 10` is a `NamedExpression` (PEP 572). It binds a value
4956        // → one assignment under Fitzpatrick's rule.
4957        check_metrics::<PythonParser>("if (n := 10) > 5:\n    pass\n", "foo.py", |metric| {
4958            // 1 assignment (walrus) + 1 condition (`> 5` is a
4959            // ComparisonOperator).
4960            assert_eq!(metric.abc.assignments_sum(), 1.0);
4961            assert_eq!(metric.abc.conditions_sum(), 1.0);
4962            insta::assert_json_snapshot!(metric.abc);
4963        });
4964    }
4965
4966    #[test]
4967    fn python_calls_are_branches() {
4968        // `foo()`, `bar()`, `Baz()` (constructor) all parse as `Call`
4969        // → three branches.
4970        check_metrics::<PythonParser>(
4971            "def foo():\n    pass\ndef bar():\n    pass\nclass Baz:\n    pass\nfoo()\nbar()\nBaz()\n",
4972            "foo.py",
4973            |metric| {
4974                assert_eq!(metric.abc.branches_sum(), 3.0);
4975                assert_eq!(metric.abc.assignments_sum(), 0.0);
4976                insta::assert_json_snapshot!(metric.abc);
4977            },
4978        );
4979    }
4980
4981    #[test]
4982    fn python_comparisons_count_conditions() {
4983        // `x > 0`, `x == y`, `x is None` are each a single
4984        // `ComparisonOperator` node — three conditions.
4985        check_metrics::<PythonParser>(
4986            "def f(x, y):\n    a = x > 0\n    b = x == y\n    c = x is None\n",
4987            "foo.py",
4988            |metric| {
4989                assert_eq!(metric.abc.conditions_sum(), 3.0);
4990                // 3 plain assignments; the comparisons are operands.
4991                assert_eq!(metric.abc.assignments_sum(), 3.0);
4992                insta::assert_json_snapshot!(metric.abc);
4993            },
4994        );
4995    }
4996
4997    #[test]
4998    fn python_chained_comparison_counts_once() {
4999        // tree-sitter-python collapses `0 < x < 10` into a single
5000        // `ComparisonOperator` — one condition, not two.
5001        check_metrics::<PythonParser>("def f(x):\n    return 0 < x < 10\n", "foo.py", |metric| {
5002            assert_eq!(metric.abc.conditions_sum(), 1.0);
5003            insta::assert_json_snapshot!(metric.abc);
5004        });
5005    }
5006
5007    #[test]
5008    fn python_boolean_operators_count_conditions() {
5009        // `and` / `or` are each a `BooleanOperator` node → one condition
5010        // per logical-binop instance.
5011        check_metrics::<PythonParser>(
5012            "def f(a, b, c):\n    if a and b or c:\n        pass\n",
5013            "foo.py",
5014            |metric| {
5015                // `a and b or c` parses as `BooleanOperator(or,
5016                // BooleanOperator(and, a, b), c)` → 2 BooleanOperator
5017                // nodes → 2 conditions.
5018                assert_eq!(metric.abc.conditions_sum(), 2.0);
5019                insta::assert_json_snapshot!(metric.abc);
5020            },
5021        );
5022    }
5023
5024    /// Python's unary `not` operator parses as `NotOperator` and now
5025    /// counts as one condition, matching Java's `!x` rule. Closes
5026    /// the parity gap noted in #214: without this, `if not flag:`
5027    /// reported 0 conditions while the Java equivalent reports 1.
5028    #[test]
5029    fn python_unary_not_counts_as_condition() {
5030        check_metrics::<PythonParser>(
5031            "def f(flag):\n    if not flag:\n        return 1\n    return 0\n",
5032            "foo.py",
5033            |metric| {
5034                // One `NotOperator` -> 1 condition. The `if` itself
5035                // is structural and doesn't add an Abc condition.
5036                assert_eq!(metric.abc.conditions_sum(), 1.0);
5037                insta::assert_json_snapshot!(metric.abc);
5038            },
5039        );
5040    }
5041
5042    /// `return not flag` — the unary `not` is the entire return
5043    /// expression. Without `NotOperator` counted, this reports zero
5044    /// conditions; with it, one. Java's `return !flag;` is one.
5045    #[test]
5046    fn python_return_unary_not_counts() {
5047        check_metrics::<PythonParser>("def f(flag):\n    return not flag\n", "foo.py", |metric| {
5048            assert_eq!(metric.abc.conditions_sum(), 1.0);
5049            insta::assert_json_snapshot!(metric.abc);
5050        });
5051    }
5052
5053    /// `foo(not ready, value)` — the unary `not` inside an argument
5054    /// list still contributes. Mirrors Java's
5055    /// `java_count_unary_conditions` walk over argument lists.
5056    #[test]
5057    fn python_unary_not_in_argument_list_counts() {
5058        check_metrics::<PythonParser>(
5059            "def f(ready, value):\n    log(not ready, value)\n",
5060            "foo.py",
5061            |metric| {
5062                // 1 Call (log) -> 1 branch.
5063                // 1 NotOperator (not ready) -> 1 condition.
5064                assert_eq!(metric.abc.branches_sum(), 1.0);
5065                assert_eq!(metric.abc.conditions_sum(), 1.0);
5066                insta::assert_json_snapshot!(metric.abc);
5067            },
5068        );
5069    }
5070
5071    /// Nested `not` + comparison counts each unique node once.
5072    /// `not (x > 0)` parses as `NotOperator(ParenthesizedExpression(
5073    /// ComparisonOperator))`; both the unary and the comparison
5074    /// contribute one condition (mirrors Java's `!(x > 0)` = 2
5075    /// conditions).
5076    #[test]
5077    fn python_unary_not_with_comparison_counts_each_once() {
5078        check_metrics::<PythonParser>(
5079            "def f(x):\n    if not (x > 0):\n        return 1\n    return 0\n",
5080            "foo.py",
5081            |metric| {
5082                // NotOperator (1) + ComparisonOperator (1) = 2.
5083                assert_eq!(metric.abc.conditions_sum(), 2.0);
5084                insta::assert_json_snapshot!(metric.abc);
5085            },
5086        );
5087    }
5088
5089    /// `not x and y` parses as `BooleanOperator(NotOperator(x), and,
5090    /// y)`. The BooleanOperator counts (and/or = 1 condition); the
5091    /// inner NotOperator also counts. Total: 2.
5092    #[test]
5093    fn python_unary_not_with_boolean_combinator_counts_each() {
5094        check_metrics::<PythonParser>(
5095            "def f(x, y):\n    if not x and y:\n        return 1\n    return 0\n",
5096            "foo.py",
5097            |metric| {
5098                // BooleanOperator (1) + NotOperator (1) = 2.
5099                assert_eq!(metric.abc.conditions_sum(), 2.0);
5100                insta::assert_json_snapshot!(metric.abc);
5101            },
5102        );
5103    }
5104
5105    #[test]
5106    fn python_control_flow_arms_count_conditions() {
5107        // `elif`, `else`, `except`, `finally`, `case` each contribute
5108        // one condition. The comparisons in the `if`/`elif`/`while`
5109        // headers contribute their own ComparisonOperator counts.
5110        check_metrics::<PythonParser>(
5111            "def f(x):\n    if x > 0:\n        a = 1\n    elif x > -1:\n        a = 2\n    else:\n        a = 3\n",
5112            "foo.py",
5113            |metric| {
5114                // 2 ComparisonOperator (`x > 0`, `x > -1`) + 1
5115                // ElifClause + 1 ElseClause = 4 conditions.
5116                assert_eq!(metric.abc.conditions_sum(), 4.0);
5117                insta::assert_json_snapshot!(metric.abc);
5118            },
5119        );
5120    }
5121
5122    #[test]
5123    fn python_ternary_counts_as_condition() {
5124        // `a if c else b` is `ConditionalExpression` → 1 condition.
5125        // `c > 0` adds 1 more (ComparisonOperator).
5126        check_metrics::<PythonParser>(
5127            "def f(c):\n    return 1 if c > 0 else 0\n",
5128            "foo.py",
5129            |metric| {
5130                assert_eq!(metric.abc.conditions_sum(), 2.0);
5131                insta::assert_json_snapshot!(metric.abc);
5132            },
5133        );
5134    }
5135
5136    #[test]
5137    fn python_try_except_finally_count_conditions() {
5138        // ExceptClause + FinallyClause → 2 conditions.
5139        check_metrics::<PythonParser>(
5140            "def f():\n    try:\n        pass\n    except ValueError:\n        pass\n    finally:\n        pass\n",
5141            "foo.py",
5142            |metric| {
5143                assert_eq!(metric.abc.conditions_sum(), 2.0);
5144                insta::assert_json_snapshot!(metric.abc);
5145            },
5146        );
5147    }
5148
5149    #[test]
5150    fn python_match_case_counts_conditions() {
5151        // Each non-wildcard `CaseClause` → 1 condition. The bare
5152        // `case _:` arm is the language-neutral `default:` equivalent
5153        // and is excluded (matches Rust's bare-`_` MatchArm filter and
5154        // Java/C#'s `default:` rule). Source has `case 1:` (counts) +
5155        // `case _:` (excluded) → C = 1.
5156        check_metrics::<PythonParser>(
5157            "def f(x):\n    match x:\n        case 1:\n            pass\n        case _:\n            pass\n",
5158            "foo.py",
5159            |metric| {
5160                assert_eq!(metric.abc.conditions_sum(), 1.0);
5161                insta::assert_json_snapshot!(metric.abc);
5162            },
5163        );
5164    }
5165
5166    #[test]
5167    fn python_match_case_guarded_wildcard_counts() {
5168        // `case _ if g:` is NOT a bare wildcard — the guard
5169        // contributes real branching, so the arm counts as a
5170        // condition. Mirrors Rust's `_ if g => ...` behavior.
5171        // Source: `case 1:` (counts) + `case _ if x > 0:` (guarded
5172        // wildcard, counts) + `case _:` (bare wildcard, excluded) →
5173        // C from CaseClause = 2; the guard's `x > 0` adds one
5174        // ComparisonOperator → total C = 3.
5175        check_metrics::<PythonParser>(
5176            "def f(x):\n    match x:\n        case 1:\n            pass\n        case _ if x > 0:\n            pass\n        case _:\n            pass\n",
5177            "foo.py",
5178            |metric| {
5179                assert_eq!(metric.abc.conditions_sum(), 3.0);
5180                insta::assert_json_snapshot!(metric.abc);
5181            },
5182        );
5183    }
5184
5185    #[test]
5186    fn python_complex_function_abc() {
5187        // Mixed-shape regression: assignments, calls, conditions all in
5188        // a single function.
5189        check_metrics::<PythonParser>(
5190            "def f(items, threshold):\n\
5191             \x20   result = []\n\
5192             \x20   for item in items:\n\
5193             \x20       if item > threshold:\n\
5194             \x20           result.append(item)\n\
5195             \x20   return result\n",
5196            "foo.py",
5197            |metric| {
5198                // assignments: `result = []` → 1
5199                // branches: `result.append(item)` is one call → 1
5200                // conditions: `item > threshold` is one
5201                // ComparisonOperator → 1
5202                assert_eq!(metric.abc.assignments_sum(), 1.0);
5203                assert_eq!(metric.abc.branches_sum(), 1.0);
5204                assert_eq!(metric.abc.conditions_sum(), 1.0);
5205                insta::assert_json_snapshot!(metric.abc);
5206            },
5207        );
5208    }
5209
5210    #[test]
5211    fn rust_empty_unit_zero() {
5212        // No code at all → A=B=C=0. Establishes the trait is wired up
5213        // and the per-language compute is reachable.
5214        check_metrics::<RustParser>("", "empty.rs", |metric| {
5215            assert_eq!(metric.abc.assignments_sum(), 0.0);
5216            assert_eq!(metric.abc.branches_sum(), 0.0);
5217            assert_eq!(metric.abc.conditions_sum(), 0.0);
5218            insta::assert_json_snapshot!(metric.abc);
5219        });
5220    }
5221
5222    #[test]
5223    fn rust_assignments_count_outside_let() {
5224        // `let x = 0` is a declaration — its `=` is NOT a Fitzpatrick
5225        // assignment (mirrors Java's local-variable-declaration rule).
5226        // `x = 5` and `x = 7` are plain `=` assignments → 2. `x += 2`
5227        // is a compound assignment → 1. Total A = 3.
5228        check_metrics::<RustParser>(
5229            "fn f() { let mut x = 0; x = 5; x += 2; x = 7; }",
5230            "foo.rs",
5231            |metric| {
5232                assert_eq!(metric.abc.assignments_sum(), 3.0);
5233                assert_eq!(metric.abc.branches_sum(), 0.0);
5234                assert_eq!(metric.abc.conditions_sum(), 0.0);
5235                insta::assert_json_snapshot!(metric.abc);
5236            },
5237        );
5238    }
5239
5240    #[test]
5241    fn rust_calls_are_branches() {
5242        // Free function call + method call (parses as call_expression
5243        // with a field_expression callee) + associated-fn call. All
5244        // three are `call_expression` → B = 3. Macro invocations like
5245        // `println!` parse as `macro_invocation`, NOT `call_expression`,
5246        // so they are not branches.
5247        check_metrics::<RustParser>(
5248            "fn f() { g(); 1.to_string(); String::new(); }\nfn g() {}\n",
5249            "foo.rs",
5250            |metric| {
5251                assert_eq!(metric.abc.branches_sum(), 3.0);
5252                assert_eq!(metric.abc.assignments_sum(), 0.0);
5253                assert_eq!(metric.abc.conditions_sum(), 0.0);
5254                insta::assert_json_snapshot!(metric.abc);
5255            },
5256        );
5257    }
5258
5259    #[test]
5260    fn rust_try_operator_is_branch() {
5261        // `?` parses as `try_expression` and counts as one branch
5262        // (short-circuit return on Err / None). The `Err(())` call
5263        // contributes one branch in addition (call_expression).
5264        check_metrics::<RustParser>(
5265            "fn f() -> Result<i32, ()> { let r: Result<i32, ()> = Err(()); Ok(r?) }",
5266            "foo.rs",
5267            |metric| {
5268                // Err(()) + Ok(...) + r? → 2 calls + 1 try = 3 branches.
5269                assert_eq!(metric.abc.branches_sum(), 3.0);
5270                insta::assert_json_snapshot!(metric.abc);
5271            },
5272        );
5273    }
5274
5275    #[test]
5276    fn rust_comparisons_count_conditions() {
5277        // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six
5278        // comparisons → C = 6.
5279        check_metrics::<RustParser>(
5280            "fn f(a: i32, b: i32) -> bool { a < b || a > b || a <= b || a >= b || a == b || a != b }",
5281            "foo.rs",
5282            |metric| {
5283                assert_eq!(metric.abc.conditions_sum(), 6.0);
5284                insta::assert_json_snapshot!(metric.abc);
5285            },
5286        );
5287    }
5288
5289    #[test]
5290    fn rust_generic_brackets_not_conditions() {
5291        // `<` / `>` in `Vec<i32>` are TypeArguments delimiters, not
5292        // comparison operators. The parent-check in the LT/GT arms
5293        // must filter them out. Expected C = 0.
5294        check_metrics::<RustParser>(
5295            "fn f() -> Vec<i32> { Vec::<i32>::new() }",
5296            "foo.rs",
5297            |metric| {
5298                assert_eq!(metric.abc.conditions_sum(), 0.0);
5299                insta::assert_json_snapshot!(metric.abc);
5300            },
5301        );
5302    }
5303
5304    #[test]
5305    fn rust_if_let_counts_as_condition() {
5306        // `if let Some(v) = opt { ... }` introduces a `let_condition`
5307        // → 1 condition. The `if` keyword itself does not add another
5308        // count — Fitzpatrick counts conditions, not branch keywords.
5309        check_metrics::<RustParser>(
5310            "fn f(opt: Option<i32>) { if let Some(_v) = opt { } }",
5311            "foo.rs",
5312            |metric| {
5313                assert_eq!(metric.abc.conditions_sum(), 1.0);
5314                insta::assert_json_snapshot!(metric.abc);
5315            },
5316        );
5317    }
5318
5319    #[test]
5320    fn rust_while_let_counts_as_condition() {
5321        // `while let Some(y) = it.next() { ... }` is also a
5322        // `let_condition` (the `while` form). One condition; the
5323        // `it.next()` call adds one branch.
5324        check_metrics::<RustParser>(
5325            "fn f(mut it: std::vec::IntoIter<i32>) { while let Some(_y) = it.next() { } }",
5326            "foo.rs",
5327            |metric| {
5328                assert_eq!(metric.abc.conditions_sum(), 1.0);
5329                assert_eq!(metric.abc.branches_sum(), 1.0);
5330                insta::assert_json_snapshot!(metric.abc);
5331            },
5332        );
5333    }
5334
5335    #[test]
5336    fn rust_match_arms_count_conditions_wildcard_excluded() {
5337        // Three arms: `0 => 1`, `n if n > 0 => n`, `_ => -1`. The
5338        // bare wildcard is the `default:` equivalent and is skipped.
5339        // The guarded arm has a `n if n > 0` pattern (more than one
5340        // child in the match_pattern) and still counts. Two non-wildcard
5341        // arms → C = 2 from MatchArm. Plus the comparison `n > 0`
5342        // adds one more → C = 3.
5343        check_metrics::<RustParser>(
5344            "fn f(x: i32) -> i32 { match x { 0 => 1, n if n > 0 => n, _ => -1, } }",
5345            "foo.rs",
5346            |metric| {
5347                assert_eq!(metric.abc.conditions_sum(), 3.0);
5348                insta::assert_json_snapshot!(metric.abc);
5349            },
5350        );
5351    }
5352
5353    #[test]
5354    fn rust_else_counts_as_condition() {
5355        // `if a > b { ... } else { ... }` → `a > b` is one condition,
5356        // `else` is one condition → C = 2.
5357        check_metrics::<RustParser>(
5358            "fn f(a: i32, b: i32) -> i32 { if a > b { a } else { b } }",
5359            "foo.rs",
5360            |metric| {
5361                assert_eq!(metric.abc.conditions_sum(), 2.0);
5362                insta::assert_json_snapshot!(metric.abc);
5363            },
5364        );
5365    }
5366
5367    #[test]
5368    fn rust_complex_function_abc() {
5369        // Mixed-shape regression: assignments, calls, conditions, `?`,
5370        // `if let`, `match` in one body. Verified by hand:
5371        // - assignments: `x = 5`, `x += 2` → A = 2 (the `let` initialisers
5372        //   are not assignments).
5373        // - branches: `xs.iter()`, `.next()`, `Err(())`, `r?` → B = 4
5374        //   (3 calls + 1 try).
5375        // - conditions: `if let Some(v) = opt` → 1, `match x` arms
5376        //   `0`, `n if n>0` (wildcard excluded) → 2, `n > 0` → 1.
5377        //   Total C = 4.
5378        check_metrics::<RustParser>(
5379            "fn f(opt: Option<i32>, xs: Vec<i32>) -> Result<i32, ()> {\n\
5380             \x20   let mut x = 0;\n\
5381             \x20   x = 5;\n\
5382             \x20   x += 2;\n\
5383             \x20   if let Some(_v) = opt { }\n\
5384             \x20   let _ = xs.iter().next();\n\
5385             \x20   let r: Result<i32, ()> = Err(());\n\
5386             \x20   let _v = r?;\n\
5387             \x20   Ok(match x {\n\
5388             \x20       0 => 1,\n\
5389             \x20       n if n > 0 => n,\n\
5390             \x20       _ => -1,\n\
5391             \x20   })\n\
5392             }\n",
5393            "foo.rs",
5394            |metric| {
5395                assert_eq!(metric.abc.assignments_sum(), 2.0);
5396                // calls: xs.iter(), .next(), Err(()), Ok(...) → 4 calls
5397                // plus 1 try (`r?`) → 5 branches.
5398                assert_eq!(metric.abc.branches_sum(), 5.0);
5399                // 1 let_condition + 2 non-wildcard match_arms + 1
5400                // comparison (`n > 0`) → 4.
5401                assert_eq!(metric.abc.conditions_sum(), 4.0);
5402                insta::assert_json_snapshot!(metric.abc);
5403            },
5404        );
5405    }
5406
5407    // ----- Go -----
5408
5409    #[test]
5410    fn go_empty_unit_zero() {
5411        // Package declaration only — no Fitzpatrick events. Confirms the
5412        // GoCode Abc trait is wired up and emits zero counts.
5413        check_metrics::<GoParser>("package main\n", "empty.go", |metric| {
5414            assert_eq!(metric.abc.assignments_sum(), 0.0);
5415            assert_eq!(metric.abc.branches_sum(), 0.0);
5416            assert_eq!(metric.abc.conditions_sum(), 0.0);
5417            insta::assert_json_snapshot!(metric.abc);
5418        });
5419    }
5420
5421    #[test]
5422    fn go_assignments_count_plain_compound_short_var_and_incdec() {
5423        // `x := 0` (short var decl), `x = 5` and `x = 7` (plain `=`),
5424        // `x += 2` (compound), `x++` (inc) → A = 5. `var y = 1` is a
5425        // declaration — its `=` is not counted (matches the Rust/Java
5426        // rule for `let` / `int y = 1`).
5427        check_metrics::<GoParser>(
5428            "package main\nfunc f() { var y = 1; _ = y; x := 0; x = 5; x += 2; x = 7; x++ }\n",
5429            "foo.go",
5430            |metric| {
5431                // `_ = y` is itself an assignment_statement → +1.
5432                // x:= + x=5 + x+=2 + x=7 + x++ + _=y → 6
5433                assert_eq!(metric.abc.assignments_sum(), 6.0);
5434                assert_eq!(metric.abc.branches_sum(), 0.0);
5435                assert_eq!(metric.abc.conditions_sum(), 0.0);
5436                insta::assert_json_snapshot!(metric.abc);
5437            },
5438        );
5439    }
5440
5441    #[test]
5442    fn go_calls_are_branches() {
5443        // Three calls: free function `g()`, method call `r.Inc()`, and
5444        // builtin call `len(s)`. All parse as `call_expression` → B = 3.
5445        // Composite literal `Foo{}` is NOT a call.
5446        check_metrics::<GoParser>(
5447            "package main\n\
5448             type R struct{}\n\
5449             func (r R) Inc() {}\n\
5450             func g() {}\n\
5451             func f(s string) { g(); var r R = R{}; r.Inc(); _ = len(s) }\n",
5452            "foo.go",
5453            |metric| {
5454                assert_eq!(metric.abc.branches_sum(), 3.0);
5455                insta::assert_json_snapshot!(metric.abc);
5456            },
5457        );
5458    }
5459
5460    #[test]
5461    fn go_comparisons_count_conditions() {
5462        // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six
5463        // comparisons → C = 6.
5464        check_metrics::<GoParser>(
5465            "package main\nfunc f(a, b int) bool { return a < b || a > b || a <= b || a >= b || a == b || a != b }\n",
5466            "foo.go",
5467            |metric| {
5468                assert_eq!(metric.abc.conditions_sum(), 6.0);
5469                insta::assert_json_snapshot!(metric.abc);
5470            },
5471        );
5472    }
5473
5474    #[test]
5475    fn go_generic_brackets_not_conditions() {
5476        // Generic instantiation `Min[int](a, b)` puts `int` inside
5477        // `TypeArguments`, not `BinaryExpression`. The parent guard on
5478        // `<` / `>` must not count these. Expected C = 0; B = 1 (one call).
5479        check_metrics::<GoParser>(
5480            "package main\nfunc Min[T int | float64](a, b T) T { return a }\nfunc f() { _ = Min[int](1, 2) }\n",
5481            "foo.go",
5482            |metric| {
5483                assert_eq!(metric.abc.conditions_sum(), 0.0);
5484                assert_eq!(metric.abc.branches_sum(), 1.0);
5485                insta::assert_json_snapshot!(metric.abc);
5486            },
5487        );
5488    }
5489
5490    #[test]
5491    fn go_switch_arms_count_conditions_default_excluded() {
5492        // Four arms: `case 1:`, `case 2:`, `case 3:`, `default:`. The
5493        // bare `default` is the C/Java `default:` equivalent and is
5494        // excluded — 3 conditions from ExpressionCase. The switch
5495        // expression `x` is bare (no comparison), so no extra
5496        // condition from `==`-style operators.
5497        check_metrics::<GoParser>(
5498            "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",
5499            "foo.go",
5500            |metric| {
5501                assert_eq!(metric.abc.conditions_sum(), 3.0);
5502                insta::assert_json_snapshot!(metric.abc);
5503            },
5504        );
5505    }
5506
5507    #[test]
5508    fn go_type_switch_arms_count_conditions() {
5509        // Type switch: `case int:`, `case string:`, `default:`. Two
5510        // non-default type-case arms → C = 2.
5511        check_metrics::<GoParser>(
5512            "package main\nfunc f(v interface{}) { switch v.(type) { case int: return; case string: return; default: return } }\n",
5513            "foo.go",
5514            |metric| {
5515                assert_eq!(metric.abc.conditions_sum(), 2.0);
5516                insta::assert_json_snapshot!(metric.abc);
5517            },
5518        );
5519    }
5520
5521    #[test]
5522    fn go_select_arms_count_conditions() {
5523        // `select { case <-ch: ...; case ch <- 1: ...; default: ... }`.
5524        // Two non-default communication cases → C = 2.
5525        check_metrics::<GoParser>(
5526            "package main\nfunc f(ch chan int) { select { case <-ch: return; case ch <- 1: return; default: return } }\n",
5527            "foo.go",
5528            |metric| {
5529                assert_eq!(metric.abc.conditions_sum(), 2.0);
5530                insta::assert_json_snapshot!(metric.abc);
5531            },
5532        );
5533    }
5534
5535    #[test]
5536    fn go_else_counts_as_condition() {
5537        // `if a > b { ... } else { ... }` → `a > b` is one condition,
5538        // `else` is one condition → C = 2.
5539        check_metrics::<GoParser>(
5540            "package main\nfunc f(a, b int) int { if a > b { return a } else { return b } }\n",
5541            "foo.go",
5542            |metric| {
5543                assert_eq!(metric.abc.conditions_sum(), 2.0);
5544                insta::assert_json_snapshot!(metric.abc);
5545            },
5546        );
5547    }
5548
5549    #[test]
5550    fn go_complex_function_abc() {
5551        // Mixed shape, verified by hand:
5552        // - Assignments: `_ = x` (after `var`), `n := 0`,
5553        //   `n = n + 1`, `n += 2`, `n++`, `_ = len(s)` → A = 6.
5554        //   `var x = 10` is a declaration, not counted. Every `_ = ...`
5555        //   IS counted as an assignment_statement.
5556        // - Branches: `len(s)` → B = 1.
5557        // - Conditions: `n < 10` → 1, `else` → 1, switch arms `case 0:`
5558        //   and `case 1:` (default excluded) → 2 → total C = 4.
5559        check_metrics::<GoParser>(
5560            "package main\nfunc f(s string) int {\n\
5561             \x20   var x = 10\n\
5562             \x20   _ = x\n\
5563             \x20   n := 0\n\
5564             \x20   if n < 10 { n = n + 1 } else { n += 2 }\n\
5565             \x20   n++\n\
5566             \x20   _ = len(s)\n\
5567             \x20   switch n {\n\
5568             \x20   case 0: return 0\n\
5569             \x20   case 1: return 1\n\
5570             \x20   default: return n\n\
5571             \x20   }\n\
5572             }\n",
5573            "foo.go",
5574            |metric| {
5575                assert_eq!(metric.abc.assignments_sum(), 6.0);
5576                assert_eq!(metric.abc.branches_sum(), 1.0);
5577                assert_eq!(metric.abc.conditions_sum(), 4.0);
5578                insta::assert_json_snapshot!(metric.abc);
5579            },
5580        );
5581    }
5582
5583    // ----- Elixir -----
5584
5585    // No top-level Calls and no operators → all three vectors are
5586    // zero. Uses a bare expression rather than a `defmodule` wrapper
5587    // (which would itself be a Call → 1 branch). Confirms the
5588    // ElixirCode Abc trait is wired up and the metric emits.
5589    #[test]
5590    fn elixir_empty_unit_zero() {
5591        check_metrics::<ElixirParser>(":ok\n", "foo.ex", |metric| {
5592            assert_eq!(metric.abc.assignments_sum(), 0.0);
5593            assert_eq!(metric.abc.branches_sum(), 0.0);
5594            assert_eq!(metric.abc.conditions_sum(), 0.0);
5595            insta::assert_json_snapshot!(metric.abc);
5596        });
5597    }
5598
5599    // An empty `defmodule Foo do ... end` is itself ONE `Call` →
5600    // Documents that module-/function-defining macros (`defmodule`,
5601    // `def`, `defp`, `defmacro`, `defmacrop`) and declarative
5602    // directives (`alias`, `import`, `require`, `use`) are NOT
5603    // runtime dispatch and therefore do NOT inflate `branches`,
5604    // matching Cognitive's treatment.
5605    #[test]
5606    fn elixir_defmodule_is_zero_branches() {
5607        check_metrics::<ElixirParser>("defmodule Foo do\nend\n", "foo.ex", |metric| {
5608            assert_eq!(metric.abc.branches_sum(), 0.0);
5609            assert_eq!(metric.abc.assignments_sum(), 0.0);
5610            assert_eq!(metric.abc.conditions_sum(), 0.0);
5611            insta::assert_json_snapshot!(metric.abc);
5612        });
5613    }
5614
5615    // Pattern-match `=` counts as an assignment. Two bindings → A = 2.
5616    // `defmodule` and `def` are declarative-Call wrappers and are
5617    // filtered out of branches; the assertion focuses on assignments
5618    // so we only pin that vector.
5619    #[test]
5620    fn elixir_pattern_match_is_assignment() {
5621        check_metrics::<ElixirParser>(
5622            "defmodule Foo do\n  def f do\n    x = 1\n    y = x + 1\n    y\n  end\nend\n",
5623            "foo.ex",
5624            |metric| {
5625                assert_eq!(metric.abc.assignments_sum(), 2.0);
5626                insta::assert_json_snapshot!(metric.abc);
5627            },
5628        );
5629    }
5630
5631    // `|>` pipeline operator: each `|>` token contributes one branch.
5632    // Two `|>` ops → +2 from the pipe operator itself. Each pipeline
5633    // step also dispatches a Call (`String.upcase(...)`,
5634    // `String.trim(...)`) — these are wrapped inside the outer
5635    // pipeline Call tree, contributing additional Call branches.
5636    // The headline assertion confirms (a) `|>` is detected and (b)
5637    // pipeline steps are not silently dropped.
5638    #[test]
5639    fn elixir_pipeline_each_step_is_branch() {
5640        check_metrics::<ElixirParser>(
5641            "defmodule Foo do\n  def normalize(s) do\n    s |> String.trim() |> String.upcase()\n  end\nend\n",
5642            "foo.ex",
5643            |metric| {
5644                // Pipeline yields 2 `|>` branches plus Calls for
5645                // String.trim, String.upcase, and the outer pipeline
5646                // (which surfaces as a Call wrapping the binary
5647                // operator). `def` and `defmodule` are declarative
5648                // and excluded. Empirical total: B = 5.
5649                assert_eq!(metric.abc.branches_sum(), 5.0);
5650                assert_eq!(metric.abc.assignments_sum(), 0.0);
5651                insta::assert_json_snapshot!(metric.abc);
5652            },
5653        );
5654    }
5655
5656    // Comparison operators all count as conditions. Six comparisons
5657    // (`==`, `!=`, `<`, `>`, `<=`, `>=`) → C = 6.
5658    #[test]
5659    fn elixir_comparisons_are_conditions() {
5660        check_metrics::<ElixirParser>(
5661            "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",
5662            "foo.ex",
5663            |metric| {
5664                assert_eq!(metric.abc.conditions_sum(), 6.0);
5665                insta::assert_json_snapshot!(metric.abc);
5666            },
5667        );
5668    }
5669
5670    // Strict-equality operators `===` / `!==` count as conditions too.
5671    #[test]
5672    fn elixir_strict_equality_is_condition() {
5673        check_metrics::<ElixirParser>(
5674            "defmodule Foo do\n  def f(a, b) do\n    a === b or a !== b\n  end\nend\n",
5675            "foo.ex",
5676            |metric| {
5677                assert_eq!(metric.abc.conditions_sum(), 2.0);
5678                insta::assert_json_snapshot!(metric.abc);
5679            },
5680        );
5681    }
5682
5683    // Guard `when` clause counts as a condition. One `when` → +1.
5684    // `def f(x) when x > 0` also has `>` → +1, totalling 2.
5685    #[test]
5686    fn elixir_guard_when_is_condition() {
5687        check_metrics::<ElixirParser>(
5688            "defmodule Foo do\n  def f(x) when x > 0 do\n    :pos\n  end\nend\n",
5689            "foo.ex",
5690            |metric| {
5691                // when (+1) + > (+1) = 2
5692                assert_eq!(metric.abc.conditions_sum(), 2.0);
5693                insta::assert_json_snapshot!(metric.abc);
5694            },
5695        );
5696    }
5697
5698    // Keyword-shaped Calls (`case`, `cond`, `if`, `with`) each count
5699    // as one condition AND one branch. `case` here adds 1 condition
5700    // (the keyword Call) + 1 branch (the Call itself).
5701    #[test]
5702    fn elixir_case_is_condition_and_branch() {
5703        check_metrics::<ElixirParser>(
5704            "defmodule Foo do\n  def f(x) do\n    case x do\n      1 -> :one\n      _ -> :other\n    end\n  end\nend\n",
5705            "foo.ex",
5706            |metric| {
5707                // conditions: case → 1
5708                assert_eq!(metric.abc.conditions_sum(), 1.0);
5709                insta::assert_json_snapshot!(metric.abc);
5710            },
5711        );
5712    }
5713
5714    // `cond` is structurally identical to `case` for Abc.
5715    #[test]
5716    fn elixir_cond_is_condition() {
5717        check_metrics::<ElixirParser>(
5718            "defmodule Foo do\n  def f(x) do\n    cond do\n      x > 0 -> :pos\n      true -> :other\n    end\n  end\nend\n",
5719            "foo.ex",
5720            |metric| {
5721                // conditions: cond (+1) + > (+1) = 2
5722                assert_eq!(metric.abc.conditions_sum(), 2.0);
5723                insta::assert_json_snapshot!(metric.abc);
5724            },
5725        );
5726    }
5727
5728    // `for` is a comprehension/loop, NOT in the issue's condition
5729    // list. It is still a Call so it contributes one branch, but no
5730    // condition.
5731    #[test]
5732    fn elixir_for_is_branch_not_condition() {
5733        check_metrics::<ElixirParser>(
5734            "defmodule Foo do\n  def f(xs) do\n    for x <- xs, do: x * 2\n  end\nend\n",
5735            "foo.ex",
5736            |metric| {
5737                assert_eq!(metric.abc.conditions_sum(), 0.0);
5738                insta::assert_json_snapshot!(metric.abc);
5739            },
5740        );
5741    }
5742
5743    // Mixed shape, verified by hand: defmodule Call + def Call + if Call
5744    // + Call to side_effect/0 + assignment `x = 1` + comparison `x > 0`.
5745    // - Assignments: `x = 1` → A = 1.
5746    // - Branches: `defmodule` and `def` are declarative and excluded;
5747    //   `if` Call + `side_effect()` Call → 2 Calls, plus 0 `|>` → B = 2.
5748    // - Conditions: `if` keyword → 1, `x > 0` → 1 → C = 2.
5749    #[test]
5750    fn elixir_mixed_abc() {
5751        check_metrics::<ElixirParser>(
5752            "defmodule Foo do\n  def f do\n    x = 1\n    if x > 0 do\n      side_effect()\n    end\n  end\nend\n",
5753            "foo.ex",
5754            |metric| {
5755                assert_eq!(metric.abc.assignments_sum(), 1.0);
5756                assert_eq!(metric.abc.branches_sum(), 2.0);
5757                assert_eq!(metric.abc.conditions_sum(), 2.0);
5758                insta::assert_json_snapshot!(metric.abc);
5759            },
5760        );
5761    }
5762
5763    // ----- C++ -----
5764
5765    #[test]
5766    fn cpp_empty_unit_zero() {
5767        // No code → A=B=C=0. Wires up the trait and exercises the
5768        // per-language compute reachability.
5769        check_metrics::<CppParser>("", "empty.cpp", |metric| {
5770            assert_eq!(metric.abc.assignments_sum(), 0.0);
5771            assert_eq!(metric.abc.branches_sum(), 0.0);
5772            assert_eq!(metric.abc.conditions_sum(), 0.0);
5773            insta::assert_json_snapshot!(metric.abc);
5774        });
5775    }
5776
5777    #[test]
5778    fn cpp_plain_and_compound_assignments_count() {
5779        // `int x = 0` is an `init_declarator` (declaration initialiser)
5780        // and NOT a Fitzpatrick assignment. `x = 5`, `x += 2`, `x = 7`
5781        // all parse as `assignment_expression` → A = 3.
5782        check_metrics::<CppParser>(
5783            "void f() { int x = 0; x = 5; x += 2; x = 7; }",
5784            "foo.cpp",
5785            |metric| {
5786                assert_eq!(metric.abc.assignments_sum(), 3.0);
5787                assert_eq!(metric.abc.branches_sum(), 0.0);
5788                assert_eq!(metric.abc.conditions_sum(), 0.0);
5789                insta::assert_json_snapshot!(metric.abc);
5790            },
5791        );
5792    }
5793
5794    #[test]
5795    fn cpp_increment_and_decrement_count_as_assignment() {
5796        // `x++` / `--x` / prefix and postfix forms each parse as
5797        // `update_expression` and count as 1 assignment per Fitzpatrick.
5798        check_metrics::<CppParser>(
5799            "void f() { int x = 0; x++; --x; ++x; x--; }",
5800            "foo.cpp",
5801            |metric| {
5802                assert_eq!(metric.abc.assignments_sum(), 4.0);
5803                insta::assert_json_snapshot!(metric.abc);
5804            },
5805        );
5806    }
5807
5808    #[test]
5809    fn cpp_calls_are_branches() {
5810        // Free call + member-fn call (parses as `call_expression` with
5811        // a `field_expression` callee) + `new` allocation. All three
5812        // are branches → B = 3.
5813        check_metrics::<CppParser>(
5814            "struct S { void m(); }; void g(); void f() { g(); S s; s.m(); auto* p = new int(5); }",
5815            "foo.cpp",
5816            |metric| {
5817                assert_eq!(metric.abc.branches_sum(), 3.0);
5818                insta::assert_json_snapshot!(metric.abc);
5819            },
5820        );
5821    }
5822
5823    #[test]
5824    fn cpp_comparisons_count_conditions() {
5825        // `<`, `>`, `<=`, `>=`, `==`, `!=`, and the C++20 spaceship
5826        // `<=>` each contribute one condition. Seven comparisons → C = 7.
5827        check_metrics::<CppParser>(
5828            "#include <compare>\n\
5829             bool f(int a, int b) {\n\
5830                 return a < b || a > b || a <= b || a >= b || a == b || a != b || (a <=> b) == 0;\n\
5831             }\n",
5832            "foo.cpp",
5833            |metric| {
5834                // 6 plain comparisons + 1 spaceship + 1 `||` adds = 7? Let's
5835                // pin the exact count by hand:
5836                // `<`, `>`, `<=`, `>=`, `==`, `!=` → 6 from the
5837                // chained `||` expression. `(a <=> b) == 0` adds the
5838                // spaceship → 7, plus its `== 0` adds one more → 8.
5839                // Six `||` short-circuits add → 8 + 6 = 14.
5840                assert_eq!(metric.abc.conditions_sum(), 14.0);
5841                insta::assert_json_snapshot!(metric.abc);
5842            },
5843        );
5844    }
5845
5846    #[test]
5847    fn cpp_short_circuit_ops_count_conditions() {
5848        // `&&` and `||` each count once per occurrence (Fitzpatrick
5849        // rule). Two short-circuits → C = 2 (plus two comparisons → 4).
5850        check_metrics::<CppParser>(
5851            "bool f(int a, int b) { return a == b && a > 0 || b < 0; }",
5852            "foo.cpp",
5853            |metric| {
5854                // == 1, > 1, < 1, && 1, || 1 → 5.
5855                assert_eq!(metric.abc.conditions_sum(), 5.0);
5856                insta::assert_json_snapshot!(metric.abc);
5857            },
5858        );
5859    }
5860
5861    #[test]
5862    fn cpp_generic_brackets_not_conditions() {
5863        // `<` / `>` in `std::vector<int>` are `template_argument_list`
5864        // delimiters, NOT comparison operators. The `binary_expression`
5865        // parent check must filter them out → C = 0.
5866        check_metrics::<CppParser>(
5867            "#include <vector>\nstd::vector<int> f() { return std::vector<int>{}; }",
5868            "foo.cpp",
5869            |metric| {
5870                assert_eq!(metric.abc.conditions_sum(), 0.0);
5871                insta::assert_json_snapshot!(metric.abc);
5872            },
5873        );
5874    }
5875
5876    #[test]
5877    fn cpp_else_and_ternary_count_conditions() {
5878        // `if (cond) ... else ...` + ternary `cond ? a : b`. The
5879        // `if`-keyword is NOT a condition (its condition is the
5880        // comparison inside, which counts separately). `else` adds 1,
5881        // `?` adds 1. Two comparisons (`a > b`, `b < 0`) → 2. Total = 4.
5882        check_metrics::<CppParser>(
5883            "int f(int a, int b) {\n\
5884                 if (a > b) { return a; } else { return b; }\n\
5885                 return (b < 0) ? -b : b;\n\
5886             }\n",
5887            "foo.cpp",
5888            |metric| {
5889                assert_eq!(metric.abc.conditions_sum(), 4.0);
5890                insta::assert_json_snapshot!(metric.abc);
5891            },
5892        );
5893    }
5894
5895    #[test]
5896    fn cpp_switch_cases_count_default_excluded() {
5897        // `case 1`, `case 2` → 2 conditions. `default` is intentionally
5898        // excluded (matches the C-family precedent in Rust / Go / Python
5899        // and Java's omission of `Default` from this rule? — actually
5900        // Java DOES count `Default`. We follow Rust / Go and exclude
5901        // it). C = 2.
5902        check_metrics::<CppParser>(
5903            "void f(int x) {\n\
5904                 switch (x) {\n\
5905                     case 1: break;\n\
5906                     case 2: break;\n\
5907                     default: break;\n\
5908                 }\n\
5909             }\n",
5910            "foo.cpp",
5911            |metric| {
5912                assert_eq!(metric.abc.conditions_sum(), 2.0);
5913                insta::assert_json_snapshot!(metric.abc);
5914            },
5915        );
5916    }
5917
5918    #[test]
5919    fn cpp_try_catch_count_conditions() {
5920        // `try` and `catch` each add one condition (Fitzpatrick's rule;
5921        // Java's impl above counts them too).
5922        check_metrics::<CppParser>(
5923            "void f() { try { } catch (int) { } catch (...) { } }",
5924            "foo.cpp",
5925            |metric| {
5926                // 1 `try` + 2 `catch` arms = 3.
5927                assert_eq!(metric.abc.conditions_sum(), 3.0);
5928                insta::assert_json_snapshot!(metric.abc);
5929            },
5930        );
5931    }
5932
5933    #[test]
5934    fn cpp_complex_function_abc() {
5935        // Mixed-shape regression: assignments, calls, conditions,
5936        // ternary, switch, new. Verified by hand:
5937        // - assignments: `x = 5`, `x += 2`, `x++`, `x = (a > b) ? a : b`,
5938        //   `x = b` → A = 5. (`int x = 0`, `auto y = ...`, `auto* p = ...`
5939        //   are declaration initialisers and don't count.)
5940        // - branches: `f(a, b)` self-call + `new int(5)` → B = 2.
5941        // - conditions: `a == b`, `&&`, `a > 0` → 3 inside the if.
5942        //   `else` (1) + `a > b`, `?` → 2 in the ternary. `a < b`,
5943        //   `||` → 2 in the else-if. `case 1`, `case 2` → 2.
5944        //   default excluded. Total C = 10.
5945        check_metrics::<CppParser>(
5946            "int f(int a, int b) {\n\
5947                 int x = 0;\n\
5948                 x = 5;\n\
5949                 x += 2;\n\
5950                 x++;\n\
5951                 if (a == b && a > 0) {\n\
5952                     x = (a > b) ? a : b;\n\
5953                 } else if (a < b || !x) {\n\
5954                     x = b;\n\
5955                 }\n\
5956                 switch (x) {\n\
5957                     case 1: break;\n\
5958                     case 2: break;\n\
5959                     default: break;\n\
5960                 }\n\
5961                 auto* p = new int(5);\n\
5962                 return f(a, b);\n\
5963             }\n",
5964            "foo.cpp",
5965            |metric| {
5966                assert_eq!(metric.abc.assignments_sum(), 5.0);
5967                assert_eq!(metric.abc.branches_sum(), 2.0);
5968                assert_eq!(metric.abc.conditions_sum(), 10.0);
5969                insta::assert_json_snapshot!(metric.abc);
5970            },
5971        );
5972    }
5973
5974    #[test]
5975    fn javascript_empty_unit_zero() {
5976        // No code → A=B=C=0. Wires up the trait and exercises the
5977        // per-language compute reachability.
5978        check_metrics::<JavascriptParser>("", "empty.js", |metric| {
5979            assert_eq!(metric.abc.assignments_sum(), 0.0);
5980            assert_eq!(metric.abc.branches_sum(), 0.0);
5981            assert_eq!(metric.abc.conditions_sum(), 0.0);
5982            insta::assert_json_snapshot!(metric.abc);
5983        });
5984    }
5985
5986    #[test]
5987    fn javascript_plain_and_compound_assignments_count() {
5988        // `let` / `var` declarations behave like TypeScript: the `Var`
5989        // sentinel is pushed but only `const` suppresses the
5990        // initializer `=`. So `let x = 0` does count as A=+1; only
5991        // `const PI = 3.14` would be elided. Plain `x = 5`, `x += 2`,
5992        // `x = 7` all count → A = 4 total here.
5993        check_metrics::<JavascriptParser>(
5994            "function f() { let x = 0; x = 5; x += 2; x = 7; }",
5995            "foo.js",
5996            |metric| {
5997                assert_eq!(metric.abc.assignments_sum(), 4.0);
5998                assert_eq!(metric.abc.branches_sum(), 0.0);
5999                assert_eq!(metric.abc.conditions_sum(), 0.0);
6000                insta::assert_json_snapshot!(metric.abc);
6001            },
6002        );
6003    }
6004
6005    #[test]
6006    fn javascript_const_initializer_not_assignment() {
6007        // `const PI = 3.14` must NOT count as an assignment — the
6008        // `Const` sentinel suppresses the initializer `=`. `let x = 1`
6009        // and `var y = 2` still count (matches the TS impl: only
6010        // `const` suppresses).
6011        check_metrics::<JavascriptParser>(
6012            "function f() { const PI = 3.14; let x = 1; var y = 2; x = 9; }",
6013            "foo.js",
6014            |metric| {
6015                // `const PI` suppressed; `let x = 1`, `var y = 2`,
6016                // `x = 9` all count → A = 3.
6017                assert_eq!(metric.abc.assignments_sum(), 3.0);
6018                insta::assert_json_snapshot!(metric.abc);
6019            },
6020        );
6021    }
6022
6023    #[test]
6024    fn javascript_increment_and_decrement_count_as_assignment() {
6025        // `x++` (post) and `--x` (pre) both update an lvalue and so
6026        // count as assignments. Combined with the `let x = 0`
6027        // initializer (which counts under the JS/TS sentinel rule —
6028        // only `const` suppresses), A = 3.
6029        check_metrics::<JavascriptParser>(
6030            "function f() { let x = 0; x++; --x; }",
6031            "foo.js",
6032            |metric| {
6033                assert_eq!(metric.abc.assignments_sum(), 3.0);
6034                insta::assert_json_snapshot!(metric.abc);
6035            },
6036        );
6037    }
6038
6039    #[test]
6040    fn javascript_calls_are_branches() {
6041        // `g(1)` is a `call_expression` → B = 1. `new Foo(2)` is a
6042        // `new_expression` → B = 1. Total B = 2.
6043        check_metrics::<JavascriptParser>(
6044            "function f() { g(1); new Foo(2); }",
6045            "foo.js",
6046            |metric| {
6047                assert_eq!(metric.abc.branches_sum(), 2.0);
6048                assert_eq!(metric.abc.conditions_sum(), 0.0);
6049                insta::assert_json_snapshot!(metric.abc);
6050            },
6051        );
6052    }
6053
6054    #[test]
6055    fn javascript_comparisons_count_conditions() {
6056        // `==`, `===`, `!=`, `!==`, `<`, `>`, `<=`, `>=` each count
6057        // once. The `&&` / `||` short-circuit operators are NOT
6058        // counted as conditions in this impl (matches the TS
6059        // precedent — short-circuit ops are folded into the
6060        // surrounding `if` / control-flow arm, not separately).
6061        // Total C = 8.
6062        check_metrics::<JavascriptParser>(
6063            "function f(a, b) { return a == b && a === b && a != b && a !== b && a < b && a > b && a <= b && a >= b; }",
6064            "foo.js",
6065            |metric| {
6066                assert_eq!(metric.abc.conditions_sum(), 8.0);
6067                insta::assert_json_snapshot!(metric.abc);
6068            },
6069        );
6070    }
6071
6072    #[test]
6073    fn javascript_nullish_coalescing_counts_condition() {
6074        // `a ?? b` is one nullish-coalescing operator → C = 1.
6075        check_metrics::<JavascriptParser>(
6076            "function f(a, b) { return a ?? b; }",
6077            "foo.js",
6078            |metric| {
6079                assert_eq!(metric.abc.conditions_sum(), 1.0);
6080                insta::assert_json_snapshot!(metric.abc);
6081            },
6082        );
6083    }
6084
6085    #[test]
6086    fn javascript_else_ternary_case_default_try_catch() {
6087        // `else`, `?` (ternary), `case`, `default`, `try`, `catch`
6088        // all count. With the comparisons:
6089        //   - `a > 0` → 1
6090        //   - `else` opens an else_clause → 1
6091        //   - `?` ternary → 1
6092        //   - `case 1` → 1
6093        //   - `default` → 1
6094        //   - `try` + `catch` → 2
6095        // Total C = 7.
6096        check_metrics::<JavascriptParser>(
6097            "function f(a) { if (a > 0) {} else {} let x = a ? 1 : 2; switch (x) { case 1: break; default: break; } try { } catch (e) { } }",
6098            "foo.js",
6099            |metric| {
6100                assert_eq!(metric.abc.conditions_sum(), 7.0);
6101                insta::assert_json_snapshot!(metric.abc);
6102            },
6103        );
6104    }
6105
6106    #[test]
6107    fn javascript_instanceof_counts_condition() {
6108        // `x instanceof Foo` is a binary expression whose operator is
6109        // the `instanceof` keyword token → C = 1.
6110        check_metrics::<JavascriptParser>(
6111            "function f(x) { return x instanceof Foo; }",
6112            "foo.js",
6113            |metric| {
6114                assert_eq!(metric.abc.conditions_sum(), 1.0);
6115                insta::assert_json_snapshot!(metric.abc);
6116            },
6117        );
6118    }
6119
6120    #[test]
6121    fn javascript_complex_function_abc() {
6122        // Mixed-shape regression. Verified by hand:
6123        // - assignments: `let x = 0` (Var sentinel does not suppress)
6124        //   + `x = 5`, `x += 2`, `x++`, `x = (a>b)?a:b`, `x = b`,
6125        //   `let p = ...` (Var sentinel) → A = 7.
6126        // - branches: `f(a, b)` self-call + `new Bar()` → B = 2.
6127        // - conditions: `a == b`, `a > 0` → 2 inside the if header
6128        //   (`&&` is not counted). `else` (1) + `a > b`, `?` → 2 in
6129        //   the ternary. `a < b` → 1 in the else-if (`||` not
6130        //   counted). `case 1`, `default` → 2 in the switch. Total
6131        //   C = 8.
6132        check_metrics::<JavascriptParser>(
6133            "function f(a, b) {\n\
6134                 let x = 0;\n\
6135                 x = 5;\n\
6136                 x += 2;\n\
6137                 x++;\n\
6138                 if (a == b && a > 0) {\n\
6139                     x = (a > b) ? a : b;\n\
6140                 } else if (a < b || !x) {\n\
6141                     x = b;\n\
6142                 }\n\
6143                 switch (x) {\n\
6144                     case 1: break;\n\
6145                     default: break;\n\
6146                 }\n\
6147                 let p = new Bar();\n\
6148                 return f(a, b);\n\
6149             }\n",
6150            "foo.js",
6151            |metric| {
6152                assert_eq!(metric.abc.assignments_sum(), 7.0);
6153                assert_eq!(metric.abc.branches_sum(), 2.0);
6154                assert_eq!(metric.abc.conditions_sum(), 8.0);
6155                insta::assert_json_snapshot!(metric.abc);
6156            },
6157        );
6158    }
6159
6160    #[test]
6161    fn mozjs_complex_function_abc() {
6162        // Mozjs shares JavaScript's expression / statement vocabulary;
6163        // the `js_abc_compute!` macro expands identical token-level
6164        // rules for both. This test pins parity against the JS impl.
6165        check_metrics::<MozjsParser>(
6166            "function f(a, b) {\n\
6167                 let x = 0;\n\
6168                 x = 5;\n\
6169                 x += 2;\n\
6170                 x++;\n\
6171                 if (a == b && a > 0) {\n\
6172                     x = (a > b) ? a : b;\n\
6173                 } else if (a < b || !x) {\n\
6174                     x = b;\n\
6175                 }\n\
6176                 switch (x) {\n\
6177                     case 1: break;\n\
6178                     default: break;\n\
6179                 }\n\
6180                 let p = new Bar();\n\
6181                 return f(a, b);\n\
6182             }\n",
6183            "foo.js",
6184            |metric| {
6185                assert_eq!(metric.abc.assignments_sum(), 7.0);
6186                assert_eq!(metric.abc.branches_sum(), 2.0);
6187                assert_eq!(metric.abc.conditions_sum(), 8.0);
6188                insta::assert_json_snapshot!(metric.abc);
6189            },
6190        );
6191    }
6192
6193    // ---------- Perl ABC tests ----------
6194
6195    #[test]
6196    fn perl_empty_unit_zero() {
6197        // Empty source produces zero ABC magnitude — pins the trait
6198        // wiring without exercising any compute branch.
6199        check_metrics::<PerlParser>("", "empty.pl", |metric| {
6200            assert_eq!(metric.abc.assignments_sum(), 0.0);
6201            assert_eq!(metric.abc.branches_sum(), 0.0);
6202            assert_eq!(metric.abc.conditions_sum(), 0.0);
6203            insta::assert_json_snapshot!(metric.abc);
6204        });
6205    }
6206
6207    #[test]
6208    fn perl_plain_and_compound_assignments_count() {
6209        // `my $x = 0` parses as a `binary_expression` with an `=`
6210        // token, so the initialiser counts (Perl has no equivalent of
6211        // the JS `const` initialiser-suppression rule). Each
6212        // assignment operator token contributes one assignment:
6213        // `=`, `=`, `+=`, `.=`, `**=` → A = 5. Two of those `=` come
6214        // from the `my $x = 0` initialiser and the later `$x = 5`
6215        // reassignment.
6216        check_metrics::<PerlParser>(
6217            "sub f { my $x = 0; $x = 5; $x += 2; $x .= \"a\"; $x **= 3; }",
6218            "foo.pl",
6219            |metric| {
6220                assert_eq!(metric.abc.assignments_sum(), 5.0);
6221                assert_eq!(metric.abc.branches_sum(), 0.0);
6222                assert_eq!(metric.abc.conditions_sum(), 0.0);
6223                insta::assert_json_snapshot!(metric.abc);
6224            },
6225        );
6226    }
6227
6228    #[test]
6229    fn perl_calls_are_branches() {
6230        // `foo()` parses as `call_expression_with_args_with_brackets`
6231        // wrapping an inner `call_expression_with_bareword(foo)`;
6232        // `bar 1, 2` wraps `bar` likewise under spaced-args; `shift`
6233        // appears as a standalone bareword. The bareword-inside-
6234        // wrapper case must NOT double-count — only the outer wrapper
6235        // contributes a branch. So B = 3 (foo, bar, shift), not 5.
6236        check_metrics::<PerlParser>(
6237            "sub f { foo(); bar 1, 2; my $a = shift; }",
6238            "foo.pl",
6239            |metric| {
6240                // shift's `my $a = shift` initialiser contributes one
6241                // assignment via the `=` token.
6242                assert_eq!(metric.abc.assignments_sum(), 1.0);
6243                assert_eq!(metric.abc.branches_sum(), 3.0);
6244                assert_eq!(metric.abc.conditions_sum(), 0.0);
6245                insta::assert_json_snapshot!(metric.abc);
6246            },
6247        );
6248    }
6249
6250    #[test]
6251    fn perl_method_invocation_counts_as_branch() {
6252        // `$obj->method(...)` parses as `method_invocation`. Any
6253        // arrow-dispatch counts as one branch regardless of how the
6254        // arguments are passed.
6255        check_metrics::<PerlParser>(
6256            "sub f { my $obj = shift; $obj->run($x); $obj->ping; }",
6257            "foo.pl",
6258            |metric| {
6259                // `my $obj = shift` → A=1, B=1 (shift bareword).
6260                // `$obj->run($x)` and `$obj->ping` → 2 more branches.
6261                assert_eq!(metric.abc.assignments_sum(), 1.0);
6262                assert_eq!(metric.abc.branches_sum(), 3.0);
6263                assert_eq!(metric.abc.conditions_sum(), 0.0);
6264                insta::assert_json_snapshot!(metric.abc);
6265            },
6266        );
6267    }
6268
6269    #[test]
6270    fn perl_numeric_and_string_comparisons_count_conditions() {
6271        // Numeric ops `==`, `!=`, `<`, `>`, `<=`, `>=`, `<=>` and
6272        // string ops `eq`, `ne`, `lt`, `gt`, `le`, `ge`, `cmp` each
6273        // fire once per token. The sample below uses one of each →
6274        // C = 14. No assignments, no branches.
6275        check_metrics::<PerlParser>(
6276            "sub f {\n\
6277                 my $r;\n\
6278                 $r = $a == $b;\n\
6279                 $r = $a != $b;\n\
6280                 $r = $a <  $b;\n\
6281                 $r = $a >  $b;\n\
6282                 $r = $a <= $b;\n\
6283                 $r = $a >= $b;\n\
6284                 $r = $a <=> $b;\n\
6285                 $r = $a eq $b;\n\
6286                 $r = $a ne $b;\n\
6287                 $r = $a lt $b;\n\
6288                 $r = $a gt $b;\n\
6289                 $r = $a le $b;\n\
6290                 $r = $a ge $b;\n\
6291                 $r = $a cmp $b;\n\
6292             }",
6293            "foo.pl",
6294            |metric| {
6295                // 15 `=` tokens: one declaration `my $r` (no `=`),
6296                // then 14 `$r = …` plus there's no `=` in `my $r;`.
6297                // Actually: `my $r;` has no `=`; the 14 `$r = …` are
6298                // 14 `=` tokens. So A=14, C=14.
6299                assert_eq!(metric.abc.assignments_sum(), 14.0);
6300                assert_eq!(metric.abc.branches_sum(), 0.0);
6301                assert_eq!(metric.abc.conditions_sum(), 14.0);
6302                insta::assert_json_snapshot!(metric.abc);
6303            },
6304        );
6305    }
6306
6307    #[test]
6308    fn perl_short_circuit_and_ternary_count_conditions() {
6309        // `&&`, `||`, `//`, low-precedence `and`, `or`, `xor`, plus
6310        // ternary `? :` each contribute one condition.
6311        check_metrics::<PerlParser>(
6312            "sub f {\n\
6313                 my $r;\n\
6314                 $r = $a && $b;\n\
6315                 $r = $a || $b;\n\
6316                 $r = $a // $b;\n\
6317                 $r = $a and $b;\n\
6318                 $r = $a or  $b;\n\
6319                 $r = $a xor $b;\n\
6320                 $r = $a ? 1 : 2;\n\
6321             }",
6322            "foo.pl",
6323            |metric| {
6324                // 7 `=` tokens (one per reassignment line).
6325                assert_eq!(metric.abc.assignments_sum(), 7.0);
6326                assert_eq!(metric.abc.branches_sum(), 0.0);
6327                assert_eq!(metric.abc.conditions_sum(), 7.0);
6328                insta::assert_json_snapshot!(metric.abc);
6329            },
6330        );
6331    }
6332
6333    #[test]
6334    fn perl_elsif_and_else_count_conditions() {
6335        // `if (… == …) { … } elsif (… < …) { … } else { … }` →
6336        // 2 comparison tokens (`==`, `<`), plus `elsif_clause` and
6337        // `else_clause` each + 1 → C = 4. Branches: 0 (only
6338        // assignments). Assignments: just the `=` initialisers /
6339        // reassignments — there are 4 here (`$x` init plus three
6340        // `$x = …` reassigns).
6341        check_metrics::<PerlParser>(
6342            "sub f {\n\
6343                 my $x = 0;\n\
6344                 if ($a == $b) {\n\
6345                     $x = 1;\n\
6346                 } elsif ($a < $b) {\n\
6347                     $x = 2;\n\
6348                 } else {\n\
6349                     $x = 3;\n\
6350                 }\n\
6351             }",
6352            "foo.pl",
6353            |metric| {
6354                assert_eq!(metric.abc.assignments_sum(), 4.0);
6355                assert_eq!(metric.abc.branches_sum(), 0.0);
6356                assert_eq!(metric.abc.conditions_sum(), 4.0);
6357                insta::assert_json_snapshot!(metric.abc);
6358            },
6359        );
6360    }
6361
6362    #[test]
6363    fn perl_regex_match_operators_count_conditions() {
6364        // `=~` and `!~` are pattern-match operators; we count both
6365        // as conditions because they evaluate the regex match in a
6366        // boolean context.
6367        check_metrics::<PerlParser>(
6368            "sub f { my $s = shift; my $m = $s =~ /foo/; my $n = $s !~ /bar/; }",
6369            "foo.pl",
6370            |metric| {
6371                // 3 `=` tokens, 0 branches except `shift` bareword.
6372                assert_eq!(metric.abc.assignments_sum(), 3.0);
6373                assert_eq!(metric.abc.branches_sum(), 1.0);
6374                assert_eq!(metric.abc.conditions_sum(), 2.0);
6375                insta::assert_json_snapshot!(metric.abc);
6376            },
6377        );
6378    }
6379
6380    #[test]
6381    fn perl_complex_function_abc() {
6382        // Mixed program exercising every category. Computed
6383        // expected:
6384        //   Assignments: `my $i = 0` (1), `$i++` is a unary
6385        //     increment — Perl's grammar emits `PLUSPLUS` not an `=`
6386        //     operator, so it does NOT count under the operator-
6387        //     token rule. The for-loop's `$i++` is similarly
6388        //     uncounted.
6389        //     Total A: 1 from `my $i = 0`, 1 from `$total += $i`
6390        //     (the `+=` token) → A = 2.
6391        //   Branches: `do_work($i)` → 1; `print "done\n"` is a
6392        //     call_expression_with_spaced_args → 1; `return $total`
6393        //     uses the `return` keyword not a call → 0. B = 2.
6394        //   Conditions: `$i < 10` (`<`) → 1; `$i % 2 == 0` (`==`) →
6395        //     1; `else_clause` → 1. C = 3.
6396        check_metrics::<PerlParser>(
6397            "sub run {\n\
6398                 my $total = 0;\n\
6399                 for (my $i = 0; $i < 10; $i++) {\n\
6400                     if ($i % 2 == 0) {\n\
6401                         do_work($i);\n\
6402                     } else {\n\
6403                         $total += $i;\n\
6404                     }\n\
6405                 }\n\
6406                 print \"done\\n\";\n\
6407                 return $total;\n\
6408             }",
6409            "foo.pl",
6410            |metric| {
6411                // `my $total = 0` is one `=`; `my $i = 0` is another
6412                // `=`; `$total += $i` is one `+=`. Total = 3.
6413                assert_eq!(metric.abc.assignments_sum(), 3.0);
6414                assert_eq!(metric.abc.branches_sum(), 2.0);
6415                assert_eq!(metric.abc.conditions_sum(), 3.0);
6416                insta::assert_json_snapshot!(metric.abc);
6417            },
6418        );
6419    }
6420
6421    // ---------- Lua ABC tests ----------
6422
6423    #[test]
6424    fn lua_empty_unit_zero() {
6425        check_metrics::<LuaParser>("", "empty.lua", |metric| {
6426            assert_eq!(metric.abc.assignments_sum(), 0.0);
6427            assert_eq!(metric.abc.branches_sum(), 0.0);
6428            assert_eq!(metric.abc.conditions_sum(), 0.0);
6429            insta::assert_json_snapshot!(metric.abc);
6430        });
6431    }
6432
6433    #[test]
6434    fn lua_assignments_count_locals_and_plain() {
6435        // `local x = 0` wraps an `assignment_statement` under a
6436        // `variable_declaration`; the inner wrapper still counts.
6437        // Multi-target assignment `a, b = 1, 2` is a single
6438        // `assignment_statement` and contributes 1, NOT 2 — the
6439        // wrapper is the unit of counting (matches the Python rule:
6440        // one `Assignment` node, one assignment).
6441        check_metrics::<LuaParser>(
6442            "function f()\n\
6443                 local x = 0\n\
6444                 x = 1\n\
6445                 local a, b = 1, 2\n\
6446                 a, b = b, a\n\
6447             end",
6448            "foo.lua",
6449            |metric| {
6450                assert_eq!(metric.abc.assignments_sum(), 4.0);
6451                assert_eq!(metric.abc.branches_sum(), 0.0);
6452                assert_eq!(metric.abc.conditions_sum(), 0.0);
6453                insta::assert_json_snapshot!(metric.abc);
6454            },
6455        );
6456    }
6457
6458    #[test]
6459    fn lua_calls_are_branches() {
6460        // `print(x)`, `obj.m(x)`, `obj:m(x)`, `f(g(1))` — every
6461        // call form is a `function_call` node. The nested
6462        // `f(g(1))` counts as 2 branches (one per dispatch).
6463        check_metrics::<LuaParser>(
6464            "function r(x)\n\
6465                 print(x)\n\
6466                 obj.m(x)\n\
6467                 obj:m(x)\n\
6468                 return f(g(1))\n\
6469             end",
6470            "foo.lua",
6471            |metric| {
6472                assert_eq!(metric.abc.assignments_sum(), 0.0);
6473                assert_eq!(metric.abc.branches_sum(), 5.0);
6474                assert_eq!(metric.abc.conditions_sum(), 0.0);
6475                insta::assert_json_snapshot!(metric.abc);
6476            },
6477        );
6478    }
6479
6480    #[test]
6481    fn lua_comparisons_and_boolean_ops_count_conditions() {
6482        // Each comparison / logical operator token contributes one
6483        // condition.
6484        check_metrics::<LuaParser>(
6485            "function f(a, b)\n\
6486                 local r\n\
6487                 r = a == b\n\
6488                 r = a ~= b\n\
6489                 r = a <  b\n\
6490                 r = a >  b\n\
6491                 r = a <= b\n\
6492                 r = a >= b\n\
6493                 r = a and b\n\
6494                 r = a or  b\n\
6495             end",
6496            "foo.lua",
6497            |metric| {
6498                // 8 `r = …` reassignments, plus `local r` (no `=`).
6499                assert_eq!(metric.abc.assignments_sum(), 8.0);
6500                assert_eq!(metric.abc.branches_sum(), 0.0);
6501                assert_eq!(metric.abc.conditions_sum(), 8.0);
6502                insta::assert_json_snapshot!(metric.abc);
6503            },
6504        );
6505    }
6506
6507    #[test]
6508    fn lua_elseif_and_else_count_conditions() {
6509        // Each elseif / else arm of the if contributes one
6510        // condition, mirroring the Python rule.
6511        check_metrics::<LuaParser>(
6512            "function f(x)\n\
6513                 if x > 0 then\n\
6514                     return 1\n\
6515                 elseif x < 0 then\n\
6516                     return -1\n\
6517                 else\n\
6518                     return 0\n\
6519                 end\n\
6520             end",
6521            "foo.lua",
6522            |metric| {
6523                // Comparisons: `>`, `<` → 2; elseif_statement → 1;
6524                // else_statement → 1. C = 4. No branches (no calls).
6525                assert_eq!(metric.abc.assignments_sum(), 0.0);
6526                assert_eq!(metric.abc.branches_sum(), 0.0);
6527                assert_eq!(metric.abc.conditions_sum(), 4.0);
6528                insta::assert_json_snapshot!(metric.abc);
6529            },
6530        );
6531    }
6532
6533    #[test]
6534    fn lua_complex_function_abc() {
6535        // Combines every category to pin the metric.
6536        check_metrics::<LuaParser>(
6537            "function run(n)\n\
6538                 local total = 0\n\
6539                 for i = 1, n do\n\
6540                     if i % 2 == 0 then\n\
6541                         do_work(i)\n\
6542                     else\n\
6543                         total = total + i\n\
6544                     end\n\
6545                 end\n\
6546                 print(\"done\")\n\
6547                 return total\n\
6548             end",
6549            "foo.lua",
6550            |metric| {
6551                // Assignments: `local total = 0` (1), `total = total + i` (1) → 2.
6552                // Branches: `do_work(i)` (1), `print(\"done\")` (1) → 2.
6553                // Conditions: `==` (1), `else_statement` (1) → 2.
6554                assert_eq!(metric.abc.assignments_sum(), 2.0);
6555                assert_eq!(metric.abc.branches_sum(), 2.0);
6556                assert_eq!(metric.abc.conditions_sum(), 2.0);
6557                insta::assert_json_snapshot!(metric.abc);
6558            },
6559        );
6560    }
6561
6562    // ---------- Tcl ABC tests ----------
6563
6564    #[test]
6565    fn tcl_empty_unit_zero() {
6566        check_metrics::<TclParser>("", "empty.tcl", |metric| {
6567            assert_eq!(metric.abc.assignments_sum(), 0.0);
6568            assert_eq!(metric.abc.branches_sum(), 0.0);
6569            assert_eq!(metric.abc.conditions_sum(), 0.0);
6570            insta::assert_json_snapshot!(metric.abc);
6571        });
6572    }
6573
6574    #[test]
6575    fn tcl_set_command_counts_assignment() {
6576        // `set` has its own grammar production; each invocation is
6577        // one assignment.
6578        check_metrics::<TclParser>(
6579            "proc f {} {\n\
6580                 set x 1\n\
6581                 set y 2\n\
6582                 set x [expr {$x + $y}]\n\
6583             }",
6584            "foo.tcl",
6585            |metric| {
6586                // 3 `set` invocations → A=3. The inner `expr` is a
6587                // sub-command (`command_substitution` + `expr_cmd`),
6588                // not a `command` node, so it doesn't add a branch.
6589                assert_eq!(metric.abc.assignments_sum(), 3.0);
6590                assert_eq!(metric.abc.branches_sum(), 0.0);
6591                assert_eq!(metric.abc.conditions_sum(), 0.0);
6592                insta::assert_json_snapshot!(metric.abc);
6593            },
6594        );
6595    }
6596
6597    #[test]
6598    fn tcl_incr_append_lappend_count_assignment() {
6599        // Variable-mutation commands (`incr`, `append`, `lappend`)
6600        // are recognised by name and count as assignments, not
6601        // branches.
6602        check_metrics::<TclParser>(
6603            "proc f {} {\n\
6604                 set x 0\n\
6605                 incr x\n\
6606                 append s \"hi\"\n\
6607                 lappend lst 1\n\
6608             }",
6609            "foo.tcl",
6610            |metric| {
6611                // `set` (1) + `incr` (1) + `append` (1) + `lappend`
6612                // (1) → A=4. No branches, no conditions.
6613                assert_eq!(metric.abc.assignments_sum(), 4.0);
6614                assert_eq!(metric.abc.branches_sum(), 0.0);
6615                assert_eq!(metric.abc.conditions_sum(), 0.0);
6616                insta::assert_json_snapshot!(metric.abc);
6617            },
6618        );
6619    }
6620
6621    #[test]
6622    fn tcl_generic_commands_are_branches() {
6623        // Anything that isn't `set` or a known mutator command
6624        // counts as a branch — including builtins like `puts` and
6625        // `return`.
6626        check_metrics::<TclParser>(
6627            "proc f {} {\n\
6628                 puts \"hello\"\n\
6629                 do_work 1 2\n\
6630                 return 0\n\
6631             }",
6632            "foo.tcl",
6633            |metric| {
6634                // 3 commands, all branches.
6635                assert_eq!(metric.abc.assignments_sum(), 0.0);
6636                assert_eq!(metric.abc.branches_sum(), 3.0);
6637                assert_eq!(metric.abc.conditions_sum(), 0.0);
6638                insta::assert_json_snapshot!(metric.abc);
6639            },
6640        );
6641    }
6642
6643    #[test]
6644    fn tcl_comparisons_and_boolean_ops_count_conditions() {
6645        // `expr` predicates expose comparison / logical tokens at
6646        // the leaf level; each token contributes one condition.
6647        check_metrics::<TclParser>(
6648            "proc f {a b} {\n\
6649                 set r [expr {$a == $b}]\n\
6650                 set r [expr {$a != $b}]\n\
6651                 set r [expr {$a <  $b}]\n\
6652                 set r [expr {$a >  $b}]\n\
6653                 set r [expr {$a <= $b}]\n\
6654                 set r [expr {$a >= $b}]\n\
6655                 set r [expr {$a eq $b}]\n\
6656                 set r [expr {$a ne $b}]\n\
6657                 set r [expr {$a && $b}]\n\
6658                 set r [expr {$a || $b}]\n\
6659             }",
6660            "foo.tcl",
6661            |metric| {
6662                // 10 `set` assignments. Each `expr` predicate
6663                // produces exactly one comparison/logical token.
6664                assert_eq!(metric.abc.assignments_sum(), 10.0);
6665                assert_eq!(metric.abc.branches_sum(), 0.0);
6666                assert_eq!(metric.abc.conditions_sum(), 10.0);
6667                insta::assert_json_snapshot!(metric.abc);
6668            },
6669        );
6670    }
6671
6672    #[test]
6673    fn tcl_ternary_counts_condition() {
6674        // `$a ? $b : $c` inside an `expr` is one `ternary_expr`
6675        // node → 1 condition.
6676        check_metrics::<TclParser>(
6677            "proc f {a b c} {\n\
6678                 set r [expr {$a ? $b : $c}]\n\
6679             }",
6680            "foo.tcl",
6681            |metric| {
6682                assert_eq!(metric.abc.assignments_sum(), 1.0);
6683                assert_eq!(metric.abc.branches_sum(), 0.0);
6684                assert_eq!(metric.abc.conditions_sum(), 1.0);
6685                insta::assert_json_snapshot!(metric.abc);
6686            },
6687        );
6688    }
6689
6690    #[test]
6691    fn tcl_elseif_and_else_count_conditions() {
6692        // `if` / `elseif` / `else` clause productions each
6693        // contribute one condition. The leaf comparison inside the
6694        // predicate is counted independently.
6695        check_metrics::<TclParser>(
6696            "proc f {x} {\n\
6697                 if {$x > 0} {\n\
6698                     return 1\n\
6699                 } elseif {$x < 0} {\n\
6700                     return -1\n\
6701                 } else {\n\
6702                     return 0\n\
6703                 }\n\
6704             }",
6705            "foo.tcl",
6706            |metric| {
6707                // Branches: three `return` commands → 3.
6708                // Conditions: `>` (1), `<` (1), `elseif` (1), `else`
6709                // (1) → 4.
6710                assert_eq!(metric.abc.assignments_sum(), 0.0);
6711                assert_eq!(metric.abc.branches_sum(), 3.0);
6712                assert_eq!(metric.abc.conditions_sum(), 4.0);
6713                insta::assert_json_snapshot!(metric.abc);
6714            },
6715        );
6716    }
6717
6718    #[test]
6719    fn tcl_complex_function_abc() {
6720        // Mixed program covering every category. Tcl's grammar
6721        // re-parses braced content that looks command-shaped as a
6722        // nested `command` node, which inflates the branch count
6723        // relative to a naive read of the source — see breakdown.
6724        check_metrics::<TclParser>(
6725            "proc run {n} {\n\
6726                 set total 0\n\
6727                 for {set i 0} {$i < $n} {incr i} {\n\
6728                     if {$i % 2 == 0} {\n\
6729                         do_work $i\n\
6730                     } else {\n\
6731                         incr total $i\n\
6732                     }\n\
6733                 }\n\
6734                 puts \"done\"\n\
6735                 return $total\n\
6736             }",
6737            "foo.tcl",
6738            |metric| {
6739                // Assignments: `set total 0` (1), `set i 0` (1),
6740                // `incr i` (1), `incr total $i` (1) → A = 4.
6741                // Branches: the outer `for …` is one `command` node;
6742                // the `{$i < $n}` predicate ALSO re-parses as a
6743                // `command` node (tree-sitter-tcl treats braced
6744                // predicates as nested commands at the pinned
6745                // grammar version); plus `do_work $i`, `puts
6746                // "done"`, and `return $total`. The for-loop body's
6747                // `incr` and `incr total $i` are assignment commands
6748                // and don't add branches. Total B = 5.
6749                // Conditions: `==` (1) and `else` (1) → C = 2. The
6750                // `<` inside `{$i < $n}` is NOT `Tcl::LT`: because
6751                // that predicate re-parses as a `command`, the `<`
6752                // is emitted as `simple_word`. Only `<` inside a
6753                // real `expr` production becomes `Tcl::LT`.
6754                assert_eq!(metric.abc.assignments_sum(), 4.0);
6755                assert_eq!(metric.abc.branches_sum(), 5.0);
6756                assert_eq!(metric.abc.conditions_sum(), 2.0);
6757                insta::assert_json_snapshot!(metric.abc);
6758            },
6759        );
6760    }
6761}