Skip to main content

big_code_analysis/metrics/
abc.rs

1// Per-language metric and AST modules deliberately consume the macro-
2// generated tree-sitter token enums via `use crate::*` and `use Foo::*`
3// inside match expressions — explicit imports would list dozens of
4// variants per arm and obscure the per-language token sets that are the
5// point of these files. Allowed at the module level rather than per
6// function so the per-language impl blocks stay readable.
7#![allow(
8    clippy::enum_glob_use,
9    clippy::too_many_lines,
10    clippy::wildcard_imports
11)]
12// Metric counts (token, function, branch, argument, etc.) are stored as
13// `usize` and crossed with `f64` averages, ratios, and Halstead scores
14// across the cyclomatic / MI / Halstead computations. The `usize as f64`
15// and `f64 as usize` casts are intentional and snapshot-anchored — every
16// site is bounded by the count it came from. Allowing the lints at the
17// module level keeps the metric arithmetic legible.
18#![allow(
19    clippy::cast_precision_loss,
20    clippy::cast_possible_truncation,
21    clippy::cast_sign_loss
22)]
23
24use std::fmt;
25
26use crate::checker::Checker;
27
28use crate::macros::implement_metric_trait;
29
30use crate::*;
31
32mod bash;
33mod c;
34mod cpp;
35mod csharp;
36mod elixir;
37mod go;
38mod groovy;
39mod irules;
40mod java;
41mod js_family;
42mod kotlin;
43mod lua;
44mod mozcpp;
45mod objc;
46mod perl;
47mod php;
48mod python;
49mod ruby;
50mod rust;
51mod tcl;
52
53/// The `ABC` metric.
54///
55/// The `ABC` metric measures the size of a source code by counting
56/// the number of Assignments (`A`), Branches (`B`) and Conditions (`C`).
57/// The metric defines an ABC score as a vector of three elements (`<A,B,C>`).
58/// The ABC score can be represented by its individual components (`A`, `B` and `C`)
59/// or by the magnitude of the vector (`|<A,B,C>| = sqrt(A^2 + B^2 + C^2)`).
60///
61/// Official paper and definition:
62///
63/// Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report.
64///
65/// <https://www.softwarerenovation.com/Articles.aspx>
66///
67/// # Cross-language `&&` / `||` policy
68///
69/// Per Fitzpatrick's conditional-operator rule (Rule 5 in Figure 2
70/// for C and Figure 4 for Java; Rule 7 in Figure 3 for C++), only
71/// comparison operators (`==`, `!=`, `<=`, `>=`, `<`, `>`) and a
72/// paper-defined keyword set (`else`, `case`, `default`, `?`, plus
73/// `try` / `catch` for C++ and Java) contribute to the condition
74/// count. Per-language `impl Abc` blocks narrow this set where
75/// appropriate — e.g., C++/Rust/Go/Python exclude `default` since
76/// it falls through unconditionally (matching the Rust `_ =>` and
77/// Java `default:` precedent). The short-
78/// circuit logical operators `&&` and `||` (and per-language
79/// equivalents — Python's `and` / `or`, Lua's `and` / `or`, Tcl's
80/// `&&` / `||`, Perl's `&&` / `||` / `//` / `and` / `or` / `xor`)
81/// are deliberately **not** counted on their own. The paper's
82/// worked Listing 2 annotates `(am >= 0 && am <= 0xF) ? '/' : 'C'`
83/// as `accc` — three conditions for `>=`, `<=`, `?`, zero for
84/// `&&`.
85///
86/// Fitzpatrick's Rule 7 (Figure 3, C++) / Rule 9 (Figure 4, Java) —
87/// "Add one to the condition count for each unary conditional
88/// expression" — instead counts each non-comparison operand of a
89/// `&&` / `||` chain once. The paper's worked example for this
90/// rule is `if (x || y) printf("test failure\n");`, annotated:
91/// "there are two unary conditions since both `x` and `y` are
92/// tested as conditional expressions" (so `||` contributes zero,
93/// `x` contributes one, `y` contributes one, and `printf(...)`
94/// contributes one branch). The walker machinery for this —
95/// modelled on `java_count_unary_conditions` /
96/// `java_inspect_container` — is present today for Java, Groovy,
97/// C#, Rust, Go, JavaScript, TypeScript, TSX, Mozjs, PHP, C++,
98/// Python, Perl, Lua, Tcl, iRules, Kotlin, Ruby, and Elixir. So
99/// `if (a && b)` reports 2 conditions across this set, matching
100/// the paper. Bash is the lone exception: its `&&` / `||` are
101/// command-list separators rather than boolean-expression operands
102/// with named leaf operands, so Fitzpatrick's Rule 9 does not map
103/// onto its grammar and the walker is deliberately not wired.
104///
105/// This policy is paper-faithful and deviates from RuboCop's
106/// `Metrics/AbcSize` (which counts `and` / `or` as conditions
107/// directly) while matching `StepicOrg/abcmeter` and
108/// `eoinnoble/python-abc`. The book's *ABC counting rules*
109/// section reproduces the rule tables, a per-language deviation
110/// table, and worked examples — see the chapter at
111/// <https://dekobon.github.io/big-code-analysis/metrics.html#abc>.
112///
113/// # Cross-language empty-`for`-condition policy
114///
115/// `for (;;)` — and every other spelling that omits the test slot
116/// (`for (init; ; update)`, Go's bare `for {}`) — counts **zero**
117/// conditions. Nothing in Fitzpatrick's condition rules attributes a
118/// count to a `for` keyword: they count conditional operators and
119/// unary conditions that are *present*, and an omitted test is not a
120/// decision. Most languages get this for free: `*_walk_for_statement`
121/// asks `for_statement` for its `condition` field and an empty header
122/// has none. Two do not, and neither needs a special case either —
123/// the JS family fills the slot with an `empty_statement`, which is
124/// not a boolean terminal and not a paren / `!` wrapper, so it falls
125/// through; and Go, whose `for_statement` exposes no `condition` field
126/// at all, locates the header slot structurally and finds only the
127/// body. Before #1276 Java and Groovy alone disagreed, counting the
128/// `;` or `)` that landed in a positional child slot as a
129/// vacuously-true condition.
130///
131/// See issue #395 for the Phase-1 cross-language policy
132/// alignment, #403 for the Phase-2 unary-conditional walker
133/// fan-out, #404 for the Phase-3 book documentation, #557
134/// for the Kotlin / Ruby / Elixir walker wiring, and #1276 for the
135/// `for`-header condition slot.
136#[derive(Debug, Clone, PartialEq)]
137#[non_exhaustive]
138pub struct Stats {
139    pub(super) assignments: f64,
140    assignments_sum: f64,
141    assignments_min: f64,
142    assignments_max: f64,
143    pub(super) branches: f64,
144    branches_sum: f64,
145    branches_min: f64,
146    branches_max: f64,
147    pub(super) conditions: f64,
148    conditions_sum: f64,
149    conditions_min: f64,
150    conditions_max: f64,
151    space_count: usize,
152}
153
154impl Default for Stats {
155    fn default() -> Self {
156        Self {
157            assignments: 0.,
158            assignments_sum: 0.,
159            assignments_min: f64::MAX,
160            assignments_max: 0.,
161            branches: 0.,
162            branches_sum: 0.,
163            branches_min: f64::MAX,
164            branches_max: 0.,
165            conditions: 0.,
166            conditions_sum: 0.,
167            conditions_min: f64::MAX,
168            conditions_max: 0.,
169            space_count: 1,
170        }
171    }
172}
173
174impl fmt::Display for Stats {
175    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
176        write!(
177            f,
178            "assignments: {}, branches: {}, conditions: {}, magnitude: {}, \
179            assignments_average: {}, branches_average: {}, conditions_average: {}, \
180            assignments_min: {}, assignments_max: {}, \
181            branches_min: {}, branches_max: {}, \
182            conditions_min: {}, conditions_max: {}",
183            self.assignments_sum(),
184            self.branches_sum(),
185            self.conditions_sum(),
186            self.magnitude_sum(),
187            self.assignments_average(),
188            self.branches_average(),
189            self.conditions_average(),
190            self.assignments_min(),
191            self.assignments_max(),
192            self.branches_min(),
193            self.branches_max(),
194            self.conditions_min(),
195            self.conditions_max()
196        )
197    }
198}
199
200impl Stats {
201    /// Merges a second `Abc` metric into the first one.
202    pub fn merge(&mut self, other: &Stats) {
203        // Calculates minimum and maximum values
204        self.assignments_min = self.assignments_min.min(other.assignments_min);
205        self.assignments_max = self.assignments_max.max(other.assignments_max);
206        self.branches_min = self.branches_min.min(other.branches_min);
207        self.branches_max = self.branches_max.max(other.branches_max);
208        self.conditions_min = self.conditions_min.min(other.conditions_min);
209        self.conditions_max = self.conditions_max.max(other.conditions_max);
210
211        self.assignments_sum += other.assignments_sum;
212        self.branches_sum += other.branches_sum;
213        self.conditions_sum += other.conditions_sum;
214
215        self.space_count += other.space_count;
216    }
217
218    /// Returns the `Abc` assignments metric value.
219    #[must_use]
220    pub fn assignments(&self) -> u64 {
221        self.assignments as u64
222    }
223
224    /// Returns the `Abc` assignments sum metric value.
225    #[must_use]
226    pub fn assignments_sum(&self) -> u64 {
227        self.assignments_sum as u64
228    }
229
230    /// Returns the `Abc` assignments average value.
231    ///
232    /// This value is computed dividing the `Abc`
233    /// assignments value for the number of spaces.
234    #[must_use]
235    pub fn assignments_average(&self) -> f64 {
236        crate::metrics::average(self.assignments_sum() as f64, self.space_count)
237    }
238
239    /// Returns the `Abc` assignments minimum value.
240    ///
241    /// Collapses the `f64::MAX` sentinel that `Stats::default()` plants
242    /// into `assignments_min` to `0`, so a never-observed space
243    /// serializes to a meaningful number rather than `1.7976931e308`.
244    #[allow(clippy::float_cmp)]
245    #[must_use]
246    pub fn assignments_min(&self) -> u64 {
247        if self.assignments_min == f64::MAX {
248            0
249        } else {
250            self.assignments_min as u64
251        }
252    }
253
254    /// Returns the `Abc` assignments maximum value.
255    #[must_use]
256    pub fn assignments_max(&self) -> u64 {
257        self.assignments_max as u64
258    }
259
260    /// Returns the `Abc` branches metric value.
261    #[must_use]
262    pub fn branches(&self) -> u64 {
263        self.branches as u64
264    }
265
266    /// Returns the `Abc` branches sum metric value.
267    #[must_use]
268    pub fn branches_sum(&self) -> u64 {
269        self.branches_sum as u64
270    }
271
272    /// Returns the `Abc` branches average value.
273    ///
274    /// This value is computed dividing the `Abc`
275    /// branches value for the number of spaces.
276    #[must_use]
277    pub fn branches_average(&self) -> f64 {
278        crate::metrics::average(self.branches_sum() as f64, self.space_count)
279    }
280
281    /// Returns the `Abc` branches minimum value.
282    ///
283    /// Same `f64::MAX` sentinel collapse as `assignments_min`.
284    #[allow(clippy::float_cmp)]
285    #[must_use]
286    pub fn branches_min(&self) -> u64 {
287        if self.branches_min == f64::MAX {
288            0
289        } else {
290            self.branches_min as u64
291        }
292    }
293
294    /// Returns the `Abc` branches maximum value.
295    #[must_use]
296    pub fn branches_max(&self) -> u64 {
297        self.branches_max as u64
298    }
299
300    /// Returns the `Abc` conditions metric value.
301    #[must_use]
302    pub fn conditions(&self) -> u64 {
303        self.conditions as u64
304    }
305
306    /// Returns the `Abc` conditions sum metric value.
307    #[must_use]
308    pub fn conditions_sum(&self) -> u64 {
309        self.conditions_sum as u64
310    }
311
312    /// Returns the `Abc` conditions average value.
313    ///
314    /// This value is computed dividing the `Abc`
315    /// conditions value for the number of spaces.
316    #[must_use]
317    pub fn conditions_average(&self) -> f64 {
318        crate::metrics::average(self.conditions_sum() as f64, self.space_count)
319    }
320
321    /// Returns the `Abc` conditions minimum value.
322    ///
323    /// Same `f64::MAX` sentinel collapse as `assignments_min`.
324    #[allow(clippy::float_cmp)]
325    #[must_use]
326    pub fn conditions_min(&self) -> u64 {
327        if self.conditions_min == f64::MAX {
328            0
329        } else {
330            self.conditions_min as u64
331        }
332    }
333
334    /// Returns the `Abc` conditions maximum value.
335    #[must_use]
336    pub fn conditions_max(&self) -> u64 {
337        self.conditions_max as u64
338    }
339
340    /// Returns the `Abc` magnitude metric value.
341    #[must_use]
342    pub fn magnitude(&self) -> f64 {
343        (self.assignments.powi(2) + self.branches.powi(2) + self.conditions.powi(2)).sqrt()
344    }
345
346    /// Returns the `Abc` magnitude sum metric value.
347    #[must_use]
348    pub fn magnitude_sum(&self) -> f64 {
349        (self.assignments_sum.powi(2) + self.branches_sum.powi(2) + self.conditions_sum.powi(2))
350            .sqrt()
351    }
352
353    #[inline]
354    pub(crate) fn compute_sum(&mut self) {
355        self.assignments_sum += self.assignments;
356        self.branches_sum += self.branches;
357        self.conditions_sum += self.conditions;
358    }
359
360    #[inline]
361    pub(crate) fn compute_minmax(&mut self) {
362        self.assignments_min = self.assignments_min.min(self.assignments);
363        self.assignments_max = self.assignments_max.max(self.assignments);
364        self.branches_min = self.branches_min.min(self.branches);
365        self.branches_max = self.branches_max.max(self.branches);
366        self.conditions_min = self.conditions_min.min(self.conditions);
367        self.conditions_max = self.conditions_max.max(self.conditions);
368        self.compute_sum();
369    }
370}
371
372#[doc(hidden)]
373/// Per-language computation of the ABC metric.
374pub(crate) trait Abc
375where
376    Self: Checker,
377{
378    /// Walk `node` and update `stats` with this metric for the language
379    /// implementing the trait.
380    ///
381    /// `code` is the source bytes underlying the parsed tree. Most
382    /// languages ignore it: assignments, branches, and conditions all
383    /// surface as distinct grammar productions and a `kind_id()` match
384    /// is enough. Elixir is the exception — `case` / `cond` / `if` /
385    /// `with` / guard `when` arms surface as `Call` nodes whose keyword
386    /// target lives only in the source text. Matching the `Cyclomatic`
387    /// / `Halstead` / `Exit` / `Cognitive` pattern keeps the signature
388    /// uniform.
389    ///
390    /// `ancestors` is the chain the walker descended through. Nearly
391    /// every language classifies some token by what encloses it: `<`
392    /// and `>` are comparisons only under a binary expression (a type
393    /// -argument list otherwise), and `&&` / `||` reach their operands
394    /// through the enclosing chain node. Reaching that parent with
395    /// [`Node::parent`] costs `O(depth)` per node (#1096). The
396    /// condition-slot walkers take *their* parent as an argument
397    /// instead, because the caller descended from it.
398    fn compute<'a>(
399        node: &Node<'a>,
400        code: &'a [u8],
401        ancestors: Ancestors<'a, '_>,
402        stats: &mut Stats,
403    );
404}
405
406// Shared Phase-2B helper (issue #403): walk every named child of an
407// expression-list-style wrapper (Go's `expression_list`, Lua's
408// `expression_list`) and route each through a language-specific
409// classifier. Used for `return value1, value2, ...` arms where the
410// values live one level below the return statement under a list
411// wrapper. The classifier receives only named children so that
412// `,` / `;` / `(` / `)` tokens never reach it, plus `list` itself as
413// the child's parent — the container classifiers seed their
414// boolean-context flag from the parent kind, and reaching it with
415// `Node::parent` would cost `O(depth)` per node (#1096).
416pub(super) fn for_each_named_child(
417    list: &Node,
418    conditions: &mut f64,
419    f: fn(&Node, &Node, &mut f64),
420) {
421    let mut cursor = list.cursor();
422    if cursor.goto_first_child() {
423        loop {
424            let child = cursor.node();
425            if child.is_named() {
426                f(&child, list, conditions);
427            }
428            if !cursor.goto_next_sibling() {
429                break;
430            }
431        }
432    }
433}
434
435// Default no-op `Abc` impls. Audited in #188; the matrix below
436// records the rationale for every entry so the no-op default is a
437// deliberate choice, not scaffolding leftover.
438//
439// Real defaults (the language has no construct ABC measures, so the
440// metric is genuinely 0):
441//   - PreprocCode, CcommentCode: no executable code (comments /
442//     preprocessor lines only).
443implement_metric_trait!(Abc, PreprocCode, CcommentCode);
444
445#[cfg(test)]
446#[allow(
447    clippy::float_cmp,
448    clippy::cast_precision_loss,
449    clippy::cast_possible_truncation,
450    clippy::cast_sign_loss,
451    clippy::similar_names,
452    clippy::doc_markdown,
453    clippy::needless_raw_string_hashes,
454    clippy::too_many_lines
455)]
456mod tests {
457    use crate::test_support::{
458        ast_has_kind_id, check_func_space_only_shim, check_metrics_only_shim, metrics_verbatim,
459    };
460    use crate::traits::ParserTrait;
461
462    use super::*;
463
464    check_metrics_only_shim!(check_metrics, Abc);
465    // Every `check_func_space` caller in this module is an ABC-versus-
466    // cyclomatic parity test (`abc.conditions() == cyclomatic() - 1` on
467    // the same space), so the func-space shim carries Cyclomatic too.
468    check_func_space_only_shim!(check_func_space, Abc, Cyclomatic);
469
470    /// `abc.conditions_sum()` of `src` under a walk restricted to ABC
471    /// and the metrics it resolves, for the cross-language parity tests
472    /// that compare one construct across sibling grammars.
473    /// `check_metrics` expands to a plain `fn` and so cannot close over
474    /// a reference value; this can. Restricted rather than
475    /// `MetricsOptions::default()` per `metrics_verbatim`'s own doc
476    /// (#1127).
477    fn abc_conditions(lang: LANG, src: &str) -> u64 {
478        metrics_verbatim(
479            lang,
480            src.as_bytes(),
481            MetricsOptions::default().with_only(&[crate::Metric::Abc]),
482        )
483        .abc
484        .conditions_sum()
485    }
486
487    // Recurse to the innermost function space and assert the invariant
488    // named above there. `conditions()` is that one space's own count,
489    // so it pairs with `cyclomatic()` and never `cyclomatic_sum()`,
490    // which folds in a base of 1 per nested space.
491    fn assert_deepest_conditions_match_cyclomatic(space: &crate::FuncSpace, expected: u64) {
492        let mut deepest = space;
493        while let Some(child) = deepest.spaces.last() {
494            deepest = child;
495        }
496        let decisions = deepest.metrics.cyclomatic.cyclomatic() - 1;
497        assert_eq!(decisions, expected);
498        assert_eq!(deepest.metrics.abc.conditions(), decisions);
499    }
500
501    /// Regression for #227: a `Stats::default()` that never sees an
502    /// observation must not leak the `f64::MAX` sentinel for
503    /// `assignments_min`, `branches_min`, or `conditions_min`. All
504    /// three getters collapse the sentinel to `0.0` so JSON never
505    /// emits `1.7976931e308`.
506    #[test]
507    fn abc_empty_file_min_is_zero() {
508        let stats = Stats::default();
509        assert_eq!(stats.assignments_min(), 0);
510        assert_eq!(stats.branches_min(), 0);
511        assert_eq!(stats.conditions_min(), 0);
512    }
513
514    // The `EQ` arm of `java_count_token_assignment`: a plain `=` counts
515    // unless `java_eq_initializes_final_binding` finds it initialising a
516    // `final` binding, whether the declaration is a local or a field.
517    #[test]
518    fn java_eq_arm_counts_outside_final_declarations() {
519        check_metrics::<JavaParser>(
520            "class A { void m() { int x = 0; x = 1; x = 2; x = 3; } }",
521            "foo.java",
522            |metric| {
523                // `int x = 0;` is not `final`, so it counts like each
524                // `x = N;` that follows.
525                assert_eq!(metric.abc.assignments_sum(), 4);
526            },
527        );
528    }
529
530    #[test]
531    fn java_eq_arm_skips_final_initializers() {
532        check_metrics::<JavaParser>(
533            "class A {
534                final int X = 1;
535                @Deprecated private final int Y = 2;
536                void m() { final int Z = 3; }
537            }",
538            "foo.java",
539            |metric| {
540                // All three `=` tokens are `final` initializers — the
541                // second behind an annotation and an access modifier in
542                // the same `modifiers` node — so assignments are 0
543                // across all spaces.
544                assert_eq!(metric.abc.assignments_sum(), 0);
545            },
546        );
547    }
548
549    #[test]
550    fn java_final_initializer_does_not_suppress_the_assignments_inside_it() {
551        // The sentinel stack the structural predicate replaced stayed
552        // live from `final` to the next `;`, so every `=` nested in the
553        // initializer — a lambda body's, an array initializer's — was
554        // suppressed with the declarator's own. Java lambdas open no
555        // space, so nothing else separated them. Each row names the
556        // pre-fix value.
557        let cases = [
558            ("void m() { final Runnable r = () -> { x = 1; }; }", 1, 0),
559            (
560                "void m() { final Runnable r = () -> { x = 1; y = 2; }; }",
561                2,
562                1,
563            ),
564            ("void m() { final Runnable r = () -> x = 1; }", 1, 0),
565            ("void m() { final int[] a = { x = 1 }; }", 1, 0),
566            ("private final Runnable f = () -> { x = 1; };", 1, 0),
567            // The `;` that closed the leak is the reference: the same
568            // body after a `final` local counts as it always did.
569            ("void m() { final int q = 1; x = 1; }", 1, 1),
570        ];
571        let mut ran = 0;
572        for (body, expected, before) in cases {
573            let src = format!("class K {{ int x, y; {body} }}\n");
574            let assignments = metrics_verbatim(
575                LANG::Java,
576                src.as_bytes(),
577                MetricsOptions::default().with_only(&[crate::Metric::Abc]),
578            )
579            .abc
580            .assignments_sum();
581            assert_eq!(assignments, expected, "`{body}` (pre-fix {before})");
582            ran += 1;
583        }
584        assert_eq!(ran, cases.len());
585        assert!(cases.iter().any(|&(_, now, before)| now != before));
586    }
587
588    // Constant declarations are not counted as assignments
589    #[test]
590    fn java_constant_declarations() {
591        check_metrics::<JavaParser>(
592            "class A {
593                private final int X1 = 0, Y1 = 0;
594                public final float PI = 3.14f;
595                final static String HELLO = \"Hello,\";
596                protected String world = \" world!\";   // +1a
597                public float e = 2.718f;                // +1a
598                private int x2 = 1, y2 = 2;             // +2a
599
600                void m() {
601                    final int Z1 = 0, Z2 = 0, Z3 = 0;
602                    final float T = 0.0f;
603                    int z1 = 1, z2 = 2, z3 = 3;         // +3a
604                    float t = 60.0f;                    // +1a
605                }
606            }",
607            "foo.java",
608            |metric| {
609                // magnitude: sqrt(64 + 0 + 0) = sqrt(64)
610                // space count: 3 (1 unit, 1 class and 1 method)
611                insta::assert_json_snapshot!(
612                    metric.abc,
613                    @r#"
614                {
615                  "assignments": 8,
616                  "branches": 0,
617                  "conditions": 0,
618                  "magnitude": 8.0,
619                  "value": 0.0,
620                  "assignments_average": 2.6666666666666665,
621                  "branches_average": 0.0,
622                  "conditions_average": 0.0,
623                  "assignments_min": 0,
624                  "assignments_max": 4,
625                  "branches_min": 0,
626                  "branches_max": 0,
627                  "conditions_min": 0,
628                  "conditions_max": 0
629                }
630                "#
631                );
632            },
633        );
634    }
635
636    // "In computer science, conditionals (that is, conditional statements, conditional expressions
637    // and conditional constructs,) are programming language commands for handling decisions."
638    // Source: https://en.wikipedia.org/wiki/Conditional_(computer_programming)
639    // According to this definition, boolean expressions that are evaluated to make a decision are considered as conditions
640    // Variables, method invocations and true or false values used inside
641    // variable declarations and assignment expressions are not counted as conditions
642    #[test]
643    fn java_declarations_with_conditions() {
644        check_metrics::<JavaParser>(
645            "
646            boolean a = (1 > 2);            // +1a +1c
647            boolean b = 3 > 4;              // +1a +1c
648            boolean c = (1 > 2) && 3 > 4;   // +1a +2c
649            boolean d = b && (x > 5) || c;  // +1a +3c
650            boolean e = !d;                 // +1a +1c
651            boolean f = ((!false));         // +1a +1c
652            boolean g = !(!(true));         // +1a +1c
653            boolean h = true;               // +1a
654            boolean i = (false);            // +1a
655            boolean j = (((((true)))));     // +1a
656            boolean k = (((((m())))));      // +1a +1b
657            boolean l = (((((!m())))));     // +1a +1b +1c
658            boolean m = (!(!((m()))));      // +1a +1b +1c
659            List<String> n = null;          // +1a (< and > used for generic types are not counted as conditions)
660            ",
661            "foo.java",
662          |metric| {
663                // magnitude: sqrt(196 + 9 + 144) = sqrt(349)
664                // space count: 1 (1 unit)
665                insta::assert_json_snapshot!(
666                    metric.abc,
667                    @r#"
668                {
669                  "assignments": 14,
670                  "branches": 3,
671                  "conditions": 12,
672                  "magnitude": 18.681541692269406,
673                  "value": 18.681541692269406,
674                  "assignments_average": 14.0,
675                  "branches_average": 3.0,
676                  "conditions_average": 12.0,
677                  "assignments_min": 14,
678                  "assignments_max": 14,
679                  "branches_min": 3,
680                  "branches_max": 3,
681                  "conditions_min": 12,
682                  "conditions_max": 12
683                }
684                "#
685                );
686            },
687        );
688    }
689
690    // Conditions can be found in assignment expressions
691    #[test]
692    fn java_assignments_with_conditions() {
693        check_metrics::<JavaParser>(
694            "
695            a = 2 < 1;                  // +1a +1c
696            b = (4 >= 3) && 2 <= 1;     // +1a +2c
697            c = a || (x != 10) && b;    // +1a +3c
698            d = !false;                 // +1a +1c
699            e = (!false);               // +1a +1c
700            f = !(false);               // +1a +1c
701            g = (!(((true))));          // +1a +1c
702            h = ((true));               // +1a
703            i = !m();                   // +1a +1b +1c
704            j = !((m()));               // +1a +1b +1c
705            k = (!(m()));               // +1a +1b +1c
706            l = ((!(m())));             // +1a +1b +1c
707            m = !B.<Integer>m(2);       // +1a +1b +1c
708            n = !((B.<Integer>m(4)));   // +1a +1b +1c
709            ",
710            "foo.java",
711            |metric| {
712                // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
713                // space count: 1 (1 unit)
714                insta::assert_json_snapshot!(
715                    metric.abc,
716                    @r#"
717                {
718                  "assignments": 14,
719                  "branches": 6,
720                  "conditions": 16,
721                  "magnitude": 22.090722034374522,
722                  "value": 22.090722034374522,
723                  "assignments_average": 14.0,
724                  "branches_average": 6.0,
725                  "conditions_average": 16.0,
726                  "assignments_min": 14,
727                  "assignments_max": 14,
728                  "branches_min": 6,
729                  "branches_max": 6,
730                  "conditions_min": 16,
731                  "conditions_max": 16
732                }
733                "#
734                );
735            },
736        );
737    }
738
739    // Conditions can be found in method arguments
740    #[test]
741    fn java_methods_arguments_with_conditions() {
742        check_metrics::<JavaParser>(
743            "
744            m1(a);                                  // +1b
745            m2(a, b);                               // +1b
746            m3(true, (false), (((true))));          // +1b
747            m3(m1(false), m1(true), m1(false));     // +4b
748            m1(!a);                                 // +1b +1c
749            m2((((a))), (!b));                      // +1b +1c
750            m3(!(a), b, !!!c);                      // +1b +2c
751            m3(a, !b, m2(!a, !m2(!b, !m1(!c))));    // +4b +6c
752            ",
753            "foo.java",
754            |metric| {
755                // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
756                // space count: 1 (1 unit)
757                insta::assert_json_snapshot!(
758                    metric.abc,
759                    @r#"
760                {
761                  "assignments": 0,
762                  "branches": 14,
763                  "conditions": 10,
764                  "magnitude": 17.204650534085253,
765                  "value": 17.204650534085253,
766                  "assignments_average": 0.0,
767                  "branches_average": 14.0,
768                  "conditions_average": 10.0,
769                  "assignments_min": 0,
770                  "assignments_max": 0,
771                  "branches_min": 14,
772                  "branches_max": 14,
773                  "conditions_min": 10,
774                  "conditions_max": 10
775                }
776                "#
777                );
778            },
779        );
780    }
781
782    // "A unary conditional expression is an implicit condition that uses no relational operators."
783    // Source: Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report.
784    // https://www.softwarerenovation.com/Articles.aspx (page 5)
785    #[test]
786    fn java_if_single_conditions() {
787        check_metrics::<JavaParser>(
788            "
789            if ( a < 0 ) {}             // +1c
790            if ( ((a != 0)) ) {}        // +1c
791            if ( !(a > 0) ) {}          // +1c
792            if ( !(((a == 0))) ) {}     // +1c
793            if ( b.m1() ) {}            // +1b +1c
794            if ( !b.m1() ) {}           // +1b +1c
795            if ( !!b.m2() ) {}          // +1b +1c
796            if ( (!(b.m1())) ) {}       // +1b +1c
797            if ( (!(!b.m1())) ) {}      // +1b +1c
798            if ( ((b.m2())) ) {}        // +1b +1c
799            if ( ((b.m().m1())) ) {}    // +2b +1c
800            if ( c ) {}                 // +1c
801            if ( !c ) {}                // +1c
802            if ( !!!!!!!!!!c ) {}       // +1c
803            if ( (((c))) ) {}           // +1c
804            if ( (((!c))) ) {}          // +1c
805            if ( ((!(c))) ) {}          // +1c
806            if ( true ) {}              // +1c
807            if ( !true ) {}             // +1c
808            if ( ((false)) ) {}         // +1c
809            if ( !(!(false)) ) {}       // +1c
810            if ( !!!false ) {}          // +1c
811            ",
812            "foo.java",
813            |metric| {
814                // magnitude: sqrt(0 + 64 + 484) = sqrt(548)
815                // space count: 1 (1 unit)
816                insta::assert_json_snapshot!(
817                    metric.abc,
818                    @r#"
819                {
820                  "assignments": 0,
821                  "branches": 8,
822                  "conditions": 22,
823                  "magnitude": 23.40939982143925,
824                  "value": 23.40939982143925,
825                  "assignments_average": 0.0,
826                  "branches_average": 8.0,
827                  "conditions_average": 22.0,
828                  "assignments_min": 0,
829                  "assignments_max": 0,
830                  "branches_min": 8,
831                  "branches_max": 8,
832                  "conditions_min": 22,
833                  "conditions_max": 22
834                }
835                "#
836                );
837            },
838        );
839    }
840
841    #[test]
842    fn java_if_multiple_conditions() {
843        check_metrics::<JavaParser>(
844            "
845            if ( a || b || c || d ) {}              // +4c
846            if ( a || b && c && d ) {}              // +4c
847            if ( x < y && a == b ) {}               // +2c
848            if ( ((z < (x + y))) ) {}               // +1c
849            if ( a || ((((b))) && c) ) {}           // +3c
850            if ( a && ((((a == b))) && c) ) {}      // +3c
851            if ( a || ((((a == b))) || ((c))) ) {}  // +3c
852            if ( x < y && B.m() ) {}                // +1b +2c
853            if ( x < y && !(((B.m()))) ) {}         // +1b +2c
854            if ( !(x < y) && !B.m() ) {}            // +1b +2c
855            if ( !!!(!!!(a)) && B.m() ||            // +1b +2c
856                 !B.m() && (((x > 4))) ) {}         // +1b +2c
857            ",
858            "foo.java",
859            |metric| {
860                // magnitude: sqrt(0 + 25 + 900) = sqrt(925)
861                // space count: 1 (1 unit)
862                insta::assert_json_snapshot!(
863                    metric.abc,
864                    @r#"
865                {
866                  "assignments": 0,
867                  "branches": 5,
868                  "conditions": 30,
869                  "magnitude": 30.4138126514911,
870                  "value": 30.4138126514911,
871                  "assignments_average": 0.0,
872                  "branches_average": 5.0,
873                  "conditions_average": 30.0,
874                  "assignments_min": 0,
875                  "assignments_max": 0,
876                  "branches_min": 5,
877                  "branches_max": 5,
878                  "conditions_min": 30,
879                  "conditions_max": 30
880                }
881                "#
882                );
883            },
884        );
885    }
886
887    #[test]
888    fn java_while_and_do_while_conditions() {
889        check_metrics::<JavaParser>(
890            "
891            while ( (!(!(!(a)))) ) {}                   // +1c
892            while ( b || 1 > 2 ) {}                     // +2c
893            while ( x.m() && (((c))) ) {}               // +1b +2c
894            do {} while ( !!!(((!!!a))) );              // +1c
895            do {} while ( a || (b && c) );              // +3c
896            do {} while ( !x.m() && 1 > 2 || !true );   // +1b +3c
897            ",
898            "foo.java",
899            |metric| {
900                // magnitude: sqrt(0 + 4 + 144) = sqrt(148)
901                // space count: 1 (1 unit)
902                insta::assert_json_snapshot!(
903                    metric.abc,
904                    @r#"
905                {
906                  "assignments": 0,
907                  "branches": 2,
908                  "conditions": 12,
909                  "magnitude": 12.165525060596439,
910                  "value": 12.165525060596439,
911                  "assignments_average": 0.0,
912                  "branches_average": 2.0,
913                  "conditions_average": 12.0,
914                  "assignments_min": 0,
915                  "assignments_max": 0,
916                  "branches_min": 2,
917                  "branches_max": 2,
918                  "conditions_min": 12,
919                  "conditions_max": 12
920                }
921                "#
922                );
923            },
924        );
925    }
926
927    // GMetrics, a Groovy source code analyzer, provides the following definition of unary conditional expression:
928    // "These are cases where a single variable/field/value is treated as a boolean value.
929    // Examples include `if (x)` and `return !ready`."
930    // According to this definition, unary conditional expressions are counted also in function return values.
931    // Source: https://dx42.github.io/gmetrics/metrics/AbcMetric.html
932    // Examples: https://github.com/dx42/gmetrics/blob/master/src/test/groovy/org/gmetrics/metric/abc/AbcMetric_MethodTest.groovy
933    #[test]
934    fn java_return_with_conditions() {
935        check_metrics::<JavaParser>(
936            "class A {
937                boolean m1() {
938                    return !(z >= 0);       // +1c
939                }
940                boolean m2() {
941                    return (((!x)));        // +1c
942                }
943                boolean m3() {
944                    return x && y;          // +2c
945                }
946                boolean m4() {
947                    return y || (z < 0);    // +2c
948                }
949                boolean m5() {
950                    return x || y ?         // +3c (two unary conditions and one ?)
951                        true : false;
952                }
953            }",
954            "foo.java",
955            |metric| {
956                // magnitude: sqrt(0 + 0 + 81) = sqrt(81)
957                // space count: 7 (1 unit, 1 class and 5 methods)
958                insta::assert_json_snapshot!(
959                    metric.abc,
960                    @r#"
961                {
962                  "assignments": 0,
963                  "branches": 0,
964                  "conditions": 9,
965                  "magnitude": 9.0,
966                  "value": 0.0,
967                  "assignments_average": 0.0,
968                  "branches_average": 0.0,
969                  "conditions_average": 1.2857142857142858,
970                  "assignments_min": 0,
971                  "assignments_max": 0,
972                  "branches_min": 0,
973                  "branches_max": 0,
974                  "conditions_min": 0,
975                  "conditions_max": 3
976                }
977                "#
978                );
979            },
980        );
981    }
982
983    // Variables, method invocations, and true or false values
984    // inside return statements are not counted as conditions
985    #[test]
986    fn java_return_without_conditions() {
987        check_metrics::<JavaParser>(
988            "class A {
989                boolean m1() {
990                    return x;
991                }
992                boolean m2() {
993                    return (x);
994                }
995                boolean m3() {
996                    return y.m();   // +1b
997                }
998                boolean m4() {
999                    return false;
1000                }
1001                void m5() {
1002                    return;
1003                }
1004            }",
1005            "foo.java",
1006            |metric| {
1007                // magnitude: sqrt(0 + 1 + 0) = sqrt(1)
1008                // space count: 7 (1 unit, 1 class and 5 methods)
1009                insta::assert_json_snapshot!(
1010                    metric.abc,
1011                    @r#"
1012                {
1013                  "assignments": 0,
1014                  "branches": 1,
1015                  "conditions": 0,
1016                  "magnitude": 1.0,
1017                  "value": 0.0,
1018                  "assignments_average": 0.0,
1019                  "branches_average": 0.14285714285714285,
1020                  "conditions_average": 0.0,
1021                  "assignments_min": 0,
1022                  "assignments_max": 0,
1023                  "branches_min": 0,
1024                  "branches_max": 1,
1025                  "conditions_min": 0,
1026                  "conditions_max": 0
1027                }
1028                "#
1029                );
1030            },
1031        );
1032    }
1033
1034    // Variables, method invocations, and true or false values
1035    // in lambda expression return values are not counted as conditions
1036    #[test]
1037    fn java_lambda_expressions_return_with_conditions() {
1038        check_metrics::<JavaParser>(
1039            "
1040            Predicate<Boolean> p1 = a -> a;                         // +1a
1041            Predicate<Boolean> p2 = b -> true;                      // +1a
1042            Predicate<Boolean> p3 = c -> m();                       // +1a
1043            Predicate<Integer> p4 = d -> d > 10;                    // +1a +1c
1044            Predicate<Boolean> p5 = (e) -> !e;                      // +1a +1c
1045            Predicate<Boolean> p6 = (f) -> !((!f));                 // +1a +1c
1046            Predicate<Boolean> p7 = (g) -> !g && true;              // +1a +2c
1047            BiPredicate<Boolean, Boolean> bp1 = (h, i) -> !h && !i; // +1a +2c
1048            BiPredicate<Boolean, Boolean> bp2 = (j, k) -> {
1049                return j || k;                                      // +1a +2c
1050            };
1051            ",
1052            "foo.java",
1053            |metric| {
1054                // magnitude: sqrt(81 + 1 + 81) = sqrt(163)
1055                // space count: 1 (1 unit)
1056                insta::assert_json_snapshot!(
1057                    metric.abc,
1058                    @r#"
1059                {
1060                  "assignments": 9,
1061                  "branches": 1,
1062                  "conditions": 9,
1063                  "magnitude": 12.767145334803704,
1064                  "value": 12.767145334803704,
1065                  "assignments_average": 9.0,
1066                  "branches_average": 1.0,
1067                  "conditions_average": 9.0,
1068                  "assignments_min": 9,
1069                  "assignments_max": 9,
1070                  "branches_min": 1,
1071                  "branches_max": 1,
1072                  "conditions_min": 9,
1073                  "conditions_max": 9
1074                }
1075                "#
1076                );
1077            },
1078        );
1079    }
1080
1081    #[test]
1082    fn java_for_with_variable_declaration() {
1083        check_metrics::<JavaParser>(
1084            "
1085            for ( int i1 = 0; !(!(!(!a))); i1++ ) {}                // +2a +1c
1086            for ( int i2 = 0; !B.m(); i2++ ) {}                     // +2a +1b +1c
1087            for ( int i3 = 0; a || false; i3++ ) {}                 // +2a +2c
1088            for ( int i4 = 0; a && B.m() ? true : false; i4++ ) {}  // +2a +1b +3c
1089            for ( int i5 = 0; true; i5++ ) {}                       // +2a +1c
1090            ",
1091            "foo.java",
1092            |metric| {
1093                // magnitude: sqrt(100 + 4 + 64) = sqrt(168)
1094                // space count: 1 (1 unit)
1095                insta::assert_json_snapshot!(
1096                    metric.abc,
1097                    @r#"
1098                {
1099                  "assignments": 10,
1100                  "branches": 2,
1101                  "conditions": 8,
1102                  "magnitude": 12.96148139681572,
1103                  "value": 12.96148139681572,
1104                  "assignments_average": 10.0,
1105                  "branches_average": 2.0,
1106                  "conditions_average": 8.0,
1107                  "assignments_min": 10,
1108                  "assignments_max": 10,
1109                  "branches_min": 2,
1110                  "branches_max": 2,
1111                  "conditions_min": 8,
1112                  "conditions_max": 8
1113                }
1114                "#
1115                );
1116            },
1117        );
1118    }
1119
1120    #[test]
1121    fn java_for_without_variable_declaration() {
1122        check_metrics::<JavaParser>(
1123            "class A{
1124                void m1() {
1125                    for (i = 0; x < y; i++) {}          // +2a +1c
1126                    for (i = 0; ((x < y)); i++) {}      // +2a +1c
1127                    for (i = 0; !(!(x < y)); i++) {}    // +2a +1c
1128                    for (i = 0; true; i++) {}           // +2a +1c
1129                }
1130                void m2() {
1131                    for ( ; true; ) {}  // +1c
1132                }
1133                void m3() {
1134                    for ( ; ; ) {}      // +0c — no condition to count (#1276)
1135                }
1136            }",
1137            "foo.java",
1138            |metric| {
1139                // magnitude: sqrt(64 + 0 + 25) = sqrt(89)
1140                // space count: 5 (1 unit, 1 class and 3 methods)
1141                insta::assert_json_snapshot!(
1142                    metric.abc,
1143                    @r#"
1144                {
1145                  "assignments": 8,
1146                  "branches": 0,
1147                  "conditions": 5,
1148                  "magnitude": 9.433981132056603,
1149                  "value": 0.0,
1150                  "assignments_average": 1.6,
1151                  "branches_average": 0.0,
1152                  "conditions_average": 1.0,
1153                  "assignments_min": 0,
1154                  "assignments_max": 8,
1155                  "branches_min": 0,
1156                  "branches_max": 0,
1157                  "conditions_min": 0,
1158                  "conditions_max": 4
1159                }
1160                "#
1161                );
1162            },
1163        );
1164    }
1165
1166    // Issue #1276 changed Java's answer here. `java_walk_for_statement`
1167    // used to read child(3), fall through to child(4) when that was the
1168    // `;` an expression initializer leaves behind, and count a `;` or
1169    // `)` landing there as a vacuously-true condition — so `for (;;)`
1170    // scored one. Nothing in Fitzpatrick's C dimension attributes a
1171    // condition to a `for` keyword; it counts conditional operators and
1172    // unary conditions that are present. Java and Groovy were the only
1173    // two impls disagreeing, and the field-addressed walker now reports
1174    // zero the way the C family, the JS family, PHP, C# and Go all do.
1175    #[test]
1176    fn java_empty_for_condition_counts_nothing() {
1177        check_metrics::<JavaParser>(
1178            "class A { void m() { for (;;) { break; } } }",
1179            "foo.java",
1180            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
1181        );
1182        // The other empty spelling, and the one the old cascade
1183        // actually mis-scored: with an **expression** initializer the
1184        // children are `for ( init ; ; update )`, so child(3) was the
1185        // separating `;` and child(4) the empty condition's `;`, which
1186        // the vacuous-true arm counted. The `for (int i = 0; ; i++)`
1187        // spelling scored 0 both before and after — Java's
1188        // `local_variable_declaration` swallows its own `;`, putting the
1189        // update expression at child(4), where it matched no arm — so it
1190        // would be a vacuous regression test and is deliberately not
1191        // used here.
1192        check_metrics::<JavaParser>(
1193            "class A { void m() { int i; for (i = 0; ; i++) { break; } } }",
1194            "foo.java",
1195            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
1196        );
1197    }
1198
1199    // The second defect the positional cascade carried: tree-sitter
1200    // counts comments among a node's children, so a comment anywhere in
1201    // the header shifted every index and the condition went unread —
1202    // the same failure #1181 removed from `java_walk_ternary`. Reading
1203    // the `condition` field cannot shift.
1204    #[test]
1205    fn java_for_condition_survives_a_header_comment() {
1206        check_metrics::<JavaParser>(
1207            "class A { void m(boolean a) { for (; /* n */ a; ) { break; } } }",
1208            "foo.java",
1209            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
1210        );
1211        // Unchanged control: the same loop without the comment. Both
1212        // spellings must agree, which is the property the positional
1213        // form broke.
1214        check_metrics::<JavaParser>(
1215            "class A { void m(boolean a) { for (; a; ) { break; } } }",
1216            "foo.java",
1217            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
1218        );
1219    }
1220
1221    // Variables, method invocations, and true or false values
1222    // in ternary expression return values are not counted as conditions
1223    #[test]
1224    fn java_ternary_conditions() {
1225        check_metrics::<JavaParser>(
1226            "
1227            a = true;                                   // +1a
1228            b = a ? true : false;                       // +1a +2c
1229            c = ((((a)))) ? !false : !b;                // +1a +4c
1230            d = !this.m() ? !!a : (false);              // +1a +1b +3c
1231            e = !(a) && b ? ((c)) : !d;                 // +1a +4c
1232            if ( this.m() ? a : !this.m() ) {}          // +2b +3c
1233            if ( x > 0 ? !(false) : this.m() ) {}       // +1b +3c
1234            if ( x > 0 && x != 3 ? !(a) : (!(b)) ) {}   // +5c
1235            ",
1236            "foo.java",
1237            |metric| {
1238                // magnitude: sqrt(25 + 16 + 576) = sqrt(617)
1239                //  space count: 1 (1 unit)
1240                insta::assert_json_snapshot!(
1241                    metric.abc,
1242                    @r#"
1243                {
1244                  "assignments": 5,
1245                  "branches": 4,
1246                  "conditions": 24,
1247                  "magnitude": 24.839484696748443,
1248                  "value": 24.839484696748443,
1249                  "assignments_average": 5.0,
1250                  "branches_average": 4.0,
1251                  "conditions_average": 24.0,
1252                  "assignments_min": 5,
1253                  "assignments_max": 5,
1254                  "branches_min": 4,
1255                  "branches_max": 4,
1256                  "conditions_min": 24,
1257                  "conditions_max": 24
1258                }
1259                "#
1260                );
1261            },
1262        );
1263    }
1264
1265    #[test]
1266    fn bash_assignments_only() {
1267        check_metrics::<BashParser>(
1268            "f() {
1269                 a=1
1270                 b=2
1271                 c+=3
1272             }",
1273            "foo.sh",
1274            |metric| {
1275                insta::assert_json_snapshot!(
1276                    metric.abc,
1277                    @r#"
1278                {
1279                  "assignments": 3,
1280                  "branches": 0,
1281                  "conditions": 0,
1282                  "magnitude": 3.0,
1283                  "value": 0.0,
1284                  "assignments_average": 1.5,
1285                  "branches_average": 0.0,
1286                  "conditions_average": 0.0,
1287                  "assignments_min": 0,
1288                  "assignments_max": 3,
1289                  "branches_min": 0,
1290                  "branches_max": 0,
1291                  "conditions_min": 0,
1292                  "conditions_max": 0
1293                }
1294                "#
1295                );
1296            },
1297        );
1298    }
1299
1300    #[test]
1301    fn bash_commands_only() {
1302        check_metrics::<BashParser>(
1303            "f() {
1304                 echo a
1305                 ls
1306             }",
1307            "foo.sh",
1308            |metric| {
1309                insta::assert_json_snapshot!(
1310                    metric.abc,
1311                    @r#"
1312                {
1313                  "assignments": 0,
1314                  "branches": 2,
1315                  "conditions": 0,
1316                  "magnitude": 2.0,
1317                  "value": 0.0,
1318                  "assignments_average": 0.0,
1319                  "branches_average": 1.0,
1320                  "conditions_average": 0.0,
1321                  "assignments_min": 0,
1322                  "assignments_max": 0,
1323                  "branches_min": 0,
1324                  "branches_max": 2,
1325                  "conditions_min": 0,
1326                  "conditions_max": 0
1327                }
1328                "#
1329                );
1330            },
1331        );
1332    }
1333
1334    #[test]
1335    fn bash_control_flow_counts_conditions() {
1336        // Regression for #696: Bash control-flow branches are ABC
1337        // conditions (a Bash predicate is a command, so the branch keyword
1338        // is the only condition signal). Each mirrors a cyclomatic decision.
1339        //
1340        // expected: 4 conditions — `if` (1) + `elif` (1) + `while` (1) +
1341        // the non-wildcard case arm `a)` (1). The bare-`*)` wildcard arm is
1342        // the Bash analogue of `default:` and is excluded, exactly as the
1343        // cyclomatic standard count excludes it. No comparison / test
1344        // operators appear, so every condition here is control-flow.
1345        check_metrics::<BashParser>(
1346            "f() {
1347                 if cmd; then
1348                     echo a
1349                 elif other; then
1350                     echo b
1351                 fi
1352                 while running; do
1353                     echo c
1354                 done
1355                 case \"$x\" in
1356                     a) echo d ;;
1357                     *) echo e ;;
1358                 esac
1359             }",
1360            "foo.sh",
1361            |metric| {
1362                assert_eq!(metric.abc.conditions_sum(), 4);
1363            },
1364        );
1365    }
1366
1367    #[test]
1368    fn bash_conditions_mix() {
1369        // Exercises every condition path: `==` and `!=` inside `[[ ]]`,
1370        // arithmetic `<` inside `(( ))`, and the prefix `-z` test operator
1371        // inside `[ ]`. Each `if` body's `echo` contributes a branch.
1372        //
1373        // expected: 8 conditions — each of the four `if`s contributes one
1374        // for the control-flow branch (#696) plus one for its comparison /
1375        // test operator (`==`, `!=`, `<`, `-z`). 4 branches (one `echo`
1376        // each). magnitude = sqrt(4² + 8²) = sqrt(80).
1377        check_metrics::<BashParser>(
1378            "f() {
1379                 if [[ \"$a\" == \"$b\" ]]; then
1380                     echo eq
1381                 fi
1382                 if [[ \"$x\" != \"$y\" ]]; then
1383                     echo ne
1384                 fi
1385                 if (( $a < $b )); then
1386                     echo lt
1387                 fi
1388                 if [ -z \"$x\" ]; then
1389                     echo empty
1390                 fi
1391             }",
1392            "foo.sh",
1393            |metric| {
1394                assert_eq!(metric.abc.conditions_sum(), 8);
1395                assert_eq!(metric.abc.branches_sum(), 4);
1396                insta::assert_json_snapshot!(
1397                    metric.abc,
1398                    @r#"
1399                {
1400                  "assignments": 0,
1401                  "branches": 4,
1402                  "conditions": 8,
1403                  "magnitude": 8.94427190999916,
1404                  "value": 0.0,
1405                  "assignments_average": 0.0,
1406                  "branches_average": 2.0,
1407                  "conditions_average": 4.0,
1408                  "assignments_min": 0,
1409                  "assignments_max": 0,
1410                  "branches_min": 0,
1411                  "branches_max": 4,
1412                  "conditions_min": 0,
1413                  "conditions_max": 8
1414                }
1415                "#
1416                );
1417            },
1418        );
1419    }
1420
1421    #[test]
1422    fn bash_redirection_is_not_a_condition() {
1423        // `>` and `<` spell an I/O redirection as well as a comparison, and
1424        // the grammar parents the redirection under `file_redirect` rather
1425        // than `binary_expression`. Ungated, every redirect in a script
1426        // scored a condition: this fixture measured 2 before the parent
1427        // gate — the Bash instance of #1280's positive-parent polarity.
1428        // expected: 0 conditions — no test, no `if`, no comparison.
1429        check_metrics::<BashParser>(
1430            "f() {\n  echo hi > out.txt\n  read x < in.txt\n}\n",
1431            "foo.sh",
1432            |metric| {
1433                assert_eq!(metric.abc.conditions_sum(), 0);
1434                // The two `echo` / `read` commands still count as branches,
1435                // so the zero above is the gate firing rather than the walk
1436                // skipping the function body.
1437                assert_eq!(metric.abc.branches_sum(), 2);
1438            },
1439        );
1440    }
1441
1442    #[test]
1443    fn bash_comparison_inside_an_arithmetic_or_test_context_is_a_condition() {
1444        // The positive control for the gate above: the same tokens under a
1445        // `binary_expression` are real comparisons in both the `[[ … ]]`
1446        // test form and the `(( … ))` arithmetic form, and still count.
1447        // expected: 4 — one `if` control-flow condition and one `>` per
1448        // function.
1449        check_metrics::<BashParser>(
1450            "f() {\n  if [[ $a > $b ]]; then :; fi\n}\ng() {\n  if (( a > b )); then :; fi\n}\n",
1451            "foo.sh",
1452            |metric| {
1453                assert_eq!(metric.abc.conditions_sum(), 4);
1454            },
1455        );
1456    }
1457
1458    #[test]
1459    fn bash_arithmetic_ternary_is_a_condition() {
1460        // The ABC half of #1268. Cyclomatic and cognitive both count Bash's
1461        // only ternary form; ABC did not, so the identical construct scored
1462        // 1 here against the C family's 2.
1463        // expected: 2 — the `>` comparison and the ternary itself, matching
1464        // `int m = a > b ? a : b;` in C.
1465        check_metrics::<BashParser>(
1466            "f() {\n  local m=$(( a > b ? a : b ))\n}\n",
1467            "foo.sh",
1468            |metric| {
1469                assert_eq!(metric.abc.conditions_sum(), 2);
1470            },
1471        );
1472    }
1473
1474    #[test]
1475    fn bash_magnitude() {
1476        // Combined assignments + branches + conditions. The single `if`
1477        // contributes two conditions (the control-flow branch, #696, plus
1478        // the `==` operator), so magnitude = sqrt(2² + 1² + 2²) = sqrt(9).
1479        check_metrics::<BashParser>(
1480            "f() {
1481                 a=1
1482                 b=2
1483                 if [[ \"$a\" == \"$b\" ]]; then
1484                     echo eq
1485                 fi
1486             }",
1487            "foo.sh",
1488            |metric| {
1489                assert_eq!(metric.abc.conditions_sum(), 2);
1490                insta::assert_json_snapshot!(
1491                    metric.abc,
1492                    @r#"
1493                {
1494                  "assignments": 2,
1495                  "branches": 1,
1496                  "conditions": 2,
1497                  "magnitude": 3.0,
1498                  "value": 0.0,
1499                  "assignments_average": 1.0,
1500                  "branches_average": 0.5,
1501                  "conditions_average": 1.0,
1502                  "assignments_min": 0,
1503                  "assignments_max": 2,
1504                  "branches_min": 0,
1505                  "branches_max": 1,
1506                  "conditions_min": 0,
1507                  "conditions_max": 2
1508                }
1509                "#
1510                );
1511            },
1512        );
1513    }
1514
1515    #[test]
1516    fn java_malformed_parenthesized_no_panic() {
1517        check_metrics::<JavaParser>("class A { void m() { if (( }) }", "foo.java", |metric| {
1518            // tree-sitter emits ERROR nodes for this malformed source, so no
1519            // IfStatement, branch, or condition is recognised — all counts are 0.
1520            // Primary goal: the unwrap-free path does not panic.
1521            assert_eq!(metric.abc.assignments(), 0);
1522            assert_eq!(metric.abc.branches(), 0);
1523            assert_eq!(metric.abc.conditions(), 0);
1524            assert_eq!(metric.abc.magnitude(), 0.0);
1525        });
1526    }
1527
1528    #[test]
1529    fn java_bool_returning_terminal_kinds_count() {
1530        // Companion to `csharp_bool_returning_terminal_kinds_count`
1531        // (issue #372 / lesson #19). Java's grammar wraps every
1532        // if/while/do condition in `parenthesized_expression`, so
1533        // the gap lived in `java_inspect_container`'s terminal-arm
1534        // recognizer: `FieldAccess` (`cfg.flag`), `CastExpression`
1535        // (`(boolean)v`), `ArrayAccess` (`flags[0]`), and
1536        // `InstanceofExpression` (`x instanceof Foo`) were never
1537        // counted. Java has no `await` or `is_pattern` analogues,
1538        // so the C# fix's five-kind set collapses to four here.
1539        //
1540        // expected: 4 conditions (one per `if`), 0 assignments,
1541        // 0 branches (no invocations).
1542        check_metrics::<JavaParser>(
1543            "class Cfg { boolean flag; }
1544            class A {
1545                void m(Object v, boolean[] flags, Cfg cfg) {
1546                    if (cfg.flag) { }
1547                    if ((boolean) v) { }
1548                    if (v instanceof Cfg) { }
1549                    if (flags[0]) { }
1550                }
1551            }",
1552            "foo.java",
1553            |metric| {
1554                assert_eq!(metric.abc.conditions_sum(), 4);
1555                assert_eq!(metric.abc.assignments_sum(), 0);
1556                assert_eq!(metric.abc.branches_sum(), 0);
1557            },
1558        );
1559    }
1560
1561    // Issue #1274: a generic *declaration* is type syntax, not a
1562    // decision. `java_count_token_condition` denied only
1563    // `type_arguments`, so `class Gen<T>` and `<U> U ident(U x)` — both
1564    // `type_parameters` — each scored two conditions. The fix counts
1565    // `<` / `>` only under `binary_expression`, the polarity C / C++ /
1566    // Rust / Go already use; a `grammar.json` sweep proves the token is
1567    // emitted from exactly three productions, so the two shapes agree on
1568    // every well-formed input.
1569    //
1570    // One fixture per production the arm must ignore: the class-level
1571    // `type_parameters` (`Gen<T …>`), its `extends` bound's nested
1572    // `type_arguments` (`Comparable<T>`), the method-level
1573    // `type_parameters` (`<U>`), and a nested generic
1574    // (`Map<String, List<T>>`) whose two closing `>` lex as separate
1575    // tokens under separate `type_arguments`. Pre-fix this file scored
1576    // 4 conditions — two per `type_parameters` bracket pair; expected 0,
1577    // the file contains no conditional construct at all.
1578    #[test]
1579    fn java_generic_declarations_are_not_conditions() {
1580        check_metrics::<JavaParser>(
1581            "class Gen<T extends Comparable<T>> {
1582                <U> U ident(U x) { return x; }
1583                List<T> pick(Map<String, List<T>> m) { return m.get(\"k\"); }
1584            }",
1585            "foo.java",
1586            |metric| {
1587                assert_eq!(metric.abc.conditions_sum(), 0);
1588                // Non-vacuity guard: 0 is also what an unparsed file
1589                // scores, so pin a value only a walked body can produce
1590                // — the `m.get("k")` invocation.
1591                assert_eq!(metric.abc.branches_sum(), 1);
1592            },
1593        );
1594    }
1595
1596    // Second half of #1274, same premise and same match: a wildcard type
1597    // argument's `?` is type syntax, not a ternary. tree-sitter-java emits
1598    // a bare `?` from exactly two productions — `ternary_expression` and
1599    // `wildcard` — so the `QMARK` arm carries the same allowlist gate the
1600    // `<` / `>` arm does.
1601    //
1602    // The body carries a real ternary over a real comparison so the
1603    // expected total is 2, not 0. Asserting 0 on a wildcard-only fixture
1604    // would have been vacuous — an unparsable file scores 0 too, so the
1605    // test could not tell "the wildcard was ignored" from "the fixture
1606    // stopped being recognised". Each failure mode now lands on its own
1607    // number: 4 if the wildcard `?` counts again (the pre-fix value), 3
1608    // if the gate is aimed at `Wildcard` instead, 1 if it swallows the
1609    // genuine ternary, 0 if parsing breaks. Two wildcards against one
1610    // ternary is deliberate — with one of each, aiming the gate at the
1611    // wrong one of the two productions still totals 2 and the test
1612    // cannot see the difference.
1613    #[test]
1614    fn java_generic_wildcard_is_not_a_condition() {
1615        check_metrics::<JavaParser>(
1616            "class A {
1617                int m(List<? extends Number> xs, Map<String, ? extends Number> ys,
1618                      int a, int b) { return a < b ? 1 : 2; }
1619            }",
1620            "foo.java",
1621            |metric| {
1622                assert_eq!(metric.abc.conditions_sum(), 2);
1623            },
1624        );
1625    }
1626
1627    // The other half of #1274: narrowing the `<` / `>` arm must not
1628    // swallow real comparisons. `List<String>` in the signature keeps a
1629    // generic in scope so the fixture proves both directions at once,
1630    // and the assertion is the grammar-dispatch §8 pin — on the
1631    // method's own space the ABC condition count must equal the
1632    // cyclomatic decision count (`cyclomatic()` minus the per-space
1633    // base of 1). Both are 2, one per `if`.
1634    #[test]
1635    fn java_comparison_operators_still_count_alongside_generics() {
1636        check_func_space::<JavaParser, _>(
1637            "class A {
1638                int m(List<String> xs, int a, int b) {
1639                    if (a < b) { return 1; }
1640                    if (a > b) { return 2; }
1641                    return 0;
1642                }
1643            }",
1644            "foo.java",
1645            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
1646        );
1647    }
1648
1649    #[test]
1650    fn java_constructor_delegation_is_a_branch() {
1651        // Regression for #1279: `super(…)` / `this(…)` parse as
1652        // `explicit_constructor_invocation`, not `method_invocation`, so
1653        // each scored zero branches while Groovy scored one for identical
1654        // source. Both constructors delegate, and neither body contains any
1655        // other call.
1656        // expected: 2 branches — one delegation each.
1657        check_metrics::<JavaParser>(
1658            "class Sub extends Base {
1659                Sub() { super(1); }
1660                Sub(int a) { this(); }
1661            }",
1662            "foo.java",
1663            |metric| {
1664                assert_eq!(metric.abc.branches_sum(), 2);
1665            },
1666        );
1667    }
1668
1669    #[test]
1670    fn java_constructor_delegation_does_not_double_count_arguments() {
1671        // The delegation node does not wrap a `method_invocation` for the
1672        // call itself, so `super(f())` is exactly two branches — the
1673        // delegation and the argument call — not three (#1279).
1674        // expected: 2 branches.
1675        check_metrics::<JavaParser>(
1676            "class Sub extends Base {
1677                Sub() { super(f()); }
1678            }",
1679            "foo.java",
1680            |metric| {
1681                assert_eq!(metric.abc.branches_sum(), 2);
1682            },
1683        );
1684    }
1685
1686    #[test]
1687    fn csharp_constructor_initializer_is_a_branch() {
1688        // C# spells the same delegation as a `constructor_initializer`
1689        // (`: base(…)` / `: this(…)`), which likewise scored zero (#1279).
1690        // expected: 2 branches — one initializer each, no other calls.
1691        check_metrics::<CsharpParser>(
1692            "class Sub : Base {
1693                Sub() : base(1) { }
1694                Sub(int a) : this() { }
1695            }",
1696            "foo.cs",
1697            |metric| {
1698                assert_eq!(metric.abc.branches_sum(), 2);
1699            },
1700        );
1701    }
1702
1703    #[test]
1704    fn kotlin_constructor_delegation_is_a_branch() {
1705        // Kotlin's secondary-constructor delegation is a
1706        // `constructor_delegation_call`, a distinct production from
1707        // `CallExpression`, so it scored zero for the same reason (#1279).
1708        // expected: 1 branch — the `: super(x)` delegation.
1709        check_metrics::<KotlinParser>(
1710            "class Sub : Base {
1711                constructor(x: Int) : super(x) { }
1712            }",
1713            "foo.kt",
1714            |metric| {
1715                assert_eq!(metric.abc.branches_sum(), 1);
1716            },
1717        );
1718    }
1719
1720    #[test]
1721    fn groovy_constructor_delegation_is_a_branch() {
1722        // Groovy already counted this shape before #1279; the assertion
1723        // pins the JVM-family parity the Java and Kotlin fixes restore.
1724        // expected: 1 branch — the `super(1)` delegation.
1725        check_metrics::<GroovyParser>(
1726            "class Sub extends Base {
1727                Sub() { super(1) }
1728            }",
1729            "foo.groovy",
1730            |metric| {
1731                assert_eq!(metric.abc.branches_sum(), 1);
1732            },
1733        );
1734    }
1735
1736    #[test]
1737    fn groovy_no_abc() {
1738        // Comment-only file has no executable code → all-zero ABC.
1739        check_metrics::<GroovyParser>(
1740            "// just a comment, no executable code",
1741            "foo.groovy",
1742            |metric| {
1743                assert_eq!(metric.abc.assignments_sum(), 0);
1744                assert_eq!(metric.abc.branches_sum(), 0);
1745                assert_eq!(metric.abc.conditions_sum(), 0);
1746            },
1747        );
1748    }
1749
1750    #[test]
1751    fn groovy_single_assignment() {
1752        // `int x = 1` is a local-variable declaration whose `=` counts
1753        // as one assignment (matches Java's semantics).
1754        check_metrics::<GroovyParser>("int x = 1", "foo.groovy", |metric| {
1755            assert_eq!(metric.abc.assignments_sum(), 1);
1756            assert_eq!(metric.abc.branches_sum(), 0);
1757            assert_eq!(metric.abc.conditions_sum(), 0);
1758        });
1759    }
1760
1761    #[test]
1762    fn groovy_assignments() {
1763        check_metrics::<GroovyParser>(
1764            "void f() {
1765                int a = 1
1766                int b = 2
1767                a = 3
1768                b = 4
1769                a += 1
1770                b -= 1
1771            }",
1772            "foo.groovy",
1773            |metric| {
1774                // Six `=` tokens total. The two `Final`-less local
1775                // var-decls (`int a = 1`, `int b = 2`) and the two
1776                // bare assignments (`a = 3`, `b = 4`) each contribute
1777                // one assignment via the `EQ` arm; the `+=` / `-=`
1778                // each contribute one via the compound-assign arm.
1779                assert_eq!(metric.abc.assignments_sum(), 6);
1780            },
1781        );
1782    }
1783
1784    #[test]
1785    fn groovy_branches() {
1786        check_metrics::<GroovyParser>(
1787            "void f() {
1788                doStuff()
1789                helper.invoke()
1790                new Worker()
1791            }",
1792            "foo.groovy",
1793            |metric| {
1794                // 2 method invocations + 1 object creation = 3 branches
1795                assert_eq!(metric.abc.branches_sum(), 3);
1796            },
1797        );
1798    }
1799
1800    #[test]
1801    fn groovy_conditions_in_if() {
1802        check_metrics::<GroovyParser>(
1803            "void f(int a) {
1804                if (a == 0) { println(a) }
1805                if (a >= 1) { println(a) }
1806                if (a != 2) { println(a) }
1807            }",
1808            "foo.groovy",
1809            |metric| {
1810                // Three relational ops = 3 conditions
1811                assert_eq!(metric.abc.conditions_sum(), 3);
1812            },
1813        );
1814    }
1815
1816    #[test]
1817    fn groovy_branches_with_juxt_call() {
1818        // Groovy's parens-less call form `println foo` must be counted
1819        // as a branch (`JuxtFunctionCall`).
1820        check_metrics::<GroovyParser>(
1821            "void f() {
1822                println 'hi'
1823                println 'bye'
1824            }",
1825            "foo.groovy",
1826            |metric| {
1827                // 2 juxt calls = 2 branches.
1828                assert_eq!(metric.abc.branches_sum(), 2);
1829            },
1830        );
1831    }
1832
1833    #[test]
1834    fn groovy_try_catch_conditions() {
1835        // Each `try` and `catch` keyword token contributes +1 to
1836        // conditions (mirrors Java).
1837        check_metrics::<GroovyParser>(
1838            "void f() {
1839                try {
1840                    risky()
1841                } catch (Exception e) {
1842                    handle(e)
1843                }
1844            }",
1845            "foo.groovy",
1846            |metric| {
1847                // try + catch = 2 conditions
1848                assert_eq!(metric.abc.conditions_sum(), 2);
1849            },
1850        );
1851    }
1852
1853    #[test]
1854    fn groovy_ternary_conditions() {
1855        check_metrics::<GroovyParser>(
1856            "void f(int x) {
1857                def y = x > 0 ? 1 : 2
1858            }",
1859            "foo.groovy",
1860            |metric| {
1861                // QMARK alone is +1 condition, plus the `>` condition = 2.
1862                assert_eq!(metric.abc.conditions_sum(), 2);
1863            },
1864        );
1865    }
1866
1867    #[test]
1868    fn groovy_constant_excluded_from_assignments() {
1869        // `final` declarations are not counted as assignments
1870        // (mirrors Java's `Final` handling).
1871        check_metrics::<GroovyParser>(
1872            "class A {
1873                final int CONST = 42
1874                int field = 0
1875            }",
1876            "foo.groovy",
1877            |metric| {
1878                // The `=` on `final int CONST = 42` is a constant
1879                // initialiser (skipped). Only `field = 0` counts.
1880                assert_eq!(metric.abc.assignments_sum(), 1);
1881            },
1882        );
1883    }
1884
1885    #[test]
1886    fn groovy_malformed_parenthesized_no_panic() {
1887        // Regression: malformed Groovy input must not panic the ABC
1888        // walker; the `spaces.rs` Unit fallback (lesson 9) covers
1889        // structural recovery. amaanq's grammar treats `def x = (((`
1890        // as a `local_variable_declaration` whose initialiser is the
1891        // first opening paren — the `=` still fires the assignment
1892        // arm.
1893        check_metrics::<GroovyParser>("def x = (((", "foo.groovy", |metric| {
1894            assert_eq!(metric.abc.assignments_sum(), 1);
1895        });
1896    }
1897
1898    #[test]
1899    fn groovy_bool_returning_terminal_kinds_count() {
1900        // Companion to `csharp_bool_returning_terminal_kinds_count`
1901        // (issue #372 / lesson #19). The dekobon Groovy grammar
1902        // shares Java's wrapping conventions for `FieldAccess` and
1903        // `InstanceofExpression`, but it splits casts into two
1904        // distinct kinds — `cast_expression` for the Groovy-idiomatic
1905        // `v as Boolean` and `parenthesized_type_cast` for the
1906        // Java-style `(boolean) v`. The grammar has no `await` or
1907        // `array_access` analogues, so the C# fix's five-kind set
1908        // collapses to four here (with the cast slot doubled).
1909        //
1910        // expected: 4 conditions (one per `if`), 0 assignments,
1911        // 0 branches (no invocations).
1912        check_metrics::<GroovyParser>(
1913            "class Cfg { boolean flag }
1914            class A {
1915                void m(Object v, Cfg cfg) {
1916                    if (cfg.flag) { }
1917                    if ((boolean) v) { }
1918                    if (v as Boolean) { }
1919                    if (v instanceof Cfg) { }
1920                }
1921            }",
1922            "foo.groovy",
1923            |metric| {
1924                assert_eq!(metric.abc.conditions_sum(), 4);
1925                assert_eq!(metric.abc.assignments_sum(), 0);
1926                assert_eq!(metric.abc.branches_sum(), 0);
1927            },
1928        );
1929    }
1930
1931    // Groovy half of #1274 — see `java_generic_declarations_are_not_conditions`.
1932    // The class-level `type_parameters` and its bound's nested
1933    // `type_arguments` mirror the Java fixture; pre-fix this file scored
1934    // 2 conditions, expected 0.
1935    #[test]
1936    fn groovy_generic_declarations_are_not_conditions() {
1937        check_metrics::<GroovyParser>(
1938            "class Gen<T extends Comparable<T>> {
1939                List<T> pick(Map<String, List<T>> m) { m.get(\"k\") }
1940            }",
1941            "foo.groovy",
1942            |metric| {
1943                assert_eq!(metric.abc.conditions_sum(), 0);
1944                // Non-vacuity guard — see the Java counterpart.
1945                assert_eq!(metric.abc.branches_sum(), 1);
1946            },
1947        );
1948    }
1949
1950    // Groovy counterpart of `java_generic_wildcard_is_not_a_condition`,
1951    // including its choice of a non-zero expected total and its
1952    // two-wildcards-one-ternary shape; the dekobon grammar emits the
1953    // same `wildcard` node. Pre-fix: 4.
1954    #[test]
1955    fn groovy_generic_wildcard_is_not_a_condition() {
1956        check_metrics::<GroovyParser>(
1957            "class A {
1958                int m(List<? extends Number> xs, Map<String, ? extends Number> ys,
1959                      int a, int b) { a < b ? 1 : 2 }
1960            }",
1961            "foo.groovy",
1962            |metric| {
1963                assert_eq!(metric.abc.conditions_sum(), 2);
1964            },
1965        );
1966    }
1967
1968    // A generic *method* gets its own fixture because the dekobon
1969    // grammar gives it its own production: `def <U> U ident(U x)` emits
1970    // `method_type_parameters`, not the `type_parameters` the class form
1971    // above uses. It is the fourth and last production from which that
1972    // grammar emits a bare `<` / `>` (the others being
1973    // `binary_expression`, `type_arguments` and `type_parameters`), and
1974    // the one a denylist extended only with `TypeParameters` — the fix
1975    // this issue's own plan proposed — would still miss. Revert-verified
1976    // in both directions: under that denylist this test still fails
1977    // while `groovy_generic_declarations_are_not_conditions` passes.
1978    //
1979    // The body carries a ternary over a comparison for the same
1980    // non-vacuity reason as the wildcard tests: expected 2, pre-fix 4
1981    // (the `<U>` bracket pair), 0 if the fixture stops parsing.
1982    #[test]
1983    fn groovy_method_type_parameters_are_not_conditions() {
1984        check_metrics::<GroovyParser>(
1985            "class A {
1986                def <U> U ident(U x, int a, int b) { a < b ? x : null }
1987            }",
1988            "foo.groovy",
1989            |metric| {
1990                assert_eq!(metric.abc.conditions_sum(), 2);
1991            },
1992        );
1993    }
1994
1995    // Groovy counterpart of
1996    // `java_comparison_operators_still_count_alongside_generics`: the
1997    // narrowed arm keeps counting real comparisons, pinned against the
1998    // cyclomatic decision count on the method's own space (§8).
1999    #[test]
2000    fn groovy_elvis_counts_one_condition_per_token() {
2001        // `a ?: c` is a short-circuit decision Groovy cyclomatic already
2002        // counts and the Kotlin ABC arm counts for its identical token;
2003        // Groovy's arm listed neither `?:` nor its `elvis_expression`, so
2004        // a method whose only branching is elvis chains reported
2005        // cyclomatic > 1 with zero conditions. One per token, so the
2006        // grammar-dispatch §8 invariant holds on the chain:
2007        // `conditions() == cyclomatic() - 1`.
2008        check_func_space::<GroovyParser, _>(
2009            "class K { def f(a, c) { return a ?: c } }",
2010            "foo.groovy",
2011            |space| assert_deepest_conditions_match_cyclomatic(&space, 1),
2012        );
2013        check_func_space::<GroovyParser, _>(
2014            "class K { def g(a, b, c) { return a ?: b ?: c } }",
2015            "foo.groovy",
2016            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
2017        );
2018    }
2019
2020    #[test]
2021    fn groovy_comparison_operators_still_count_alongside_generics() {
2022        check_func_space::<GroovyParser, _>(
2023            "class A {
2024                int m(List<String> xs, int a, int b) {
2025                    if (a < b) { return 1 }
2026                    if (a > b) { return 2 }
2027                    return 0
2028                }
2029            }",
2030            "foo.groovy",
2031            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
2032        );
2033    }
2034
2035    #[test]
2036    fn groovy_if_multiple_conditions() {
2037        // Mirrors `java_if_multiple_conditions`: `&&` / `||` chains
2038        // and parenthesised unary forms each contribute one
2039        // condition per primitive comparison; the inspect-container
2040        // pass picks up the unary `!a` / `!b` arguments inside the
2041        // `BinaryExpression` and counts them too.
2042        check_metrics::<GroovyParser>(
2043            "void f(boolean a, boolean b, boolean c) {
2044                if (a || b || c) { println(a) }
2045                if (a && b && c) { println(a) }
2046                if (!a && !b) { println(a) }
2047            }",
2048            "foo.groovy",
2049            |metric| {
2050                // Conditions counted via the AMPAMP/PIPEPIPE arms
2051                // (one count per identifier in the chain — three
2052                // for `||`, three for `&&`, two for the unary chain)
2053                // = 8.
2054                assert_eq!(metric.abc.conditions_sum(), 8);
2055                // Three `println a` juxt calls — each is a branch.
2056                assert_eq!(metric.abc.branches_sum(), 3);
2057            },
2058        );
2059    }
2060
2061    #[test]
2062    fn groovy_while_and_do_while_conditions() {
2063        // Covers the WhileStatement and DoStatement arms in
2064        // `impl Abc for GroovyCode`. Each `while` / `do-while` has
2065        // its condition inspected through `groovy_inspect_container`.
2066        check_metrics::<GroovyParser>(
2067            "void f(boolean a, boolean b) {
2068                while (a) {
2069                    a = false
2070                }
2071                do {
2072                    b = !b
2073                } while (b)
2074            }",
2075            "foo.groovy",
2076            |metric| {
2077                // `while(a)` + `while(b)` each contribute one condition;
2078                // the unary `!b` on the do body's right-hand side adds
2079                // one more via the assignment-arm inspection = 3.
2080                assert_eq!(metric.abc.conditions_sum(), 3);
2081                // Two assignments to existing variables (`a = false`,
2082                // `b = !b`).
2083                assert_eq!(metric.abc.assignments_sum(), 2);
2084            },
2085        );
2086    }
2087
2088    #[test]
2089    fn groovy_if_while_boolean_literal_condition() {
2090        // Regression for the Groovy half of #371-class bugs: the
2091        // dekobon tree-sitter-groovy grammar wraps a bare
2092        // `true` / `false` literal used as the condition of
2093        // `if` / `while` / `do` / `?:` in a `boolean_literal` node
2094        // (`Groovy::BooleanLiteral`, kind_id 270), not the leaf
2095        // `True` / `False` keyword tokens. `groovy_count_condition`
2096        // must therefore match `BooleanLiteral` (the wrapper).
2097        // Without that, every literal-condition statement silently
2098        // scored 0 conditions. Mirror of
2099        // `csharp_if_while_boolean_literal_condition`.
2100        check_metrics::<GroovyParser>(
2101            "void m() {
2102                if (true) { println 'a' }
2103                if (false) { println 'b' }
2104                while (true) { break }
2105                int t = true ? 1 : 0
2106            }",
2107            "foo.groovy",
2108            |metric| {
2109                // Four literal-condition statements contribute 4
2110                // `BooleanLiteral` conditions (if / if / while /
2111                // ternary), plus the ternary's `?` token adds one
2112                // more via `groovy_count_token_condition` → 5
2113                // total. The `println` calls contribute 2 branches
2114                // (the `while` body's `break` is not a branch).
2115                // The `int t = …` initializer contributes 1
2116                // assignment.
2117                assert_eq!(metric.abc.conditions_sum(), 5);
2118                assert_eq!(metric.abc.branches_sum(), 2);
2119                assert_eq!(metric.abc.assignments_sum(), 1);
2120            },
2121        );
2122    }
2123
2124    #[test]
2125    fn groovy_return_unary_boolean_literal() {
2126        // Companion to `groovy_if_while_boolean_literal_condition`:
2127        // a `!true` / `!false` operand inside a `return` statement
2128        // routes through `groovy_inspect_container` (via
2129        // `groovy_inspect_child(node, 1)` on the ReturnStatement).
2130        // The `!` operator establishes boolean context, then the
2131        // innermost-operand check matches `BooleanLiteral` — that
2132        // helper's `BooleanLiteral` arm must be present or the
2133        // count silently drops. Mutation-verified: removing
2134        // `BooleanLiteral` from `groovy_inspect_container` leaves
2135        // every other Groovy test passing.
2136        check_metrics::<GroovyParser>(
2137            "boolean f() {
2138                return !true
2139            }
2140            boolean g() {
2141                return !false
2142            }",
2143            "foo.groovy",
2144            |metric| {
2145                // Each `return !X` walks into
2146                // `groovy_inspect_container` with a UnaryExpression
2147                // wrapping a `BANG` + BooleanLiteral. The `!` arm
2148                // seeds `has_boolean_content = true` (ReturnStatement
2149                // is not a known-boolean parent), then the
2150                // BooleanLiteral operand contributes one condition.
2151                // Two `return !X` → 2 conditions, no branches, no
2152                // assignments.
2153                assert_eq!(metric.abc.conditions_sum(), 2);
2154                assert_eq!(metric.abc.branches_sum(), 0);
2155                assert_eq!(metric.abc.assignments_sum(), 0);
2156            },
2157        );
2158    }
2159
2160    #[test]
2161    fn groovy_short_circuit_with_boolean_literal_operand() {
2162        // Companion to `groovy_if_while_boolean_literal_condition`:
2163        // a bare `true` / `false` operand of `&&` / `||` lands in
2164        // `groovy_count_unary_conditions`, which iterates the
2165        // parent BinaryExpression's children. That helper must
2166        // match the `BooleanLiteral` wrapper just like
2167        // `groovy_count_condition` does — otherwise the operand
2168        // silently scores zero. Mutation-verified: removing
2169        // `BooleanLiteral` from the `groovy_count_unary_conditions`
2170        // arm leaves every other Groovy test passing.
2171        check_metrics::<GroovyParser>(
2172            "void m(boolean x) {
2173                if (x && true) { println 'a' }
2174                if (false || x) { println 'b' }
2175            }",
2176            "foo.groovy",
2177            |metric| {
2178                // `&&` and `||` themselves are NOT in
2179                // `groovy_count_token_condition`'s match list —
2180                // they route through
2181                // `groovy_walk_for_conditions::AMPAMP|PIPEPIPE`,
2182                // which calls `groovy_count_unary_conditions` on
2183                // the parent BinaryExpression. Each invocation
2184                // counts every child that matches the terminal-
2185                // operand kinds and whose parent is a
2186                // BinaryExpression. For `x && true`: Identifier x
2187                // (+1) + BooleanLiteral true (+1) = 2. For
2188                // `false || x`: BooleanLiteral false (+1) +
2189                // Identifier x (+1) = 2. Total 4.
2190                assert_eq!(metric.abc.conditions_sum(), 4);
2191                assert_eq!(metric.abc.branches_sum(), 2);
2192                assert_eq!(metric.abc.assignments_sum(), 0);
2193            },
2194        );
2195    }
2196
2197    #[test]
2198    fn groovy_methods_arguments_with_conditions() {
2199        // Mirror of `java_methods_arguments_with_conditions`: a
2200        // unary `!x` inside an argument list must count both the
2201        // method invocation as a branch AND the unary as a
2202        // condition. The `ArgumentList | ArgumentList2` arm in
2203        // `impl Abc for GroovyCode` is what exercises this.
2204        check_metrics::<GroovyParser>(
2205            "void f(boolean a, boolean b, boolean c) {
2206                m1(a)
2207                m1(!a)
2208                m2(!a, !b)
2209            }",
2210            "foo.groovy",
2211            |metric| {
2212                // 3 method invocations (m1, m1, m2) — each fires the
2213                // branches arm.
2214                assert_eq!(metric.abc.branches_sum(), 3);
2215                // Three `!` unaries — `m1(!a)` and the two args of
2216                // `m2(!a, !b)` — each contribute one condition via
2217                // the ArgumentList inspection.
2218                assert_eq!(metric.abc.conditions_sum(), 3);
2219            },
2220        );
2221    }
2222
2223    #[test]
2224    fn groovy_return_with_conditions() {
2225        // Mirror of `java_return_with_conditions`: a parenthesised
2226        // or unary expression inside `return` flows through the
2227        // `ReturnStatement` arm to `groovy_inspect_container`.
2228        check_metrics::<GroovyParser>(
2229            "boolean f(boolean a) {
2230                return (a)
2231            }
2232            boolean g(boolean a) {
2233                return !a
2234            }",
2235            "foo.groovy",
2236            |metric| {
2237                // Only one of the two return forms surfaces a
2238                // condition: `return !a` hits the UnaryExpression
2239                // path and adds one; `return (a)` reaches
2240                // `groovy_inspect_container` but the inner
2241                // identifier `a` is not in a boolean-context-firing
2242                // parent, so no condition is added.
2243                assert_eq!(metric.abc.conditions_sum(), 1);
2244            },
2245        );
2246    }
2247
2248    #[test]
2249    fn groovy_for_with_variable_declaration() {
2250        // Classical `for (int i = 0; cond; i++)` form. The init
2251        // slot's `int i = 0` is suppressed from assignments by the
2252        // `LocalVariableDeclaration` push/pop dance; the `i++` in
2253        // the update slot contributes one assignment via the
2254        // `PLUSPLUS` arm. The condition `i < 10` flows through the
2255        // `ForStatement` arm.
2256        check_metrics::<GroovyParser>(
2257            "void f() {
2258                for (int i = 0; i < 10; i++) {
2259                    println(i)
2260                }
2261            }",
2262            "foo.groovy",
2263            |metric| {
2264                // `int i = 0` fires the EQ arm + `i++` fires the
2265                // PLUSPLUS arm = 2 assignments.
2266                assert_eq!(metric.abc.assignments_sum(), 2);
2267                // `i < 10` is one condition (the LT arm).
2268                assert_eq!(metric.abc.conditions_sum(), 1);
2269            },
2270        );
2271    }
2272
2273    /// The existing `for` test uses `i < 10`, which the `LT` token arm
2274    /// counts on its own — `groovy_walk_for_statement` never
2275    /// contributes there, so it stayed uncovered. A bare-identifier
2276    /// condition has no comparison token, so the count can only come
2277    /// from the walker.
2278    #[test]
2279    fn groovy_for_with_bare_identifier_condition() {
2280        check_metrics::<GroovyParser>(
2281            "void f(boolean go) {
2282                for (int i = 0; go; i++) {
2283                    println(i)
2284                }
2285            }",
2286            "foo.groovy",
2287            |metric| {
2288                // `go` is the whole condition and counts once.
2289                assert_eq!(metric.abc.conditions_sum(), 1);
2290                // `int i = 0` (EQ) + `i++` (PLUSPLUS) = 2, as in
2291                // `groovy_for_with_variable_declaration`.
2292                assert_eq!(metric.abc.assignments_sum(), 2);
2293            },
2294        );
2295    }
2296
2297    /// The same slot with the initialiser hoisted out of the header.
2298    /// Under the pre-#1276 positional cascade this was a distinct code
2299    /// path — the condition moved from child(4) to child(3) — and the
2300    /// pair is kept as a shape guard now that the walker reads the
2301    /// `condition` field and cannot see the difference.
2302    #[test]
2303    fn groovy_for_with_empty_initializer_counts_the_condition() {
2304        check_metrics::<GroovyParser>(
2305            "void f(boolean go) {
2306                int i = 0
2307                for (; go; i++) {
2308                    println(i)
2309                }
2310            }",
2311            "foo.groovy",
2312            |metric| {
2313                assert_eq!(metric.abc.conditions_sum(), 1);
2314                // `int i = 0` (EQ) + `i++` (PLUSPLUS), as above — the
2315                // initialiser just moved out of the loop header.
2316                assert_eq!(metric.abc.assignments_sum(), 2);
2317            },
2318        );
2319    }
2320
2321    /// Issue #1276 changed Groovy's answer here, exactly as it changed
2322    /// Java's: the positional cascade counted a `;` / `)` landing at
2323    /// child(4) as a vacuously-true condition, so `for (;;)` scored
2324    /// one. An omitted test is not a decision, and every other impl
2325    /// scores it zero. See `java_empty_for_condition_counts_nothing`.
2326    #[test]
2327    fn groovy_empty_for_condition_counts_nothing() {
2328        check_metrics::<GroovyParser>("void f() { for (;;) { break } }", "foo.groovy", |metric| {
2329            assert_eq!(metric.abc.conditions_sum(), 0);
2330        });
2331        // The second spelling that moved. Unlike Java's, Groovy's
2332        // `local_variable_declaration` does not swallow its `;`, so the
2333        // old cascade found `;` at child(3) and `;` at child(4) here
2334        // too, and counted one.
2335        check_metrics::<GroovyParser>(
2336            "void f() { for (int i = 0; ; i++) { break } }",
2337            "foo.groovy",
2338            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
2339        );
2340    }
2341
2342    /// The cascade's other defect, shared with Java: a comment in the
2343    /// header shifted every child index, so the condition went unread.
2344    /// Reading the `condition` field cannot shift.
2345    #[test]
2346    fn groovy_for_condition_survives_a_header_comment() {
2347        check_metrics::<GroovyParser>(
2348            "void f(boolean go) { for (; /* n */ go; ) { break } }",
2349            "foo.groovy",
2350            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
2351        );
2352        // Unchanged control: the same loop without the comment.
2353        check_metrics::<GroovyParser>(
2354            "void f(boolean go) { for (; go; ) { break } }",
2355            "foo.groovy",
2356            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
2357        );
2358    }
2359
2360    /// C#'s `csharp_walk_for_statement` reads the loop condition off
2361    /// the named `condition` field and routes a parenthesised or
2362    /// `!`-prefixed one through `csharp_inspect_container`. Every other
2363    /// C# `for` test uses a comparison (`i < n`), which the `LT` token
2364    /// arm counts without entering the walker.
2365    #[test]
2366    fn csharp_for_with_negated_condition() {
2367        check_metrics::<CsharpParser>(
2368            "class A {
2369                void M(bool done) {
2370                    for (int i = 0; !done; i++) { System.Console.WriteLine(i); }
2371                }
2372            }",
2373            "foo.cs",
2374            |metric| {
2375                // `!done` unwraps to the `done` terminal: one condition,
2376                // and no comparison token to double-count it.
2377                assert_eq!(metric.abc.conditions_sum(), 1);
2378                // `int i = 0` + `i++`.
2379                assert_eq!(metric.abc.assignments_sum(), 2);
2380                assert_eq!(metric.abc.branches_sum(), 1);
2381            },
2382        );
2383    }
2384
2385    #[test]
2386    fn groovy_eq_arm_counts_outside_final_declarations() {
2387        // Bare reassignment of an already-declared variable: the `=`
2388        // belongs to no `final` declaration, so it counts. Mirrors
2389        // `java_eq_arm_counts_outside_final_declarations`.
2390        check_metrics::<GroovyParser>(
2391            "void f(int x) {
2392                x = 42
2393            }",
2394            "foo.groovy",
2395            |metric| {
2396                assert_eq!(metric.abc.assignments_sum(), 1);
2397                assert_eq!(metric.abc.branches_sum(), 0);
2398                assert_eq!(metric.abc.conditions_sum(), 0);
2399            },
2400        );
2401    }
2402
2403    #[test]
2404    fn groovy_final_field_initializer_does_not_suppress_the_closure_body() {
2405        // The Groovy spelling of
2406        // `java_final_initializer_does_not_suppress_the_assignments_inside_it`:
2407        // a `final` field's closure body opens no space, and the sentinel
2408        // stack suppressed its `x = 1` with the declarator's own `=`.
2409        check_metrics::<GroovyParser>(
2410            "class K {
2411                int x
2412                final Closure c = { x = 1 }
2413                Closure d = { x = 2 }
2414                final int q = 3
2415            }",
2416            "foo.groovy",
2417            |metric| {
2418                // `x = 1`, `d = {…}`, `x = 2`; the two `final`
2419                // initializers are suppressed. Pre-fix: 2.
2420                assert_eq!(metric.abc.assignments_sum(), 3);
2421            },
2422        );
2423    }
2424
2425    #[test]
2426    fn groovy_final_local_is_an_error_at_the_pinned_grammar() {
2427        // `groovy_eq_initializes_final_binding` lists
2428        // `local_variable_declaration` for symmetry with Java, but at
2429        // dekobon-tree-sitter-groovy 0.2.2 a `final` local never
2430        // reaches it: the parser emits an `ERROR` node for the
2431        // declaration, with or without a terminator, so both of its `=`
2432        // tokens count. This pins the grammar limitation so a bump that
2433        // starts parsing the local shows up here rather than as a silent
2434        // change in what the predicate suppresses.
2435        let source = b"class K { int x; def m() { final int a = 0; x = 1 } }";
2436        let parser = GroovyParser::new(
2437            source.to_vec(),
2438            &std::path::PathBuf::from("foo.groovy"),
2439            None,
2440        );
2441        assert!(
2442            ast_has_kind_id(&parser, u16::MAX),
2443            "a `final` local should still parse to an ERROR node; if the grammar \
2444             now accepts it, re-derive the local half of the predicate",
2445        );
2446        check_metrics::<GroovyParser>(
2447            "class K { int x; def m() { final int a = 0; x = 1 } }",
2448            "foo.groovy",
2449            |metric| {
2450                assert_eq!(metric.abc.assignments_sum(), 2);
2451            },
2452        );
2453    }
2454
2455    #[test]
2456    fn csharp_const_initializer_shapes() {
2457        // `csharp_eq_initializes_const_binding` reads the `const`
2458        // through the `modifier` node one hop above the
2459        // `variable_declaration`, for a local and a field alike, behind
2460        // other modifiers; `readonly` is not `const` and its initializer
2461        // counts. A lambda's body opens its own space in C#, so unlike
2462        // Java there was never a nested `=` to leak — the row pins that
2463        // the structural rule keeps the count the sentinel gave.
2464        check_metrics::<CsharpParser>(
2465            "class K {
2466                int x;
2467                private const int Q = 1;
2468                public static readonly int R = 2;
2469                void M() { const int q = 3; System.Action a = () => { x = 4; }; }
2470            }",
2471            "foo.cs",
2472            |metric| {
2473                // `R = 2`, `a = () => …`, `x = 4`.
2474                assert_eq!(metric.abc.assignments_sum(), 3);
2475            },
2476        );
2477    }
2478
2479    #[test]
2480    fn csharp_constant_declarations() {
2481        check_metrics::<CsharpParser>(
2482            "class A {
2483                private const int X1 = 0, Y1 = 0;
2484                public const float PI = 3.14f;
2485                const string HELLO = \"Hello,\";
2486                protected string world = \" world!\";
2487                public float e = 2.718f;
2488                private int x2 = 1, y2 = 2;
2489                void M() {
2490                    const int Z1 = 0, Z2 = 0, Z3 = 0;
2491                    const float T = 0.0f;
2492                    int z1 = 1, z2 = 2, z3 = 3;
2493                }
2494            }",
2495            "foo.cs",
2496            |metric| insta::assert_json_snapshot!(metric.abc),
2497        );
2498    }
2499
2500    #[test]
2501    fn csharp_declarations_with_conditions() {
2502        check_metrics::<CsharpParser>(
2503            "class A {
2504                bool a = (1 == 2);
2505                bool b = (1 < 2);
2506                bool c = !true;
2507                bool d = !false;
2508            }",
2509            "foo.cs",
2510            |metric| insta::assert_json_snapshot!(metric.abc),
2511        );
2512    }
2513
2514    #[test]
2515    fn csharp_assignments_with_conditions() {
2516        check_metrics::<CsharpParser>(
2517            "class A {
2518                void M() {
2519                    int a = 0;
2520                    a += 1;
2521                    a -= 2;
2522                    a *= 3;
2523                    a /= 4;
2524                    a %= 5;
2525                    a++;
2526                    a--;
2527                }
2528            }",
2529            "foo.cs",
2530            |metric| insta::assert_json_snapshot!(metric.abc),
2531        );
2532    }
2533
2534    #[test]
2535    fn csharp_methods_arguments_with_conditions() {
2536        check_metrics::<CsharpParser>(
2537            "class A {
2538                void M(int x, int y) {
2539                    F(x == y, x < y, !x.Equals(y));
2540                }
2541                void F(bool a, bool b, bool c) {}
2542            }",
2543            "foo.cs",
2544            |metric| insta::assert_json_snapshot!(metric.abc),
2545        );
2546    }
2547
2548    #[test]
2549    fn csharp_if_single_conditions() {
2550        check_metrics::<CsharpParser>(
2551            "class A {
2552                void M(int x) {
2553                    if (x > 0) { System.Console.WriteLine(\"a\"); }
2554                    if (x < 0) { System.Console.WriteLine(\"b\"); }
2555                    if (x == 0) { System.Console.WriteLine(\"c\"); }
2556                }
2557            }",
2558            "foo.cs",
2559            |metric| insta::assert_json_snapshot!(metric.abc),
2560        );
2561    }
2562
2563    #[test]
2564    fn csharp_if_multiple_conditions() {
2565        check_metrics::<CsharpParser>(
2566            "class A {
2567                void M(int x, int y) {
2568                    if (x > 0 && y > 0) { System.Console.WriteLine(\"a\"); }
2569                    if (x < 0 || y < 0) { System.Console.WriteLine(\"b\"); }
2570                }
2571            }",
2572            "foo.cs",
2573            |metric| insta::assert_json_snapshot!(metric.abc),
2574        );
2575    }
2576
2577    #[test]
2578    fn csharp_while_and_do_while_conditions() {
2579        check_metrics::<CsharpParser>(
2580            "class A {
2581                void M(int x) {
2582                    while (x > 0) { x--; }
2583                    do { x++; } while (x < 10);
2584                }
2585            }",
2586            "foo.cs",
2587            |metric| insta::assert_json_snapshot!(metric.abc),
2588        );
2589    }
2590
2591    #[test]
2592    fn csharp_return_with_conditions() {
2593        check_metrics::<CsharpParser>(
2594            "class A {
2595                bool M(int x) {
2596                    return (x > 0);
2597                }
2598                bool N(int x) {
2599                    return !(x < 0);
2600                }
2601            }",
2602            "foo.cs",
2603            |metric| insta::assert_json_snapshot!(metric.abc),
2604        );
2605    }
2606
2607    // C# `switch` *expression* arms scored zero ABC conditions before
2608    // #456 — they carry no `case` / `default` token, so the token-driven
2609    // `csharp_count_token_condition` never saw them, even though C#
2610    // cyclomatic counts each non-discard arm. Revert-verified: adding the
2611    // gated `SwitchExpressionArm` arm is what lifts this from 0 to 2. The
2612    // bare `_ =>` discard arm is excluded (the `default:` analogue),
2613    // mirroring the cyclomatic gate (lesson 11).
2614    #[test]
2615    fn csharp_switch_expression_arm_counts_condition() {
2616        check_metrics::<CsharpParser>(
2617            "class A {
2618                int M(int x) {
2619                    return x switch { 1 => 10, 2 => 20, _ => 0 };
2620                }
2621            }",
2622            "foo.cs",
2623            |metric| {
2624                // arm `1 =>` (+1) + arm `2 =>` (+1) + `_ =>` discard (+0).
2625                assert_eq!(metric.abc.conditions_sum(), 2);
2626            },
2627        );
2628    }
2629
2630    // Cross-language parity (lesson 11): a C# `switch` expression and the
2631    // equivalent Java arrow-`switch` must report the same ABC condition
2632    // count on equivalent code. Both have two concrete case arms and no
2633    // fallback arm, so both must count exactly 2. `check_metrics` takes a
2634    // non-capturing `fn` pointer, so the shared expected value (2) is
2635    // asserted in each callback rather than compared across closures; the
2636    // matching constant is what enforces parity. This guards against the
2637    // C# fix drifting away from the Java arrow-case treatment.
2638    #[test]
2639    fn csharp_java_switch_arm_abc_parity() {
2640        // C# switch expression: two arms, no fallback → 2 conditions.
2641        check_metrics::<CsharpParser>(
2642            "class A {
2643                int M(int x) {
2644                    return x switch { 1 => 10, 2 => 20 };
2645                }
2646            }",
2647            "foo.cs",
2648            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2649        );
2650
2651        // Equivalent Java arrow-`switch`: two case arms, no default → 2.
2652        check_metrics::<JavaParser>(
2653            "class A {
2654                int m(int x) {
2655                    return switch (x) { case 1 -> 10; case 2 -> 20; };
2656                }
2657            }",
2658            "foo.java",
2659            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2660        );
2661    }
2662
2663    // Issue #469: the `default` arm of a C-family `switch` is the
2664    // unconditional fallthrough and must NOT count as an ABC condition,
2665    // mirroring cyclomatic — which counts only the `Case` arms, never
2666    // the `Default` token.
2667    //
2668    // expected: each fixture is a single function whose switch has two
2669    // concrete `case` arms plus one `default`. ABC must count exactly
2670    // the two case arms (conditions = 2), matching cyclomatic's two
2671    // case-arm decisions. Pre-fix, every language below scored 3 (the
2672    // `Default` token leaked into the condition tally) — revert-verified
2673    // against the pre-#469 condition arms. We anchor on the integer
2674    // `conditions_sum()` headline (the value the public JSON serializes;
2675    // float magnitude is bit-brittle and excluded by the snapshot
2676    // policy). The cyclomatic side is pinned separately in
2677    // `java_csharp_cpp_switch_default_cyclomatic_parity` below, where
2678    // the per-space `cyclomatic()` decision count is isolated.
2679    #[test]
2680    fn java_switch_default_not_a_condition() {
2681        // Classic statement `default:`.
2682        check_metrics::<JavaParser>(
2683            "class A {
2684                int m(int x) {
2685                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2686                }
2687            }",
2688            "foo.java",
2689            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2690        );
2691        // Arrow `default ->` — shares the same `Default` token.
2692        check_metrics::<JavaParser>(
2693            "class A {
2694                int m(int x) {
2695                    return switch (x) { case 1 -> 1; case 2 -> 2; default -> 0; };
2696                }
2697            }",
2698            "foo.java",
2699            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2700        );
2701    }
2702
2703    // Over-exclusion guard (issue #469): a statement `switch` with two
2704    // `case` arms and NO `default` must still count both cases. This
2705    // pins that the fix excludes only the `Default` token, never a
2706    // `Case` arm — the count is identical before and after #469 (two
2707    // cases → 2), so it would catch a fix that over-eagerly dropped a
2708    // real case (e.g. treating the trailing case as a fallthrough).
2709    // expected: case 1 (+1) + case 2 (+1) = 2.
2710    #[test]
2711    fn java_switch_without_default_counts_all_cases() {
2712        check_metrics::<JavaParser>(
2713            "class A {
2714                int m(int x) {
2715                    switch (x) { case 1: return 1; case 2: return 2; }
2716                    return -1;
2717                }
2718            }",
2719            "foo.java",
2720            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2721        );
2722    }
2723
2724    #[test]
2725    fn csharp_switch_default_not_a_condition() {
2726        check_metrics::<CsharpParser>(
2727            "class A {
2728                int M(int x) {
2729                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2730                }
2731            }",
2732            "foo.cs",
2733            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2734        );
2735    }
2736
2737    #[test]
2738    fn cpp_switch_default_not_a_condition() {
2739        // C++ (and plain C, which shares this grammar) already excluded
2740        // `default`; this pins the cross-language parity invariant.
2741        check_metrics::<CppParser>(
2742            "void f(int x) {
2743                 switch (x) { case 1: return; case 2: return; default: return; }
2744             }",
2745            "foo.cpp",
2746            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2747        );
2748    }
2749
2750    #[test]
2751    fn objc_abc() {
2752        // ObjC ABC reuses the C/C++ walker with two additions: a message
2753        // send `[obj msg]` is a call (B), and `@try` / `@catch` count as
2754        // conditions (C) like C++ try/catch.
2755        //   A: `int total = 0`, `int i = 0`, `i++`, `total = total + …`,
2756        //      `total = -1` = 5.
2757        //   B: `[self valueAt:i]`, `[self risky]` = 2 message sends.
2758        //   C: `i < n`, `@try`, `@catch`, `total >= 0` = 4.
2759        check_metrics::<ObjcParser>(
2760            "@implementation Foo\n\
2761             - (int)bar:(int)n {\n\
2762                 int total = 0;\n\
2763                 for (int i = 0; i < n; i++) {\n\
2764                     total = total + [self valueAt:i];\n\
2765                 }\n\
2766                 @try {\n\
2767                     [self risky];\n\
2768                 } @catch (NSException *e) {\n\
2769                     total = -1;\n\
2770                 }\n\
2771                 if (total >= 0) {\n\
2772                     return total;\n\
2773                 }\n\
2774                 return 0;\n\
2775             }\n\
2776             @end\n",
2777            "foo.m",
2778            |metric| {
2779                assert_eq!(metric.abc.assignments_sum(), 5);
2780                assert_eq!(metric.abc.branches_sum(), 2);
2781                assert_eq!(metric.abc.conditions_sum(), 4);
2782            },
2783        );
2784    }
2785
2786    #[test]
2787    fn objc_abc_conditions() {
2788        // Exercises the condition-slot arms shared with C/C++: a `while`
2789        // head, a `&&` chain, a `do … while` trailing condition, and a
2790        // `return <comparison>`. ObjC routes these through the same
2791        // grammar-agnostic `cpp_inspect_*` helpers.
2792        //   A: `p++`, `p--` = 2.   B: no calls = 0.
2793        //   C: `p != 0`, `*p > 0`, `*p < 9`, `*p == 0` = 4.
2794        check_metrics::<ObjcParser>(
2795            "@implementation Foo\n\
2796             - (int)g:(int *)p {\n\
2797                 while (p != 0 && *p > 0) {\n\
2798                     p++;\n\
2799                 }\n\
2800                 do {\n\
2801                     p--;\n\
2802                 } while (*p < 9);\n\
2803                 return *p == 0;\n\
2804             }\n\
2805             @end\n",
2806            "foo.m",
2807            |metric| {
2808                assert_eq!(metric.abc.assignments_sum(), 2);
2809                assert_eq!(metric.abc.branches_sum(), 0);
2810                assert_eq!(metric.abc.conditions_sum(), 4);
2811            },
2812        );
2813    }
2814
2815    #[test]
2816    fn objc_abc_message_send_unary_condition() {
2817        // A negated boolean passed as a message-send argument is a unary
2818        // condition (Fitzpatrick Rule 9), the same as in a C-call argument.
2819        // Message args are direct children of `message_expression` (no
2820        // `argument_list`), so they are inspected in the `MessageExpression`
2821        // arm. Here: `[self use:!a]` (1 call + 1 unary condition) +
2822        // `cFunc(!a)` (1 call + 1 unary condition) → B=2, C=2.
2823        check_metrics::<ObjcParser>(
2824            "@implementation Foo\n\
2825             - (void)bar:(int)a {\n\
2826                 [self use:!a];\n\
2827                 cFunc(!a);\n\
2828             }\n\
2829             @end\n",
2830            "foo.m",
2831            |metric| {
2832                assert_eq!(metric.abc.branches_sum(), 2);
2833                assert_eq!(metric.abc.conditions_sum(), 2);
2834            },
2835        );
2836    }
2837
2838    #[test]
2839    fn objc_message_send_is_a_bool_terminal_in_condition_slots() {
2840        // `[obj ok]` is Objective-C's call, so in a condition slot it is
2841        // the unary condition `ok()` is (Fitzpatrick Rule 9). Until
2842        // `message_expression` joined `cpp_bool_terminal_kinds!` every
2843        // row here scored zero conditions where its C-call twin scored
2844        // one — and #1276's `for` slot inherited the gap. Each row is a
2845        // message send as the *whole* slot (grammar-dispatch §11: a
2846        // comparison would be counted by its own operator arm anyway),
2847        // and branches are asserted beside conditions so the arm cannot
2848        // be read as counting the call twice. The last three are the
2849        // over-count guards: a value position counts nothing, as `ok()`'s
2850        // does not.
2851        let cases = [
2852            ("if ([self ok]) {}", 1, 1),
2853            ("while (![self ok]) {}", 1, 1),
2854            ("do {} while ([self ok]);", 1, 1),
2855            ("for (; [self ok]; ) {}", 1, 1),
2856            ("x = [self ok] ? 1 : 2;", 2, 1),
2857            ("if ([self ok] && [o ok]) {}", 2, 2),
2858            ("return [self ok];", 0, 1),
2859            ("[self use:[self ok]];", 0, 2),
2860            ("[self use:![self ok]];", 1, 2),
2861        ];
2862        let mut ran = 0;
2863        for (body, conditions, branches) in cases {
2864            let src = format!("@implementation Foo\n- (int)bar {{\n    {body}\n}}\n@end\n");
2865            let abc = metrics_verbatim(
2866                LANG::Objc,
2867                src.as_bytes(),
2868                MetricsOptions::default().with_only(&[crate::Metric::Abc]),
2869            )
2870            .abc;
2871            assert_eq!(abc.conditions_sum(), conditions, "`{body}` conditions");
2872            assert_eq!(abc.branches_sum(), branches, "`{body}` branches");
2873            ran += 1;
2874        }
2875        assert_eq!(ran, cases.len());
2876        // Both answers are present, so a walker stuck at 0 or at 1 cannot
2877        // pass half the table silently.
2878        assert!(cases.iter().any(|&(_, c, _)| c == 0));
2879        assert!(cases.iter().any(|&(_, c, _)| c == 2));
2880    }
2881
2882    #[test]
2883    fn objc_message_send_condition_agrees_with_c_call() {
2884        // The intra-ObjC parity C++ cannot express: a message send and a
2885        // C call in the same slot score alike. Non-degenerate by the
2886        // `assert_eq!(…, 1)` on the reference.
2887        for (send, call) in [
2888            ("if ([a ok]) {}", "if (ok()) {}"),
2889            ("for (; [a ok]; ) {}", "for (; ok(); ) {}"),
2890            ("x = [a ok] ? 1 : 2;", "x = ok() ? 1 : 2;"),
2891        ] {
2892            let wrap =
2893                |body: &str| format!("@implementation Foo\n- (int)bar {{\n    {body}\n}}\n@end\n");
2894            let reference = abc_conditions(LANG::Objc, &wrap(call));
2895            assert!(reference >= 1, "`{call}` must count at least the slot");
2896            assert_eq!(
2897                abc_conditions(LANG::Objc, &wrap(send)),
2898                reference,
2899                "`{send}`"
2900            );
2901        }
2902    }
2903
2904    #[test]
2905    fn groovy_switch_default_not_a_condition() {
2906        check_metrics::<GroovyParser>(
2907            "class A {
2908                int m(int x) {
2909                    switch (x) { case 1: return 1; case 2: return 2; default: return 0 }
2910                }
2911            }",
2912            "foo.groovy",
2913            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2914        );
2915    }
2916
2917    #[test]
2918    fn js_switch_default_not_a_condition() {
2919        check_metrics::<JavascriptParser>(
2920            "function f(x) {
2921                 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2922             }",
2923            "foo.js",
2924            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2925        );
2926    }
2927
2928    #[test]
2929    fn ts_switch_default_not_a_condition() {
2930        check_metrics::<TypescriptParser>(
2931            "function f(x: number): number {
2932                 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2933             }",
2934            "foo.ts",
2935            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2936        );
2937    }
2938
2939    // Cross-language parity (lesson 11): the equivalent statement-`switch`
2940    // with a `default` arm reports the same ABC condition count across
2941    // Java / C# / C++. All three have two concrete case arms plus a
2942    // fallthrough `default`, so all three must count exactly 2 conditions
2943    // (the `default` excluded). `check_metrics` takes a non-capturing
2944    // `fn` pointer, so the shared expected value is asserted in each
2945    // callback; the matching constant is what enforces parity.
2946    #[test]
2947    fn java_csharp_cpp_switch_default_abc_parity() {
2948        check_metrics::<JavaParser>(
2949            "class A {
2950                int m(int x) {
2951                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2952                }
2953            }",
2954            "foo.java",
2955            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2956        );
2957        check_metrics::<CsharpParser>(
2958            "class A {
2959                int M(int x) {
2960                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2961                }
2962            }",
2963            "foo.cs",
2964            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2965        );
2966        check_metrics::<CppParser>(
2967            "void f(int x) {
2968                 switch (x) { case 1: return; case 2: return; default: return; }
2969             }",
2970            "foo.cpp",
2971            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2972        );
2973    }
2974
2975    // Pins the ABC-vs-cyclomatic agreement the fix is about (lesson 11):
2976    // on the method's own function space, the cyclomatic decision count
2977    // (`cyclomatic()` minus the per-space base of 1) must equal the ABC
2978    // `conditions()` for the same switch. Both must be 2 — the two case
2979    // arms — with the `default` excluded from each. Revert-verified: pre-
2980    // #469 ABC `conditions()` was 3 here while cyclomatic stayed at 2.
2981    #[test]
2982    fn java_csharp_cpp_switch_default_cyclomatic_parity() {
2983        check_func_space::<JavaParser, _>(
2984            "class A {
2985                int m(int x) {
2986                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2987                }
2988            }",
2989            "foo.java",
2990            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
2991        );
2992        check_func_space::<CsharpParser, _>(
2993            "class A {
2994                int M(int x) {
2995                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2996                }
2997            }",
2998            "foo.cs",
2999            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3000        );
3001        check_func_space::<CppParser, _>(
3002            "void f(int x) {
3003                 switch (x) { case 1: return; case 2: return; default: return; }
3004             }",
3005            "foo.cpp",
3006            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3007        );
3008    }
3009
3010    // Issue #473: PHP `switch` `default:` (`DefaultStatement`) is the
3011    // unconditional fallthrough, not a condition. ABC `conditions()` must
3012    // equal the cyclomatic decision count (`cyclomatic() - 1`) on the
3013    // function's own space — both 2 for the two `case` arms, with the
3014    // `default` excluded. Revert-verified: re-adding `DefaultStatement` to
3015    // the PHP ABC condition arm makes `conditions()` 3 here while cyclomatic
3016    // stays at 2, failing the invariant.
3017    #[test]
3018    fn php_switch_default_not_a_condition() {
3019        check_func_space::<PhpParser, _>(
3020            "<?php
3021            function f($x) {
3022                switch ($x) {
3023                    case 1: return 1;
3024                    case 2: return 2;
3025                    default: return 0;
3026                }
3027            }",
3028            "foo.php",
3029            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3030        );
3031    }
3032
3033    // Issue #473: PHP `match` `default =>` (`MatchDefaultExpression`) is the
3034    // unconditional fallthrough, mirroring the switch `default:` case above.
3035    // ABC `conditions()` must equal the cyclomatic decision count
3036    // (`cyclomatic() - 1`) — both 2 for the two non-default match arms.
3037    // Revert-verified: re-adding `MatchDefaultExpression` to the PHP ABC
3038    // condition arm makes `conditions()` 3 here while cyclomatic stays at 2.
3039    #[test]
3040    fn php_match_default_not_a_condition() {
3041        check_func_space::<PhpParser, _>(
3042            "<?php
3043            function g($x) {
3044                return match ($x) {
3045                    1 => \"a\",
3046                    2 => \"b\",
3047                    default => \"z\",
3048                };
3049            }",
3050            "foo.php",
3051            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3052        );
3053    }
3054
3055    #[test]
3056    fn csharp_if_bare_identifier_condition() {
3057        check_metrics::<CsharpParser>(
3058            "class A {
3059                void M(bool x) {
3060                    if (x) { System.Console.WriteLine(\"a\"); }
3061                }
3062            }",
3063            "foo.cs",
3064            |metric| {
3065                // `if (x)` contributes 1 condition (bare identifier).
3066                // `System.Console.WriteLine(...)` is the only call → 1 branch.
3067                // `*_sum()` is what the public JSON serializes as the
3068                // headline value (see `crate::wire::Abc`).
3069                assert_eq!(metric.abc.conditions_sum(), 1);
3070                assert_eq!(metric.abc.branches_sum(), 1);
3071                assert_eq!(metric.abc.assignments_sum(), 0);
3072            },
3073        );
3074    }
3075
3076    #[test]
3077    fn csharp_while_bare_identifier_condition() {
3078        check_metrics::<CsharpParser>(
3079            "class A {
3080                void M(bool x) {
3081                    while (x) { x = false; }
3082                }
3083            }",
3084            "foo.cs",
3085            |metric| {
3086                // `while (x)` contributes 1 condition; `x = false` is 1 assignment.
3087                assert_eq!(metric.abc.conditions_sum(), 1);
3088                assert_eq!(metric.abc.assignments_sum(), 1);
3089                assert_eq!(metric.abc.branches_sum(), 0);
3090            },
3091        );
3092    }
3093
3094    #[test]
3095    fn csharp_do_while_bare_identifier_condition() {
3096        check_metrics::<CsharpParser>(
3097            "class A {
3098                void M(bool x) {
3099                    do { x = true; } while (x);
3100                }
3101            }",
3102            "foo.cs",
3103            |metric| {
3104                // `do { ... } while (x)` contributes 1 condition;
3105                // `x = true` is 1 assignment.
3106                assert_eq!(metric.abc.conditions_sum(), 1);
3107                assert_eq!(metric.abc.assignments_sum(), 1);
3108                assert_eq!(metric.abc.branches_sum(), 0);
3109            },
3110        );
3111    }
3112
3113    #[test]
3114    fn csharp_if_unary_not_condition() {
3115        // Two cases share one test:
3116        //
3117        //   if (!x) { … }  — IfStatement is a known-boolean parent, so
3118        //   the unary `!` arm in `csharp_inspect_container` is *one of
3119        //   two* ways `has_boolean_content` gets set to true (the parent
3120        //   seed sets it before the `!` does). A regression that broke
3121        //   only the `is_not` branch wouldn't show up here.
3122        //
3123        //   return !x;  — ReturnStatement is *not* in the boolean-context
3124        //   seed list (BinaryExpression | IfStatement | WhileStatement |
3125        //   DoStatement | ForStatement | ConditionalExpression). So the
3126        //   `!` wrapper is the *only* path that sets
3127        //   `has_boolean_content = true`. Asserting the `return !x;`
3128        //   case isolates the unary-unwrap logic from the parent-seed
3129        //   path.
3130        check_metrics::<CsharpParser>(
3131            "class A {
3132                void M(bool x) {
3133                    if (!x) { System.Console.WriteLine(\"a\"); }
3134                }
3135                bool N(bool x) {
3136                    return !x;
3137                }
3138            }",
3139            "foo.cs",
3140            |metric| {
3141                // `if (!x)` contributes 1 condition (PrefixUnaryExpression
3142                // path with parent IfStatement seeding has_boolean_content).
3143                // `return !x;` contributes 1 condition (parent doesn't seed
3144                // — the unary `!` is the only path that sets the flag).
3145                // → 2 conditions total. 1 branch from WriteLine().
3146                assert_eq!(metric.abc.conditions_sum(), 2);
3147                assert_eq!(metric.abc.branches_sum(), 1);
3148                assert_eq!(metric.abc.assignments_sum(), 0);
3149            },
3150        );
3151    }
3152
3153    #[test]
3154    fn csharp_if_double_parenthesized_condition() {
3155        // Audit-tests follow-up: with only the
3156        // `csharp_prefix_unary_expr_kinds!()` arm covered by
3157        // `csharp_if_unary_not_condition`, the
3158        // `csharp_paren_expr_kinds!()` delegation arm in
3159        // `csharp_count_condition` was a pure dead-code candidate —
3160        // disabling it caused zero existing tests to fail (verified
3161        // 2026-05-26).
3162        //
3163        // `if ((x))` puts a `ParenthesizedExpression` at child(2) of
3164        // the IfStatement (child(1) is the literal `(`, child(2) is
3165        // the inner parenthesised expression, child(3) is the literal
3166        // `)`). `csharp_count_condition` must route that case to
3167        // `csharp_inspect_container`, which then sees parent =
3168        // IfStatement, seeds `has_boolean_content = true`, walks to
3169        // the inner Identifier, and counts it. A regression that
3170        // removed the paren arm would silently score 0.
3171        check_metrics::<CsharpParser>(
3172            "class A {
3173                void M(bool x) {
3174                    if ((x)) { System.Console.WriteLine(\"a\"); }
3175                }
3176            }",
3177            "foo.cs",
3178            |metric| {
3179                assert_eq!(metric.abc.conditions_sum(), 1);
3180                assert_eq!(metric.abc.branches_sum(), 1);
3181                assert_eq!(metric.abc.assignments_sum(), 0);
3182            },
3183        );
3184    }
3185
3186    #[test]
3187    fn csharp_bool_returning_terminal_kinds_count() {
3188        // Regression for issue #372 (lesson #19): before the fix,
3189        // `csharp_count_condition` / `csharp_inspect_container` only
3190        // recognised invocation / identifier / boolean literal as
3191        // terminal-bool operands, so the five idiomatic boolean
3192        // expressions in the `if (...)` slots below silently scored
3193        // zero conditions:
3194        //
3195        //   - `cfg.flag`        — MemberAccessExpression
3196        //   - `await c.Check()` — AwaitExpression
3197        //   - `(bool)v`         — CastExpression
3198        //   - `v is not null`   — IsPatternExpression
3199        //   - `flags[0]`        — ElementAccessExpression
3200        //
3201        // expected: 5 conditions (one per `if`), 0 assignments,
3202        // 1 branch (the single `c.Check()` invocation; the other
3203        // `if`-condition expressions are not invocations).
3204        check_metrics::<CsharpParser>(
3205            "using System.Threading.Tasks;
3206            class A {
3207                async Task M(object v, bool[] flags, Cfg cfg, C c) {
3208                    if (cfg.flag) { }
3209                    if (await c.Check()) { }
3210                    if ((bool)v) { }
3211                    if (v is not null) { }
3212                    if (flags[0]) { }
3213                }
3214            }
3215            class Cfg { public bool flag; }
3216            class C { public Task<bool> Check() => null; }",
3217            "foo.cs",
3218            |metric| {
3219                assert_eq!(metric.abc.conditions_sum(), 5);
3220                assert_eq!(metric.abc.assignments_sum(), 0);
3221                assert_eq!(metric.abc.branches_sum(), 1);
3222            },
3223        );
3224    }
3225
3226    #[test]
3227    fn csharp_if_method_call_condition() {
3228        check_metrics::<CsharpParser>(
3229            "class A {
3230                void M(string s) {
3231                    if (s.StartsWith(\"x\")) { System.Console.WriteLine(\"a\"); }
3232                }
3233            }",
3234            "foo.cs",
3235            |metric| {
3236                // `if (s.StartsWith("x"))` contributes 1 condition
3237                // (InvocationExpression) plus 1 branch for the call itself,
3238                // plus 1 branch for WriteLine.
3239                assert_eq!(metric.abc.conditions_sum(), 1);
3240                assert_eq!(metric.abc.branches_sum(), 2);
3241                assert_eq!(metric.abc.assignments_sum(), 0);
3242            },
3243        );
3244    }
3245
3246    #[test]
3247    fn csharp_if_while_boolean_literal_condition() {
3248        // Regression for #371: the tree-sitter-c-sharp grammar wraps a
3249        // bare `true` / `false` literal used as the condition of
3250        // `if` / `while` / `do` / `?:` in a `boolean_literal` node,
3251        // not the leaf `true` / `false` tokens. `csharp_count_condition`
3252        // must therefore match `BooleanLiteral` (the wrapper),
3253        // mirroring the existing `csharp_walk_for_statement` arm.
3254        // Without that, every literal-condition statement scored 0
3255        // conditions. The sibling `csharp_count_unary_conditions`
3256        // arm is covered separately by
3257        // `csharp_short_circuit_with_boolean_literal_operand` and
3258        // `csharp_inspect_container` is covered by
3259        // `csharp_declarations_with_conditions` (`!true` / `!false`).
3260        check_metrics::<CsharpParser>(
3261            "class A {
3262                void M() {
3263                    if (true) { System.Console.WriteLine(\"a\"); }
3264                    if (false) { System.Console.WriteLine(\"b\"); }
3265                    while (true) { break; }
3266                    do { break; } while (false);
3267                    int t = true ? 1 : 0;
3268                }
3269            }",
3270            "foo.cs",
3271            |metric| {
3272                // Five literal-condition statements contribute 5
3273                // `BooleanLiteral` conditions (one per if/if/while/
3274                // do-while/ternary), plus the ternary's `?` token
3275                // adds one more via `csharp_count_token_condition`
3276                // → 6 total. The two `System.Console.WriteLine`
3277                // calls contribute 2 branches; the `int t = …`
3278                // initializer contributes 1 assignment.
3279                assert_eq!(metric.abc.conditions_sum(), 6);
3280                assert_eq!(metric.abc.branches_sum(), 2);
3281                assert_eq!(metric.abc.assignments_sum(), 1);
3282            },
3283        );
3284    }
3285
3286    #[test]
3287    fn csharp_short_circuit_with_boolean_literal_operand() {
3288        // Regression for #371 (companion to
3289        // `csharp_if_while_boolean_literal_condition`): a bare
3290        // `true` / `false` operand of `&&` / `||` lands in
3291        // `csharp_count_unary_conditions`, which iterates the parent
3292        // BinaryExpression's children. That helper must match the
3293        // `BooleanLiteral` wrapper just like `csharp_count_condition`
3294        // does — otherwise the operand silently scores zero. Mutation-
3295        // verified: removing `BooleanLiteral` from the
3296        // `csharp_count_unary_conditions` arm leaves every other test
3297        // in the suite passing, so this is the only test guarding
3298        // that helper's literal-operand path.
3299        check_metrics::<CsharpParser>(
3300            "class A {
3301                void M(bool x) {
3302                    if (x && true) { System.Console.WriteLine(\"a\"); }
3303                    if (false || x) { System.Console.WriteLine(\"b\"); }
3304                }
3305            }",
3306            "foo.cs",
3307            |metric| {
3308                // `&&` and `||` themselves are NOT in
3309                // `csharp_count_token_condition`'s match list — they
3310                // route through `csharp_walk_for_conditions::AMPAMP|
3311                // PIPEPIPE`, which calls
3312                // `csharp_count_unary_conditions` on the parent
3313                // BinaryExpression. Each invocation counts every
3314                // child that matches the terminal-operand kinds and
3315                // whose parent is a BinaryExpression. For
3316                // `x && true`: 1 (Identifier x) + 1 (BooleanLiteral
3317                // true) = 2. For `false || x`: 1 (BooleanLiteral
3318                // false) + 1 (Identifier x) = 2. Total 4. Without
3319                // the BooleanLiteral arm only the two Identifier
3320                // counts would land, giving 2.
3321                assert_eq!(metric.abc.conditions_sum(), 4);
3322                assert_eq!(metric.abc.branches_sum(), 2);
3323                assert_eq!(metric.abc.assignments_sum(), 0);
3324            },
3325        );
3326    }
3327
3328    #[test]
3329    fn csharp_return_without_conditions() {
3330        check_metrics::<CsharpParser>(
3331            "class A {
3332                int M() { return 42; }
3333                string N() { return \"hi\"; }
3334            }",
3335            "foo.cs",
3336            |metric| insta::assert_json_snapshot!(metric.abc),
3337        );
3338    }
3339
3340    #[test]
3341    fn csharp_lambda_expressions_return_with_conditions() {
3342        check_metrics::<CsharpParser>(
3343            "class A {
3344                public void M() {
3345                    System.Func<int, bool> f = x => (x > 0);
3346                    System.Func<int, bool> g = x => !(x < 0);
3347                }
3348            }",
3349            "foo.cs",
3350            |metric| insta::assert_json_snapshot!(metric.abc),
3351        );
3352    }
3353
3354    #[test]
3355    fn csharp_for_with_variable_declaration() {
3356        check_metrics::<CsharpParser>(
3357            "class A {
3358                void M() {
3359                    for (int i = 0; i < 10; i++) {
3360                        System.Console.WriteLine(i);
3361                    }
3362                }
3363            }",
3364            "foo.cs",
3365            |metric| insta::assert_json_snapshot!(metric.abc),
3366        );
3367    }
3368
3369    #[test]
3370    fn csharp_for_without_variable_declaration() {
3371        check_metrics::<CsharpParser>(
3372            "class A {
3373                void M() {
3374                    int i;
3375                    for (i = 0; i < 10; i++) {
3376                        System.Console.WriteLine(i);
3377                    }
3378                }
3379            }",
3380            "foo.cs",
3381            |metric| insta::assert_json_snapshot!(metric.abc),
3382        );
3383    }
3384
3385    #[test]
3386    fn csharp_for_identifier_condition() {
3387        check_metrics::<CsharpParser>(
3388            "class A {
3389                void M(bool ready) {
3390                    for (; ready ;) { }
3391                }
3392            }",
3393            "foo.cs",
3394            |metric| {
3395                // expected: assignments=0 (no `=` / `++` / `--`),
3396                // branches=0 (no invocation / object creation),
3397                // conditions=1 (bare-identifier for-loop condition).
3398                // Averages divide by 3 spaces (top-level + class + method).
3399                insta::assert_json_snapshot!(
3400                    metric.abc,
3401                    @r#"
3402                {
3403                  "assignments": 0,
3404                  "branches": 0,
3405                  "conditions": 1,
3406                  "magnitude": 1.0,
3407                  "value": 0.0,
3408                  "assignments_average": 0.0,
3409                  "branches_average": 0.0,
3410                  "conditions_average": 0.3333333333333333,
3411                  "assignments_min": 0,
3412                  "assignments_max": 0,
3413                  "branches_min": 0,
3414                  "branches_max": 0,
3415                  "conditions_min": 0,
3416                  "conditions_max": 1
3417                }
3418                "#
3419                );
3420            },
3421        );
3422    }
3423
3424    #[test]
3425    fn csharp_for_invocation_condition() {
3426        check_metrics::<CsharpParser>(
3427            "class A {
3428                bool Ok() { return true; }
3429                void M() {
3430                    for (; Ok() ;) { }
3431                }
3432            }",
3433            "foo.cs",
3434            |metric| {
3435                // expected: assignments=0, branches=1 (the `Ok()` call),
3436                // conditions=1 (invocation as for-loop condition).
3437                // Averages divide by 4 spaces (top-level + class + two
3438                // methods).
3439                insta::assert_json_snapshot!(
3440                    metric.abc,
3441                    @r#"
3442                {
3443                  "assignments": 0,
3444                  "branches": 1,
3445                  "conditions": 1,
3446                  "magnitude": 1.4142135623730951,
3447                  "value": 0.0,
3448                  "assignments_average": 0.0,
3449                  "branches_average": 0.25,
3450                  "conditions_average": 0.25,
3451                  "assignments_min": 0,
3452                  "assignments_max": 0,
3453                  "branches_min": 0,
3454                  "branches_max": 1,
3455                  "conditions_min": 0,
3456                  "conditions_max": 1
3457                }
3458                "#
3459                );
3460            },
3461        );
3462    }
3463
3464    // Regression coverage for #279: the C# grammar wraps a literal
3465    // `true` / `false` for-loop condition in a `boolean_literal` node.
3466    // The `BooleanLiteral` arm in the `ForStatement` dispatch must
3467    // attribute one condition; without it, `for (; true ;)` would
3468    // contribute 0 (the bug fixed by this commit also affected this
3469    // shape).
3470    #[test]
3471    fn csharp_for_boolean_literal_condition() {
3472        check_metrics::<CsharpParser>(
3473            "class A {
3474                void M() {
3475                    for (; true ;) { }
3476                }
3477            }",
3478            "foo.cs",
3479            |metric| {
3480                // expected: assignments=0, branches=0,
3481                // conditions=1 (the `true` literal as condition).
3482                assert_eq!(metric.abc.conditions_sum(), 1);
3483                assert_eq!(metric.abc.assignments_sum(), 0);
3484                assert_eq!(metric.abc.branches_sum(), 0);
3485            },
3486        );
3487    }
3488
3489    // Regression coverage for #279: an empty for-loop condition such as
3490    // `for (; ;) {}` must contribute 0 to conditions — there is no
3491    // condition node to count.
3492    #[test]
3493    fn csharp_for_empty_condition() {
3494        check_metrics::<CsharpParser>(
3495            "class A {
3496                void M() {
3497                    for (; ;) { }
3498                }
3499            }",
3500            "foo.cs",
3501            |metric| {
3502                // expected: assignments=0, branches=0, conditions=0
3503                // (no condition expression in `for (; ;)`).
3504                insta::assert_json_snapshot!(
3505                    metric.abc,
3506                    @r#"
3507                {
3508                  "assignments": 0,
3509                  "branches": 0,
3510                  "conditions": 0,
3511                  "magnitude": 0.0,
3512                  "value": 0.0,
3513                  "assignments_average": 0.0,
3514                  "branches_average": 0.0,
3515                  "conditions_average": 0.0,
3516                  "assignments_min": 0,
3517                  "assignments_max": 0,
3518                  "branches_min": 0,
3519                  "branches_max": 0,
3520                  "conditions_min": 0,
3521                  "conditions_max": 0
3522                }
3523                "#
3524                );
3525            },
3526        );
3527    }
3528
3529    #[test]
3530    fn csharp_ternary_conditions() {
3531        check_metrics::<CsharpParser>(
3532            "class A {
3533                int Sign(int x) {
3534                    return (x > 0) ? 1 : (x < 0 ? -1 : 0);
3535                }
3536            }",
3537            "foo.cs",
3538            |metric| insta::assert_json_snapshot!(metric.abc),
3539        );
3540    }
3541
3542    #[test]
3543    fn csharp_malformed_parenthesized_no_panic() {
3544        check_metrics::<CsharpParser>("class A { void M() { if (( }) }", "foo.cs", |metric| {
3545            // Don't panic on malformed source.
3546            assert_eq!(metric.abc.assignments(), 0);
3547            assert_eq!(metric.abc.branches(), 0);
3548        });
3549    }
3550
3551    #[test]
3552    fn csharp_function_pointer_type_no_double_count() {
3553        // EC1 extension — `<` and `>` are also parameter-list delimiters
3554        // for unsafe function-pointer types. `FunctionPointerType` must
3555        // be in the LT/GT exclusion list, otherwise these brackets
3556        // accumulate spurious `conditions` counts.
3557        check_metrics::<CsharpParser>(
3558            "unsafe class A {
3559                public delegate*<int, int, int> Adder;
3560                public delegate*<string, void> Logger;
3561            }",
3562            "foo.cs",
3563            |metric| {
3564                assert_eq!(
3565                    metric.abc.conditions(),
3566                    0,
3567                    "function-pointer-type angle brackets must not count"
3568                );
3569            },
3570        );
3571    }
3572
3573    #[test]
3574    fn csharp_generic_type_args_no_double_count() {
3575        // EC1 — `<` and `>` inside TypeArgumentList must not count as
3576        // boolean conditions.
3577        check_metrics::<CsharpParser>(
3578            "class A {
3579                void M(System.Collections.Generic.Dictionary<string, System.Collections.Generic.List<int>> d) {
3580                    System.Console.WriteLine(d);
3581                }
3582            }",
3583            "foo.cs",
3584            |metric| insta::assert_json_snapshot!(metric.abc),
3585        );
3586    }
3587
3588    // #1275: tree-sitter-c-sharp spells `int?`, `where T : class?`, the
3589    // ternary and `a?.b` with one and the same bare `?` token, so the
3590    // unguarded `QMARK` arm scored the two type-syntax forms as
3591    // decisions.
3592    //
3593    // The fixture carries two `nullable_type` `?`, one
3594    // `type_parameter_constraint` `?`, and one real ternary over a real
3595    // `>` comparison, so every way of getting the gate wrong lands on
3596    // its own number: 5 pre-fix, 3 if only `NullableType` is denied, 4
3597    // if only `TypeParameterConstraint` is, 1 if the gate swallows the
3598    // genuine ternary, 0 if the fixture stops parsing. Asserting 0 on a
3599    // nullable-only body would have been vacuous — an unparsable file
3600    // scores 0 too.
3601    #[test]
3602    fn csharp_nullable_type_syntax_is_not_a_condition() {
3603        check_metrics::<CsharpParser>(
3604            "class A {
3605                int M<T>(int? p, int a, int b) where T : class? {
3606                    int? q = null;
3607                    return a > b ? 1 : 2;
3608                }
3609            }",
3610            "foo.cs",
3611            |metric| {
3612                assert_eq!(metric.abc.conditions_sum(), 2);
3613            },
3614        );
3615    }
3616
3617    // The polarity pin for #1275's C# half. `csharp_count_token_condition`
3618    // denies the two type-syntax parents rather than allowing the two
3619    // decision-bearing ones, so that `a?.b` and `a?[0]` keep counting by
3620    // construction: both spell their operator as the same bare `?` under
3621    // `conditional_access_expression`, and C# cyclomatic counts that node
3622    // (`safe_navigation_chain_parity` pins C# at one decision per
3623    // operator alongside every other safe-navigation language).
3624    //
3625    // This test is what stops a later "make all three languages
3626    // consistent" pass from flipping C# to a `ConditionalExpression`
3627    // allowlist: that would silently drop both counts here to 0 while
3628    // every other ABC test still passed. `??` is deliberately absent
3629    // from the expected total — C# ABC does not list `QMARKQMARK` as a
3630    // condition (it does in the TS family), which is pre-existing and
3631    // out of scope for #1275.
3632    #[test]
3633    fn csharp_conditional_access_still_counts_as_a_condition() {
3634        check_metrics::<CsharpParser>(
3635            "class A {
3636                object M(string s, int[] xs) {
3637                    return s?.Length ?? xs?[0];
3638                }
3639            }",
3640            "foo.cs",
3641            |metric| {
3642                assert_eq!(metric.abc.conditions_sum(), 2);
3643            },
3644        );
3645    }
3646
3647    // The other direction of #1275's C# gate, isolated: narrowing
3648    // `QMARK` must not swallow a real ternary. The fixture carries no
3649    // comparison operator at all, so the ternary is the only thing that
3650    // can put the count above the walker's own contribution — in the
3651    // test above, a surviving `>` would have masked an over-suppressed
3652    // `?` at 1 instead of 0 (grammar-dispatch §11). A nullable
3653    // parameter keeps the denied production live in the same parse, so
3654    // one fixture proves both directions: 3 pre-fix, 2 after, 1 if the
3655    // allowlist/denylist is aimed at `ConditionalExpression`.
3656    //
3657    // The 2 is the `?` token plus the ternary's condition slot, which
3658    // `csharp_walk_for_conditions` counts as a Fitzpatrick unary
3659    // condition. That is why this is not written as an
3660    // `assert_deepest_conditions_match_cyclomatic` parity pin the way
3661    // the Java `<` / `>` sibling is: C# cyclomatic scores this method 1
3662    // decision, and the divergence is the pre-existing unary-condition
3663    // rule, not the `?` gate.
3664    #[test]
3665    fn csharp_ternary_still_counts_alongside_nullable_types() {
3666        check_metrics::<CsharpParser>(
3667            "class A {
3668                int M(int? n, bool c) {
3669                    return c ? 1 : 2;
3670                }
3671            }",
3672            "foo.cs",
3673            |metric| {
3674                assert_eq!(metric.abc.conditions_sum(), 2);
3675            },
3676        );
3677    }
3678
3679    #[test]
3680    fn csharp_aliased_invocation_expression_branches() {
3681        // Regression for issue #94 (lesson #2): the C# grammar emits three
3682        // aliased `kind_id`s for `invocation_expression`. Code that matches
3683        // only the unsuffixed `Csharp::InvocationExpression` undercounts ABC
3684        // branches whenever the AST emits an aliased variant. The three
3685        // method calls live in `M`, so the per-method maximum (visible at
3686        // the unit-space aggregate as `branches_max`) must be 3.
3687        check_metrics::<CsharpParser>(
3688            "class A {
3689                void M() {
3690                    System.Console.WriteLine(1);
3691                    System.Console.WriteLine(2);
3692                    System.Console.WriteLine(3);
3693                }
3694            }",
3695            "foo.cs",
3696            |metric| {
3697                assert_eq!(metric.abc.branches_max(), 3);
3698                assert_eq!(metric.abc.conditions_max(), 0);
3699            },
3700        );
3701    }
3702
3703    #[test]
3704    fn php_zero_abc() {
3705        check_metrics::<PhpParser>("<?php\n", "foo.php", |metric| {
3706            assert_eq!(metric.abc.assignments_sum(), 0);
3707            assert_eq!(metric.abc.branches_sum(), 0);
3708            assert_eq!(metric.abc.conditions_sum(), 0);
3709            insta::assert_json_snapshot!(metric.abc);
3710        });
3711    }
3712
3713    #[test]
3714    fn php_simple_assignment() {
3715        check_metrics::<PhpParser>(
3716            "<?php
3717function f(): void {
3718    $a = 1;
3719    $b = 2;
3720}",
3721            "foo.php",
3722            |metric| insta::assert_json_snapshot!(metric.abc),
3723        );
3724    }
3725
3726    #[test]
3727    fn php_augmented_assignment() {
3728        check_metrics::<PhpParser>(
3729            "<?php
3730function f(int $x): int {
3731    $a = 0;
3732    $a += $x;
3733    $a -= 1;
3734    $a *= 2;
3735    return $a;
3736}",
3737            "foo.php",
3738            |metric| insta::assert_json_snapshot!(metric.abc),
3739        );
3740    }
3741
3742    #[test]
3743    fn php_const_excluded() {
3744        // Constant declarations and enum cases are NOT counted as
3745        // assignments — they declare immutable values.
3746        check_metrics::<PhpParser>(
3747            "<?php
3748class A {
3749    const PI = 3.14;
3750    const E = 2.71;
3751}
3752enum Color {
3753    case Red;
3754    case Green;
3755}",
3756            "foo.php",
3757            |metric| insta::assert_json_snapshot!(metric.abc),
3758        );
3759    }
3760
3761    #[test]
3762    fn php_function_call() {
3763        check_metrics::<PhpParser>(
3764            "<?php
3765function f(): void {
3766    foo();
3767    bar(1, 2);
3768}",
3769            "foo.php",
3770            |metric| insta::assert_json_snapshot!(metric.abc),
3771        );
3772    }
3773
3774    #[test]
3775    fn php_method_call() {
3776        check_metrics::<PhpParser>(
3777            "<?php
3778function f($obj): void {
3779    $obj->m1();
3780    $obj->m2(1);
3781}",
3782            "foo.php",
3783            |metric| insta::assert_json_snapshot!(metric.abc),
3784        );
3785    }
3786
3787    #[test]
3788    fn php_static_call() {
3789        check_metrics::<PhpParser>(
3790            "<?php
3791function f(): void {
3792    Foo::bar();
3793    Foo::baz(1);
3794}",
3795            "foo.php",
3796            |metric| insta::assert_json_snapshot!(metric.abc),
3797        );
3798    }
3799
3800    #[test]
3801    fn php_nullsafe_call() {
3802        check_metrics::<PhpParser>(
3803            "<?php
3804function f($obj): void {
3805    $obj?->m1();
3806    $obj?->m2(1);
3807}",
3808            "foo.php",
3809            |metric| insta::assert_json_snapshot!(metric.abc),
3810        );
3811    }
3812
3813    #[test]
3814    fn php_object_creation() {
3815        check_metrics::<PhpParser>(
3816            "<?php
3817function f(): void {
3818    new Foo();
3819    new Bar(1);
3820}",
3821            "foo.php",
3822            |metric| insta::assert_json_snapshot!(metric.abc),
3823        );
3824    }
3825
3826    #[test]
3827    fn php_comparison_eq() {
3828        check_metrics::<PhpParser>(
3829            "<?php
3830function f(int $a, int $b): bool {
3831    return $a == $b || $a != $b;
3832}",
3833            "foo.php",
3834            |metric| insta::assert_json_snapshot!(metric.abc),
3835        );
3836    }
3837
3838    #[test]
3839    fn php_comparison_strict() {
3840        check_metrics::<PhpParser>(
3841            "<?php
3842function f(int $a, int $b): bool {
3843    return $a === $b || $a !== $b;
3844}",
3845            "foo.php",
3846            |metric| insta::assert_json_snapshot!(metric.abc),
3847        );
3848    }
3849
3850    #[test]
3851    fn php_spaceship() {
3852        check_metrics::<PhpParser>(
3853            "<?php
3854function f(int $a, int $b): int {
3855    return $a <=> $b;
3856}",
3857            "foo.php",
3858            |metric| insta::assert_json_snapshot!(metric.abc),
3859        );
3860    }
3861
3862    #[test]
3863    fn php_instanceof() {
3864        check_metrics::<PhpParser>(
3865            "<?php
3866function f($x): bool {
3867    return $x instanceof Foo;
3868}",
3869            "foo.php",
3870            |metric| insta::assert_json_snapshot!(metric.abc),
3871        );
3872    }
3873
3874    #[test]
3875    fn php_complex_function() {
3876        // One snippet exercising A, B, C buckets together.
3877        check_metrics::<PhpParser>(
3878            "<?php
3879function f(int $a, int $b): int {
3880    $sum = $a + $b;
3881    $prod = $a * $b;
3882    if ($sum > 0 && $prod === 0) {
3883        return foo($sum);
3884    }
3885    return bar()->double();
3886}",
3887            "foo.php",
3888            |metric| insta::assert_json_snapshot!(metric.abc),
3889        );
3890    }
3891
3892    #[test]
3893    fn php_if_boolean_literal_condition() {
3894        check_metrics::<PhpParser>(
3895            "<?php\n\
3896             function f() {\n\
3897             \x20   if (true) {}                 // +1c\n\
3898             \x20   if (!false) {}               // +1c\n\
3899             \x20   while (true) {}              // +1c\n\
3900             \x20   do {} while (false);         // +1c\n\
3901             }\n",
3902            "foo.php",
3903            |metric| {
3904                assert_eq!(metric.abc.conditions_sum(), 4);
3905                insta::assert_json_snapshot!(metric.abc);
3906            },
3907        );
3908    }
3909
3910    #[test]
3911    fn php_methods_arguments_with_conditions() {
3912        check_metrics::<PhpParser>(
3913            "<?php\n\
3914             function f($a, $b) {\n\
3915             \x20   m($a, $b);                   // +1b\n\
3916             \x20   m(!$a, !$b);                 // +1b +2c\n\
3917             }\n",
3918            "foo.php",
3919            |metric| {
3920                assert_eq!(metric.abc.branches_sum(), 2);
3921                assert_eq!(metric.abc.conditions_sum(), 2);
3922                insta::assert_json_snapshot!(metric.abc);
3923            },
3924        );
3925    }
3926
3927    #[test]
3928    fn php_return_with_conditions() {
3929        check_metrics::<PhpParser>(
3930            "<?php\n\
3931             function m1($z) { return !($z >= 0); }\n\
3932             function m2($x) { return (((!$x))); }\n\
3933             function m3($x, $y) { return $x && $y; }\n",
3934            "foo.php",
3935            |metric| {
3936                // m1: `>=` (1). m2: walker unwraps to $x (1).
3937                // m3: `&&` walker counts both (2). Sum: 4.
3938                assert_eq!(metric.abc.conditions_sum(), 4);
3939                insta::assert_json_snapshot!(metric.abc);
3940            },
3941        );
3942    }
3943
3944    #[test]
3945    fn php_name2_hidden_rule_drift_marker() {
3946        // Drift marker (findings.md round-2 #3): `Php::Name2` maps
3947        // to the hidden grammar rule `_name`. At the pinned
3948        // tree-sitter-php version it is never emitted as a concrete
3949        // node — the visible `Name` (= 1) carries every name.
3950        // We list `Name2` defensively in `php_bool_terminal_kinds!()`
3951        // (lesson 34); if a future grammar bump promotes `_name`
3952        // to a visible rule, this assertion fails loudly.
3953        let src = "<?php\nfunction f($x) { if ($x) { foo($x); } }\n";
3954        let parser = PhpParser::new(
3955            src.as_bytes().to_vec(),
3956            &std::path::PathBuf::from("foo.php"),
3957            None,
3958        );
3959        assert!(!ast_has_kind_id(&parser, Php::Name2 as u16));
3960    }
3961
3962    #[test]
3963    fn php_scoped_property_access_condition_counts() {
3964        // Regression for findings.md round-2 #1 (PHP):
3965        // `if (Config::$enabled) {}` parses with
3966        // `scoped_property_access_expression` as the condition
3967        // node (kind_id 333 at the pinned grammar version — the
3968        // `*2` alias). Pre-fix, neither `ScopedPropertyAccessExpression`
3969        // nor its alias was in `php_bool_terminal_kinds!()`. The
3970        // walker reached the access node, found it non-terminal,
3971        // and broke. Mirrors C#'s `MemberAccessExpression` rule
3972        // (lesson 19, #372).
3973        check_metrics::<PhpParser>(
3974            "<?php\n\
3975             class Config { public static $enabled = true; }\n\
3976             function f() { if (Config::$enabled) { } }\n",
3977            "foo.php",
3978            |metric| {
3979                assert_eq!(metric.abc.conditions_sum(), 1);
3980                insta::assert_json_snapshot!(metric.abc);
3981            },
3982        );
3983    }
3984
3985    #[test]
3986    fn php_named_argument_unary_conditional_counts() {
3987        // Regression for the code-review finding: PHP 8 named-argument
3988        // syntax `m(name: !$a)` parses as `argument(name, ':',
3989        // unary_op_expression)`. Pre-fix, the count walker took
3990        // `argument.child(0)` (the name) and missed the value at the
3991        // last child. Now it picks the last named child as the value.
3992        check_metrics::<PhpParser>(
3993            "<?php\nfunction f($a) { m(name: !$a); }\n",
3994            "foo.php",
3995            |metric| {
3996                // 1 call (branch) + 1 unary-conditional named argument.
3997                assert_eq!(metric.abc.branches_sum(), 1);
3998                assert_eq!(metric.abc.conditions_sum(), 1);
3999                insta::assert_json_snapshot!(metric.abc);
4000            },
4001        );
4002    }
4003
4004    #[test]
4005    fn php_low_precedence_keyword_logical_ops_trigger_walker() {
4006        // Regression: pre-fix, `$a or $b` reported 0 conditions
4007        // because the dispatcher only handled `AMPAMP|PIPEPIPE`,
4008        // skipping the PHP-specific `and` / `or` / `xor` keyword
4009        // forms even though they parse under the same
4010        // `binary_expression` shape.
4011        check_metrics::<PhpParser>(
4012            "<?php\n\
4013             function f($a, $b) {\n\
4014             \x20   return $a or $b;\n\
4015             }\n",
4016            "foo.php",
4017            |metric| {
4018                assert_eq!(metric.abc.conditions_sum(), 2);
4019                insta::assert_json_snapshot!(metric.abc);
4020            },
4021        );
4022    }
4023
4024    #[test]
4025    fn php_if_multiple_conditions() {
4026        check_metrics::<PhpParser>(
4027            "<?php\n\
4028             function f($a, $b, $c, $d) {\n\
4029             \x20   if ($a || $b || $c || $d) {}     // +4c\n\
4030             \x20   if ($a && $b && $c) {}           // +3c\n\
4031             \x20   if (!$a && !$b) {}               // +2c\n\
4032             }\n",
4033            "foo.php",
4034            |metric| {
4035                assert_eq!(metric.abc.conditions_sum(), 9);
4036                insta::assert_json_snapshot!(metric.abc);
4037            },
4038        );
4039    }
4040
4041    #[test]
4042    fn php_while_and_do_while_conditions() {
4043        check_metrics::<PhpParser>(
4044            "<?php\n\
4045             function f($a, $b) {\n\
4046             \x20   while ($a || $b) {}              // +2c\n\
4047             \x20   do {} while ($a && !$b);         // +2c\n\
4048             }\n",
4049            "foo.php",
4050            |metric| {
4051                assert_eq!(metric.abc.conditions_sum(), 4);
4052                insta::assert_json_snapshot!(metric.abc);
4053            },
4054        );
4055    }
4056
4057    #[test]
4058    fn php_short_circuit_with_boolean_literal_operand() {
4059        check_metrics::<PhpParser>(
4060            "<?php\nfunction f($a) { return $a && true; }\n",
4061            "foo.php",
4062            |metric| {
4063                assert_eq!(metric.abc.conditions_sum(), 2);
4064                insta::assert_json_snapshot!(metric.abc);
4065            },
4066        );
4067    }
4068
4069    // Issue #1102, PHP half. See
4070    // `cpp_ternary_operand_slots_count_as_unary_conditions` for the
4071    // rule. PHP's ABC dispatcher has no `?`-token arm — the grammar
4072    // does emit the token, but the `conditional_expression` node is
4073    // what carries the tally's +1 — so the arm keeps that increment and
4074    // adds the operand slots.
4075    #[test]
4076    fn php_ternary_operand_slots_count_as_unary_conditions() {
4077        // ternary (1) + condition `$a` (1) + `!$b` (1) + `!$c` (1) = 4.
4078        check_metrics::<PhpParser>(
4079            "<?php\nfunction f() { $x = $a ? !$b : !$c; }\n",
4080            "foo.php",
4081            |metric| assert_eq!(metric.abc.conditions_sum(), 4),
4082        );
4083        // No-double-count pin: ternary (1) + `>` (1) = 2, unchanged by
4084        // the fix.
4085        check_metrics::<PhpParser>(
4086            "<?php\nfunction f() { $x = ($a > 0) ? $b : -$b; }\n",
4087            "foo.php",
4088            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
4089        );
4090        // Nested (PHP 8 requires the inner ternary parenthesised): two
4091        // ternary nodes plus the two bare-variable conditions = 4.
4092        check_metrics::<PhpParser>(
4093            "<?php\nfunction f() { $x = $a ? ($b ? $c : $d) : $e; }\n",
4094            "foo.php",
4095            |metric| assert_eq!(metric.abc.conditions_sum(), 4),
4096        );
4097        // A negated condition is the only input reaching the walker's
4098        // `else` fallback — see the C++ sibling for why. ternary (1) +
4099        // `!$a` (1) = 2.
4100        check_metrics::<PhpParser>(
4101            "<?php\nfunction f() { $x = !$a ? $b : $c; }\n",
4102            "foo.php",
4103            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
4104        );
4105    }
4106
4107    // PHP's short ternary `$a ?: $b` elides the consequence, which the
4108    // grammar names `body` (not `consequence`) and marks optional. The
4109    // alternative lands at child(3), so addressing the slot by field
4110    // name rather than a fixed child(4) is what keeps `!$b` counted.
4111    #[test]
4112    fn php_elided_ternary_body_still_walks_the_alternative() {
4113        // ternary (1) + condition `$a` (1) + `!$b` (1) = 3.
4114        check_metrics::<PhpParser>(
4115            "<?php\nfunction f() { $x = $a ?: !$b; }\n",
4116            "foo.php",
4117            |metric| assert_eq!(metric.abc.conditions_sum(), 3),
4118        );
4119    }
4120
4121    // Issue #1276, PHP half. The `for` header's condition slot was the
4122    // one condition slot `PhpCode::compute` never dispatched, so a
4123    // bare / negated / parenthesised loop condition scored zero while
4124    // the identical predicate in an `if` header scored one. Each
4125    // fixture below is a shape only the new arm can classify — a
4126    // comparison-shaped condition proves nothing here, because the `<`
4127    // token arm counts it either way (grammar-dispatch §11).
4128    #[test]
4129    fn php_for_condition_slot_counts_unary_conditions() {
4130        // Bare variable: the whole condition, no operator token.
4131        check_metrics::<PhpParser>(
4132            "<?php\nfunction f($a) { for (; $a; ) {} }\n",
4133            "foo.php",
4134            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
4135        );
4136        // Negation: reaches the terminal through
4137        // `php_inspect_container`'s `!` unwrap.
4138        check_metrics::<PhpParser>(
4139            "<?php\nfunction f($a) { for (; !$a; ) {} }\n",
4140            "foo.php",
4141            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
4142        );
4143        // Parentheses: counts only because the `for_statement` parent
4144        // seeds `has_boolean_content`, the seed #1276 found dead.
4145        check_metrics::<PhpParser>(
4146            "<?php\nfunction f($a) { for (; ($a); ) {} }\n",
4147            "foo.php",
4148            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
4149        );
4150        // No-double-count pin: the `<` token arm already counted this
4151        // shape before the fix, and the walker must not add a second.
4152        // Two assignments (`$i = 0`, `$i++`) confirm the header parsed
4153        // as the three-clause form rather than degenerating.
4154        check_metrics::<PhpParser>(
4155            "<?php\nfunction f($n) { for ($i = 0; $i < $n; $i++) {} }\n",
4156            "foo.php",
4157            |metric| {
4158                assert_eq!(metric.abc.conditions_sum(), 1);
4159                assert_eq!(metric.abc.assignments_sum(), 2);
4160            },
4161        );
4162        // Empty condition: no `condition` field, no decision, zero.
4163        check_metrics::<PhpParser>(
4164            "<?php\nfunction f() { for (;;) { break; } }\n",
4165            "foo.php",
4166            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
4167        );
4168    }
4169
4170    // --- Kotlin ABC tests -------------------------------------------------
4171
4172    #[test]
4173    fn kotlin_empty_class() {
4174        check_metrics::<KotlinParser>("class C {}", "foo.kt", |metric| {
4175            assert_eq!(metric.abc.assignments_sum(), 0);
4176            assert_eq!(metric.abc.branches_sum(), 0);
4177            assert_eq!(metric.abc.conditions_sum(), 0);
4178            insta::assert_json_snapshot!(metric.abc);
4179        });
4180    }
4181
4182    #[test]
4183    fn kotlin_val_declarations_are_not_assignments() {
4184        // `val` introduces an immutable binding — the `=` initialising it
4185        // is not an assignment in the ABC sense.
4186        check_metrics::<KotlinParser>(
4187            "class C {
4188                val a: Int = 1
4189                val b: Int = 2
4190                val c: Int = 3
4191            }",
4192            "foo.kt",
4193            |metric| {
4194                assert_eq!(metric.abc.assignments_sum(), 0);
4195                assert_eq!(metric.abc.branches_sum(), 0);
4196                insta::assert_json_snapshot!(metric.abc);
4197            },
4198        );
4199    }
4200
4201    #[test]
4202    fn kotlin_var_declarations_count_assignment() {
4203        // `var` initialisers count as assignments (mutable binding).
4204        check_metrics::<KotlinParser>(
4205            "class C {
4206                var a: Int = 1
4207                var b: Int = 2
4208            }",
4209            "foo.kt",
4210            |metric| {
4211                assert_eq!(metric.abc.assignments_sum(), 2);
4212                insta::assert_json_snapshot!(metric.abc);
4213            },
4214        );
4215    }
4216
4217    #[test]
4218    fn kotlin_val_then_assignments_count() {
4219        // Regression for #455: a `val` initialiser must not suppress the
4220        // standalone `=` assignments that follow it. tree-sitter-kotlin
4221        // emits no `SEMI` token (even for explicit semicolons), so the
4222        // pre-#455 `SEMI`-cleared declaration stack never cleared and the
4223        // immutable-`val` sentinel leaked, reporting A=0 here.
4224        check_metrics::<KotlinParser>(
4225            "fun f() {
4226                val cfg = 0
4227                a = 1
4228                b = 2
4229            }",
4230            "foo.kt",
4231            |metric| {
4232                // val initialiser suppressed; `a = 1` and `b = 2` count.
4233                assert_eq!(metric.abc.assignments_sum(), 2);
4234                insta::assert_json_snapshot!(metric.abc);
4235            },
4236        );
4237    }
4238
4239    #[test]
4240    fn kotlin_var_then_assignments_count() {
4241        // Companion to the #455 regression: a `var` declaration leaves a
4242        // mutable-binding sentinel that *permits* the `=` — this path
4243        // accidentally masked the leak (its `Var` sentinel never suppressed
4244        // anything), so it must keep counting both the initialiser and the
4245        // following standalone assignments.
4246        check_metrics::<KotlinParser>(
4247            "fun f() {
4248                var cfg = 0
4249                a = 1
4250                b = 2
4251            }",
4252            "foo.kt",
4253            |metric| {
4254                // var initialiser (+1) plus `a = 1` and `b = 2` (+2).
4255                assert_eq!(metric.abc.assignments_sum(), 3);
4256                insta::assert_json_snapshot!(metric.abc);
4257            },
4258        );
4259    }
4260
4261    #[test]
4262    fn kotlin_augmented_assignments_count() {
4263        // Augmented operators (+=, -=, etc.) and ++/-- always count.
4264        check_metrics::<KotlinParser>(
4265            "fun m() {
4266                var x = 0
4267                x += 1
4268                x -= 2
4269                x *= 3
4270                x++
4271                --x
4272            }",
4273            "foo.kt",
4274            |metric| {
4275                // var declaration (var x = 0): +1
4276                // x += 1, x -= 2, x *= 3, x++, --x: +5
4277                assert_eq!(metric.abc.assignments_sum(), 6);
4278                insta::assert_json_snapshot!(metric.abc);
4279            },
4280        );
4281    }
4282
4283    #[test]
4284    fn kotlin_branches_call_expression() {
4285        check_metrics::<KotlinParser>(
4286            "fun m() {
4287                println(\"a\")
4288                println(\"b\")
4289                println(\"c\")
4290            }",
4291            "foo.kt",
4292            |metric| {
4293                assert_eq!(metric.abc.branches_sum(), 3);
4294                insta::assert_json_snapshot!(metric.abc);
4295            },
4296        );
4297    }
4298
4299    #[test]
4300    fn kotlin_object_construction_branch() {
4301        // Kotlin's object construction is just `Foo()` — a `CallExpression`.
4302        check_metrics::<KotlinParser>(
4303            "class P(val x: Int)
4304            fun m(): P = P(1)",
4305            "foo.kt",
4306            |metric| {
4307                assert_eq!(metric.abc.branches_sum(), 1);
4308                insta::assert_json_snapshot!(metric.abc);
4309            },
4310        );
4311    }
4312
4313    #[test]
4314    fn kotlin_comparisons_count_conditions() {
4315        check_metrics::<KotlinParser>(
4316            "fun m(a: Int, b: Int): Boolean {
4317                val r1 = a < b
4318                val r2 = a > b
4319                val r3 = a <= b
4320                val r4 = a >= b
4321                val r5 = a == b
4322                val r6 = a != b
4323                return r1 || r2 || r3 || r4 || r5 || r6
4324            }",
4325            "foo.kt",
4326            |metric| {
4327                // Six comparison operators in the `val` initialisers
4328                // (<, >, <=, >=, ==, !=) → 6, plus the six bare-identifier
4329                // operands of the `r1 || … || r6` return chain, each a
4330                // Fitzpatrick Rule 9 unary condition (issue #557) → 6.
4331                // Total 12. Before the Kotlin walker was wired the chain
4332                // operands were silently dropped and this read 6.
4333                assert_eq!(metric.abc.conditions_sum(), 12);
4334                insta::assert_json_snapshot!(metric.abc);
4335            },
4336        );
4337    }
4338
4339    #[test]
4340    fn kotlin_identity_equality_conditions() {
4341        // `===` / `!==` are referential equality in Kotlin; they count too.
4342        check_metrics::<KotlinParser>(
4343            "fun m(a: Any, b: Any): Boolean {
4344                return a === b || a !== b
4345            }",
4346            "foo.kt",
4347            |metric| {
4348                assert_eq!(metric.abc.conditions_sum(), 2);
4349                insta::assert_json_snapshot!(metric.abc);
4350            },
4351        );
4352    }
4353
4354    #[test]
4355    fn kotlin_else_branch_counts() {
4356        check_metrics::<KotlinParser>(
4357            "fun m(x: Int): Int {
4358                return if (x > 0) 1 else -1
4359            }",
4360            "foo.kt",
4361            |metric| {
4362                // condition: > (1) + else (1) = 2
4363                assert_eq!(metric.abc.conditions_sum(), 2);
4364                insta::assert_json_snapshot!(metric.abc);
4365            },
4366        );
4367    }
4368
4369    #[test]
4370    fn kotlin_when_entries_count() {
4371        check_metrics::<KotlinParser>(
4372            "fun m(x: Int): Int {
4373                return when (x) {
4374                    1 -> 10
4375                    2 -> 20
4376                    else -> 0
4377                }
4378            }",
4379            "foo.kt",
4380            |metric| {
4381                // Non-`else` WhenEntry arms count; the `else ->` fallback
4382                // arm does not (issue #456). Two case arms + zero for the
4383                // `else` arm = 2.
4384                assert_eq!(metric.abc.conditions_sum(), 2);
4385                insta::assert_json_snapshot!(metric.abc);
4386            },
4387        );
4388    }
4389
4390    // Pins the `else ->` exclusion directly: a `when` whose only fallback
4391    // is `else ->` must not count that arm. Revert-verified — gating the
4392    // `WhenEntry` arm on `!kotlin_when_entry_is_else` is what drops this
4393    // from 3 to 2 (issue #456, lesson 11). Mirrors the cyclomatic gate.
4394    #[test]
4395    fn kotlin_when_else_not_a_condition() {
4396        check_metrics::<KotlinParser>(
4397            "fun m(x: Int): Int {
4398                return when (x) { 1 -> 10; 2 -> 20; else -> 0 }
4399            }",
4400            "foo.kt",
4401            |metric| {
4402                // case `1 ->` (+1) + case `2 ->` (+1) + `else ->` (+0) = 2.
4403                assert_eq!(metric.abc.conditions_sum(), 2);
4404            },
4405        );
4406    }
4407
4408    #[test]
4409    fn kotlin_catch_block_counts() {
4410        check_metrics::<KotlinParser>(
4411            "fun m() {
4412                try {
4413                    println(\"ok\")
4414                } catch (e: Exception) {
4415                    println(\"err\")
4416                }
4417            }",
4418            "foo.kt",
4419            |metric| {
4420                // `try` (+1) and `catch` (+1) each contribute one condition,
4421                // matching Java / C# / C++ / Groovy (Fitzpatrick counts both
4422                // keywords). Before #696 Kotlin counted only the catch block.
4423                assert_eq!(metric.abc.conditions_sum(), 2);
4424                insta::assert_json_snapshot!(metric.abc);
4425            },
4426        );
4427    }
4428
4429    #[test]
4430    fn kotlin_elvis_and_safe_cast() {
4431        // `?:` (elvis) and `as?` (safe cast) are condition-like.
4432        check_metrics::<KotlinParser>(
4433            "fun m(s: String?): Int {
4434                val n = (s as? Int) ?: 0
4435                return n
4436            }",
4437            "foo.kt",
4438            |metric| {
4439                // as? (+1) + ?: (+1) = 2 conditions.
4440                assert_eq!(metric.abc.conditions_sum(), 2);
4441                insta::assert_json_snapshot!(metric.abc);
4442            },
4443        );
4444    }
4445
4446    #[test]
4447    fn kotlin_generic_brackets_not_conditions() {
4448        // `<` / `>` used as type-parameter brackets must not be counted.
4449        check_metrics::<KotlinParser>(
4450            "class Box<T>(val v: T)
4451            fun <T> wrap(x: T): Box<T> = Box(x)",
4452            "foo.kt",
4453            |metric| {
4454                // No comparisons — only generic brackets.
4455                assert_eq!(metric.abc.conditions_sum(), 0);
4456                insta::assert_json_snapshot!(metric.abc);
4457            },
4458        );
4459    }
4460
4461    #[test]
4462    fn kotlin_class_with_methods_and_branches() {
4463        check_metrics::<KotlinParser>(
4464            "class C {
4465                var counter: Int = 0
4466                fun bump() {
4467                    counter += 1
4468                    println(counter)
4469                }
4470            }",
4471            "foo.kt",
4472            |metric| {
4473                // assignments: var counter = 0 (+1), counter += 1 (+1) = 2
4474                // branches: println(counter) = 1
4475                assert_eq!(metric.abc.assignments_sum(), 2);
4476                assert_eq!(metric.abc.branches_sum(), 1);
4477                assert_eq!(metric.abc.conditions_sum(), 0);
4478                insta::assert_json_snapshot!(metric.abc);
4479            },
4480        );
4481    }
4482
4483    #[test]
4484    fn kotlin_object_singleton_abc() {
4485        check_metrics::<KotlinParser>(
4486            "object Util {
4487                fun work(x: Int): Int {
4488                    var y = x
4489                    y += 1
4490                    if (y > 0) {
4491                        return y
4492                    }
4493                    return -1
4494                }
4495            }",
4496            "foo.kt",
4497            |metric| {
4498                // assignments: var y = x (+1), y += 1 (+1) = 2
4499                // branches: 0 (return is not a call)
4500                // conditions: y > 0 (+1) = 1
4501                assert_eq!(metric.abc.assignments_sum(), 2);
4502                assert_eq!(metric.abc.branches_sum(), 0);
4503                assert_eq!(metric.abc.conditions_sum(), 1);
4504                insta::assert_json_snapshot!(metric.abc);
4505            },
4506        );
4507    }
4508
4509    #[test]
4510    fn kotlin_interface_abc() {
4511        // Pure-abstract interface with no bodies — all-zero.
4512        check_metrics::<KotlinParser>(
4513            "interface I {
4514                fun work(): Int
4515                fun describe(): String
4516            }",
4517            "foo.kt",
4518            |metric| {
4519                assert_eq!(metric.abc.assignments_sum(), 0);
4520                assert_eq!(metric.abc.branches_sum(), 0);
4521                assert_eq!(metric.abc.conditions_sum(), 0);
4522                insta::assert_json_snapshot!(metric.abc);
4523            },
4524        );
4525    }
4526
4527    #[test]
4528    fn kotlin_nested_class_abc() {
4529        check_metrics::<KotlinParser>(
4530            "class Outer {
4531                var o: Int = 0
4532                class Nested {
4533                    var n: Int = 0
4534                    fun bump() { n += 1 }
4535                }
4536            }",
4537            "foo.kt",
4538            |metric| {
4539                // Outer: var o = 0 (+1)
4540                // Nested: var n = 0 (+1), n += 1 (+1) = 2
4541                // total assignments = 3
4542                assert_eq!(metric.abc.assignments_sum(), 3);
4543                insta::assert_json_snapshot!(metric.abc);
4544            },
4545        );
4546    }
4547
4548    #[test]
4549    fn kotlin_data_class_abc() {
4550        // `data class` with primary-constructor `val`s — no assignments
4551        // (vals don't count) and no body conditions.
4552        check_metrics::<KotlinParser>(
4553            "data class Point(val x: Int, val y: Int)",
4554            "foo.kt",
4555            |metric| {
4556                assert_eq!(metric.abc.assignments_sum(), 0);
4557                assert_eq!(metric.abc.branches_sum(), 0);
4558                assert_eq!(metric.abc.conditions_sum(), 0);
4559                insta::assert_json_snapshot!(metric.abc);
4560            },
4561        );
4562    }
4563
4564    #[test]
4565    fn kotlin_primary_constructor_default_value_not_assignment() {
4566        // Regression: default values on primary-constructor `val`
4567        // parameters are initialisers, not assignments. Without
4568        // `ClassParameter` pushing a declaration sentinel, the `=` token
4569        // here would be counted unconditionally as a standalone
4570        // assignment.
4571        check_metrics::<KotlinParser>("class C(val a: Int = 5)", "foo.kt", |metric| {
4572            // `val a = 5` → suppressed (Const sentinel).
4573            assert_eq!(metric.abc.assignments_sum(), 0);
4574            insta::assert_json_snapshot!(metric.abc);
4575        });
4576    }
4577
4578    #[test]
4579    fn kotlin_unary_conditions_in_chain() {
4580        // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
4581        // `&&` / `||` chain is one condition. `a && b || c` → a, b, c each
4582        // contribute one; the `&&` / `||` operators contribute nothing.
4583        // expected: 3 unary conditions, no comparisons, no `if`-keyword
4584        // condition in Kotlin (matches the Java byte-equivalent of 3).
4585        check_metrics::<KotlinParser>(
4586            "fun f(a: Boolean, b: Boolean, c: Boolean) {
4587                if (a && b || c) { println(\"x\") }
4588            }",
4589            "foo.kt",
4590            |metric| {
4591                assert_eq!(metric.abc.conditions_sum(), 3);
4592            },
4593        );
4594    }
4595
4596    #[test]
4597    fn kotlin_comparison_operands_add_nothing() {
4598        // Isolation check: comparison operands of a `&&` chain are nested
4599        // `binary_expression` nodes, not bare boolean leaves, so the
4600        // walker adds nothing — only the two `>` comparisons count.
4601        // expected: 2 (the two `>` tokens), walker contributes 0.
4602        check_metrics::<KotlinParser>(
4603            "fun g(x: Int, y: Int) {
4604                if (x > 0 && y > 0) { println(\"x\") }
4605            }",
4606            "foo.kt",
4607            |metric| {
4608                assert_eq!(metric.abc.conditions_sum(), 2);
4609            },
4610        );
4611    }
4612
4613    #[test]
4614    fn kotlin_negated_operand_is_unary_condition() {
4615        // A `!`-negated operand is still a unary condition: `a && !b`
4616        // unwraps the `unary_expression` to reach the inner identifier.
4617        // expected: 2 (`a` and the `!b` operand).
4618        check_metrics::<KotlinParser>(
4619            "fun f(a: Boolean, b: Boolean) {
4620                if (a && !b) { println(\"x\") }
4621            }",
4622            "foo.kt",
4623            |metric| {
4624                assert_eq!(metric.abc.conditions_sum(), 2);
4625            },
4626        );
4627    }
4628
4629    #[test]
4630    fn kotlin_bare_if_predicate_is_one_condition() {
4631        // Issue #773: a bare-boolean `if` predicate (`if (flag)`) is one
4632        // Fitzpatrick unary condition. Before the Phase-2B arm it counted
4633        // 0, so `if (flag) 1 else -1` scored 1 (only the `else`) instead of
4634        // 2, dropping below Kotlin's own cyclomatic decision count.
4635        // expected: predicate (1) + else (1) = 2.
4636        check_metrics::<KotlinParser>(
4637            "fun m(flag: Boolean): Int { return if (flag) 1 else -1 }",
4638            "foo.kt",
4639            |metric| {
4640                assert_eq!(metric.abc.conditions_sum(), 2);
4641            },
4642        );
4643    }
4644
4645    #[test]
4646    fn kotlin_bare_while_predicate_is_one_condition() {
4647        // Issue #773: the bare predicate of a `while` loop counts one
4648        // condition via the `condition` field. expected: 1.
4649        check_metrics::<KotlinParser>(
4650            "fun m(running: Boolean) { while (running) { println(\"x\") } }",
4651            "foo.kt",
4652            |metric| {
4653                assert_eq!(metric.abc.conditions_sum(), 1);
4654            },
4655        );
4656    }
4657
4658    #[test]
4659    fn kotlin_bare_do_while_predicate_is_one_condition() {
4660        // Issue #773: the bare predicate of a `do`/`while` loop counts one
4661        // condition. expected: 1.
4662        check_metrics::<KotlinParser>(
4663            "fun m(ok: Boolean) { do { println(\"x\") } while (ok) }",
4664            "foo.kt",
4665            |metric| {
4666                assert_eq!(metric.abc.conditions_sum(), 1);
4667            },
4668        );
4669    }
4670
4671    #[test]
4672    fn kotlin_comparison_predicate_not_double_counted() {
4673        // Double-count guard (#773): a comparison predicate (`if (a == b)`)
4674        // is a nested `binary_expression` already counted by the `==` token
4675        // arm, so the Phase-2B condition-slot arm must add nothing here.
4676        // expected: `==` (1) + else (1) = 2 — unchanged by the new arm.
4677        check_metrics::<KotlinParser>(
4678            "fun m(a: Int, b: Int): Int { return if (a == b) 1 else -1 }",
4679            "foo.kt",
4680            |metric| {
4681                assert_eq!(metric.abc.conditions_sum(), 2);
4682            },
4683        );
4684    }
4685
4686    #[test]
4687    fn kotlin_short_circuit_predicate_not_double_counted() {
4688        // Double-count guard (#773): an `&&`/`||` predicate is counted by
4689        // the Rule 9 chain walker (each operand once); the Phase-2B arm
4690        // must add nothing for it. expected: `x` (1) + `y` (1) + else (1)
4691        // = 3 — unchanged by the new arm.
4692        check_metrics::<KotlinParser>(
4693            "fun m(x: Boolean, y: Boolean): Int { return if (x && y) 1 else -1 }",
4694            "foo.kt",
4695            |metric| {
4696                assert_eq!(metric.abc.conditions_sum(), 3);
4697            },
4698        );
4699    }
4700
4701    #[test]
4702    fn kotlin_parenthesised_bare_predicate_is_one_condition() {
4703        // A parenthesised bare predicate (`if ((flag))`) is unwrapped by
4704        // `kotlin_inspect_container` and still counts one condition (#773).
4705        // expected: 1.
4706        check_metrics::<KotlinParser>(
4707            "fun m(flag: Boolean) { if ((flag)) { println(\"x\") } }",
4708            "foo.kt",
4709            |metric| {
4710                assert_eq!(metric.abc.conditions_sum(), 1);
4711            },
4712        );
4713    }
4714
4715    // --- TypeScript / TSX ABC tests --------------------------------------
4716    //
4717    // Assignment, branch, condition counting per Fitzpatrick:
4718    // - Augmented assignment / `++` / `--` always count.
4719    // - Plain `=` counts unless inside `const` declaration.
4720    // - `call_expression` / `new_expression` count as branches.
4721    // - Comparison / equality operators, ternary `?`, `??`, control-flow
4722    //   arms (`else`, `case`, `default`, `catch`, `try`, `instanceof`),
4723    //   and `<`/`>` (outside `type_arguments` / `type_parameters`) count
4724    //   as conditions.
4725
4726    #[test]
4727    fn typescript_assignments_basic() {
4728        check_metrics::<TypescriptParser>(
4729            "class C {
4730                m(): void {
4731                    let x = 0;          // +1 — only a `const` initializer is suppressed
4732                    x = 1;              // +1
4733                    x += 2;             // +1
4734                    x++;                // +1
4735                }
4736            }",
4737            "foo.ts",
4738            |metric| {
4739                assert_eq!(metric.abc.assignments_sum(), 4);
4740                insta::assert_json_snapshot!(metric.abc);
4741            },
4742        );
4743    }
4744
4745    #[test]
4746    fn typescript_const_excluded_from_assignments() {
4747        check_metrics::<TypescriptParser>(
4748            "class C {
4749                m(): void {
4750                    const a = 1;        // suppressed (`const` initializer)
4751                    const b = 2;        // suppressed
4752                    let c = 3;          // +1 — `let` initializers count
4753                }
4754            }",
4755            "foo.ts",
4756            |metric| {
4757                assert_eq!(metric.abc.assignments_sum(), 1);
4758                insta::assert_json_snapshot!(metric.abc);
4759            },
4760        );
4761    }
4762
4763    // Regression cluster for #1277. The pre-fix implementation decided
4764    // "is this `=` a `const` initializer?" from a sentinel stack cleared
4765    // only on a `SEMI` token, so the answer for one statement depended on
4766    // the *previous* statement's terminator. Automatic semicolon
4767    // insertion makes that terminator optional in all four JS-family
4768    // languages. The replacement is structural — see
4769    // `impl_js_family_const_binding!` in `src/metrics/abc/js_family.rs`.
4770
4771    #[test]
4772    fn typescript_asi_const_does_not_suppress_later_assignments() {
4773        check_metrics::<TypescriptParser>(
4774            "function f() {
4775                const a = 1
4776                x = 2
4777                y = 3
4778            }",
4779            "foo.ts",
4780            |metric| {
4781                // `const a = 1` suppressed; `x = 2` and `y = 3` count.
4782                // Pre-#1277 this reported 0: the unterminated `const`
4783                // never popped its sentinel.
4784                assert_eq!(metric.abc.assignments_sum(), 2);
4785            },
4786        );
4787    }
4788
4789    #[test]
4790    fn typescript_semicolon_const_does_not_suppress_later_assignments() {
4791        // The semicolon-terminated spelling of the fixture above, which
4792        // must score the same as it. Pre-#1277 they scored 2 and 0 — the
4793        // pair is what pins the terminator out of the answer.
4794        check_metrics::<TypescriptParser>(
4795            "function f() {
4796                const a = 1;
4797                x = 2;
4798                y = 3;
4799            }",
4800            "foo.ts",
4801            |metric| {
4802                assert_eq!(metric.abc.assignments_sum(), 2);
4803            },
4804        );
4805    }
4806
4807    #[test]
4808    fn typescript_as_const_does_not_suppress_later_assignments() {
4809        // The sentinel stack was also reachable from the other side: the
4810        // `const` token of a TypeScript `x as const` assertion promoted a
4811        // live `let` slot to `Const` and suppressed every `=` until the
4812        // next `;`. Structurally that `const` is a child of an
4813        // `as_expression`, not a declaration keyword.
4814        check_metrics::<TypescriptParser>(
4815            "function f(x: number) {
4816                let y = x as const
4817                w = 3
4818            }",
4819            "foo.ts",
4820            |metric| {
4821                // `let` initializer (+1) and `w = 3` (+1); pre-#1277: 1.
4822                assert_eq!(metric.abc.assignments_sum(), 2);
4823            },
4824        );
4825    }
4826
4827    #[test]
4828    fn typescript_const_declarator_shapes_stay_suppressed() {
4829        // Shapes the sentinel stack handled implicitly, which the
4830        // structural predicate must reproduce: *every* declarator under a
4831        // `const`-bearing `lexical_declaration` is suppressed, including
4832        // the second element of a multi-declarator list, a destructuring
4833        // pattern, and the defaults inside one — `c = 5`, the nested
4834        // `e = 6` and its `= {}`, `g = 7`, and the rest element's `h = 8`,
4835        // each one more pattern layer the predicate's climb has to cross
4836        // — while `for (const x of xs)` carries no `=` at all. Only
4837        // `let i = 3` and `var j = 4` count — the deliberate deviation
4838        // documented on `js_abc_compute!`. Gating on
4839        // `variable_declaration` instead of `lexical_declaration`,
4840        // dropping the `const` check, or stopping the climb at the
4841        // declarator's own `=`, each flips one of these rows. The
4842        // `for`-of row is the exception: it carries no `=`, so no change
4843        // to the predicate can move it. It pins the grammar shape instead
4844        // — a future grammar emitting a `variable_declarator` there would
4845        // start suppressing something that never counted.
4846        check_metrics::<TypescriptParser>(
4847            "function f(o: any, xs: number[]) {
4848                const a = 1, b = 2
4849                const {c = 5, d: {e = 6} = {}} = o
4850                const [g = 7, ...[h = 8]] = xs
4851                let i = 3
4852                var j = 4
4853                for (const x of xs) { k(x) }
4854            }",
4855            "foo.ts",
4856            |metric| {
4857                assert_eq!(metric.abc.assignments_sum(), 2);
4858            },
4859        );
4860    }
4861
4862    #[test]
4863    fn typescript_const_initializer_value_assignments_still_count() {
4864        // TypeScript half of
4865        // `javascript_const_initializer_value_assignments_still_count`.
4866        check_metrics::<TypescriptParser>(
4867            "function m(o: any, a: any, b: any) { const x = (o.p = 1); const y = a || (b = 2); }",
4868            "foo.ts",
4869            |metric| {
4870                assert_eq!(metric.abc.assignments_sum(), 2);
4871            },
4872        );
4873    }
4874
4875    #[test]
4876    fn typescript_branches_function_calls() {
4877        check_metrics::<TypescriptParser>(
4878            "class C {
4879                m(): void {
4880                    foo();              // +1
4881                    bar(1, 2);          // +1
4882                    new Date();         // +1
4883                }
4884            }",
4885            "foo.ts",
4886            |metric| {
4887                assert_eq!(metric.abc.branches_sum(), 3);
4888                insta::assert_json_snapshot!(metric.abc);
4889            },
4890        );
4891    }
4892
4893    #[test]
4894    fn typescript_conditions_comparison_operators() {
4895        check_metrics::<TypescriptParser>(
4896            "class C {
4897                m(x: number, y: number): boolean {
4898                    return x == y       // +1
4899                        || x === y      // +1
4900                        || x != y       // +1
4901                        || x !== y      // +1
4902                        || x < y        // +1
4903                        || x <= y       // +1
4904                        || x > y        // +1
4905                        || x >= y;      // +1
4906                }
4907            }",
4908            "foo.ts",
4909            |metric| {
4910                assert_eq!(metric.abc.conditions_sum(), 8);
4911                insta::assert_json_snapshot!(metric.abc);
4912            },
4913        );
4914    }
4915
4916    #[test]
4917    fn typescript_conditions_control_flow_arms() {
4918        check_metrics::<TypescriptParser>(
4919            "class C {
4920                m(x: number): number {
4921                    try {                       // +1 (try)
4922                        if (x > 0) {            // +1 (>)
4923                            return 1;
4924                        } else {                // +1 (else)
4925                            return -1;
4926                        }
4927                    } catch (e) {               // +1 (catch)
4928                        return 0;
4929                    }
4930                }
4931            }",
4932            "foo.ts",
4933            |metric| {
4934                assert_eq!(metric.abc.conditions_sum(), 4);
4935                insta::assert_json_snapshot!(metric.abc);
4936            },
4937        );
4938    }
4939
4940    #[test]
4941    fn typescript_conditions_switch_case() {
4942        check_metrics::<TypescriptParser>(
4943            "class C {
4944                m(x: number): number {
4945                    switch (x) {
4946                        case 1:                 // +1
4947                            return 1;
4948                        case 2:                 // +1
4949                            return 2;
4950                        default:                // +0 (fallthrough, #469)
4951                            return 0;
4952                    }
4953                }
4954            }",
4955            "foo.ts",
4956            |metric| {
4957                assert_eq!(metric.abc.conditions_sum(), 2);
4958                insta::assert_json_snapshot!(metric.abc);
4959            },
4960        );
4961    }
4962
4963    #[test]
4964    fn typescript_ternary_and_nullish() {
4965        check_metrics::<TypescriptParser>(
4966            "class C {
4967                m(x: number | null): number {
4968                    return x !== null           // +1 (!==)
4969                        ? x                     // +1 (ternary ?)
4970                        : 0;
4971                }
4972                n(x: number | null): number {
4973                    return x ?? 0;              // +1 (??)
4974                }
4975            }",
4976            "foo.ts",
4977            |metric| {
4978                assert_eq!(metric.abc.conditions_sum(), 3);
4979                insta::assert_json_snapshot!(metric.abc);
4980            },
4981        );
4982    }
4983
4984    #[test]
4985    fn typescript_instanceof_counts_as_condition() {
4986        check_metrics::<TypescriptParser>(
4987            "class C {
4988                m(o: unknown): boolean {
4989                    return o instanceof C;      // +1
4990                }
4991            }",
4992            "foo.ts",
4993            |metric| {
4994                assert_eq!(metric.abc.conditions_sum(), 1);
4995                insta::assert_json_snapshot!(metric.abc);
4996            },
4997        );
4998    }
4999
5000    #[test]
5001    fn typescript_generic_lt_gt_not_a_condition() {
5002        // `<T>` in `class C<T>` and `Array<number>` should not contribute
5003        // to conditions even though the tokens are `<` and `>`.
5004        check_metrics::<TypescriptParser>(
5005            "class C<T> {
5006                xs: Array<number> = [];
5007                m(): void {
5008                    const arr: Array<string> = [];   // suppressed const
5009                    void arr;
5010                }
5011            }",
5012            "foo.ts",
5013            |metric| {
5014                assert_eq!(metric.abc.conditions_sum(), 0);
5015                insta::assert_json_snapshot!(metric.abc);
5016            },
5017        );
5018    }
5019
5020    // #1275, TypeScript half. Eleven grammar productions emit a bare
5021    // `?` and only `ternary_expression` is a decision; the other ten are
5022    // type syntax. This fixture exercises five of them — `optional_
5023    // parameter`, `property_signature`, `method_signature`,
5024    // `abstract_method_signature` and `public_field_definition`, plus an
5025    // `optional_type` in a tuple — against one real ternary over one
5026    // real `>`.
5027    //
5028    // Six type-syntax `?` means the numbers separate cleanly: 8 pre-fix,
5029    // 2 once the allowlist is aimed at `TernaryExpression`, 1 if it is
5030    // aimed at anything else (the ternary stops counting too), 0 if the
5031    // fixture stops parsing. Any partial gate — one that named some of
5032    // the type-syntax parents in a denylist instead — lands between 3
5033    // and 7 and is equally visible.
5034    #[test]
5035    fn typescript_optional_type_syntax_is_not_a_condition() {
5036        check_metrics::<TypescriptParser>(
5037            "interface I { a?: string; m?(x: number): void; }
5038            abstract class K { f?: number; abstract g?(): void; }
5039            type Tup = [number, string?];
5040            function h(x?: number, y: number = 0): number { return y > 1 ? 1 : 2; }",
5041            "foo.ts",
5042            |metric| {
5043                assert_eq!(metric.abc.conditions_sum(), 2);
5044            },
5045        );
5046    }
5047
5048    // The explicit half of #1275's TypeScript decision: a conditional
5049    // type (`T extends U ? X : Y`) is resolved by the type checker and
5050    // erased before runtime, so its `?` is not a condition. That falls
5051    // out of the `TernaryExpression` allowlist rather than being named,
5052    // which is exactly why it needs its own test — the choice reads as
5053    // an omission otherwise, and nothing else here would notice if a
5054    // later edit added `ConditionalType` to the allowlist "for
5055    // symmetry".
5056    //
5057    // The real ternary below keeps the expectation off zero: 3 pre-fix,
5058    // 2 with the conditional type excluded, 1 if the ternary is
5059    // swallowed too.
5060    #[test]
5061    fn typescript_conditional_type_is_not_a_condition() {
5062        check_metrics::<TypescriptParser>(
5063            "type Cond<T> = T extends string ? number : boolean;
5064            function pick(a: number, b: number): number { return a > b ? 1 : 2; }",
5065            "foo.ts",
5066            |metric| {
5067                assert_eq!(metric.abc.conditions_sum(), 2);
5068            },
5069        );
5070    }
5071
5072    // grammar-dispatch §2. `Typescript::QMARK2` is the enum entry for
5073    // the external scanner's `_ternary_qmark` token, and
5074    // tree-sitter-typescript's public symbol map folds it back onto
5075    // `anon_sym_QMARK`, so `kind_id()` never reports it — every ternary
5076    // `?` in the fixture below arrives as plain `QMARK`. Listing
5077    // `QMARK2` in the gated arm would therefore be dead code, and
5078    // leaving it out is safe only for as long as that mapping holds.
5079    // This pins both halves so a grammar bump that starts exposing the
5080    // alias fails here rather than silently zeroing every TypeScript
5081    // ternary.
5082    //
5083    // The fixture's only `?` is the ternary's, deliberately. An optional
5084    // parameter (`b?: number`) would emit a `QMARK` of its own and
5085    // satisfy the positive assertion on its own, leaving it true no
5086    // matter what id the ternary's `?` came back as — decoration rather
5087    // than the non-vacuity guard it is here for.
5088    #[test]
5089    fn typescript_ternary_qmark_alias_stays_unreachable() {
5090        let parser = TypescriptParser::new(
5091            "function f(a: boolean): number { return a ? 1 : 2; }\n"
5092                .as_bytes()
5093                .to_vec(),
5094            std::path::Path::new("foo.ts"),
5095            None,
5096        );
5097        assert!(ast_has_kind_id(&parser, Typescript::QMARK as u16));
5098        assert!(!ast_has_kind_id(&parser, Typescript::QMARK2 as u16));
5099    }
5100
5101    #[test]
5102    fn typescript_abstract_class_abc() {
5103        // Abstract methods have no body — they contribute nothing.
5104        check_metrics::<TypescriptParser>(
5105            "abstract class C {
5106                abstract a(): void;
5107                m(x: number): number {
5108                    if (x > 0) return 1;        // +1 condition
5109                    return 0;
5110                }
5111            }",
5112            "foo.ts",
5113            |metric| {
5114                assert_eq!(metric.abc.conditions_sum(), 1);
5115                assert_eq!(metric.abc.branches_sum(), 0);
5116                insta::assert_json_snapshot!(metric.abc);
5117            },
5118        );
5119    }
5120
5121    #[test]
5122    fn typescript_interface_abc_zero() {
5123        check_metrics::<TypescriptParser>(
5124            "interface I {
5125                a(): void;
5126                b(): number;
5127                p: string;
5128            }",
5129            "foo.ts",
5130            |metric| {
5131                assert_eq!(metric.abc.assignments_sum(), 0);
5132                assert_eq!(metric.abc.branches_sum(), 0);
5133                assert_eq!(metric.abc.conditions_sum(), 0);
5134                insta::assert_json_snapshot!(metric.abc);
5135            },
5136        );
5137    }
5138
5139    #[test]
5140    fn typescript_arrow_field_contributes_abc() {
5141        // Arrow function class members are function spaces; their
5142        // assignments/branches/conditions are counted.
5143        check_metrics::<TypescriptParser>(
5144            "class C {
5145                arrow = (x: number) => {
5146                    if (x > 0) {                // +1 condition
5147                        return foo();           // +1 branch
5148                    }
5149                    return 0;
5150                };
5151            }",
5152            "foo.ts",
5153            |metric| {
5154                assert_eq!(metric.abc.conditions_sum(), 1);
5155                assert_eq!(metric.abc.branches_sum(), 1);
5156                insta::assert_json_snapshot!(metric.abc);
5157            },
5158        );
5159    }
5160
5161    #[test]
5162    fn typescript_parameter_property_init_not_assignment() {
5163        // Parameter properties don't introduce a `=` token themselves;
5164        // only the explicit `let z = 0` body assignment is counted.
5165        // The class field initializer `f: number = 0` likewise has a `=`
5166        // that DOES count (matches `typescript_assignments_basic`).
5167        check_metrics::<TypescriptParser>(
5168            "class C {
5169                f: number = 0;
5170                constructor(public x: number, private y: string) {
5171                    let z = 0;
5172                }
5173            }",
5174            "foo.ts",
5175            |metric| {
5176                // f's initializer + `let z = 0` = 2 assignments; the
5177                // parameter properties contribute zero.
5178                assert_eq!(metric.abc.assignments_sum(), 2);
5179                insta::assert_json_snapshot!(metric.abc);
5180            },
5181        );
5182    }
5183
5184    // TSX parity
5185
5186    #[test]
5187    fn tsx_assignments_basic() {
5188        check_metrics::<TsxParser>(
5189            "class C {
5190                m(): void {
5191                    let x = 0;
5192                    x = 1;
5193                    x += 2;
5194                    x++;
5195                }
5196            }",
5197            "foo.tsx",
5198            |metric| {
5199                assert_eq!(metric.abc.assignments_sum(), 4);
5200                insta::assert_json_snapshot!(metric.abc);
5201            },
5202        );
5203    }
5204
5205    #[test]
5206    fn tsx_const_excluded_from_assignments() {
5207        check_metrics::<TsxParser>(
5208            "class C {
5209                m(): void {
5210                    const a = 1;
5211                    let b = 2;
5212                }
5213            }",
5214            "foo.tsx",
5215            |metric| {
5216                assert_eq!(metric.abc.assignments_sum(), 1);
5217                insta::assert_json_snapshot!(metric.abc);
5218            },
5219        );
5220    }
5221
5222    #[test]
5223    fn tsx_branches_function_calls() {
5224        check_metrics::<TsxParser>(
5225            "class C {
5226                m(): void {
5227                    foo();
5228                    new Date();
5229                }
5230            }",
5231            "foo.tsx",
5232            |metric| {
5233                assert_eq!(metric.abc.branches_sum(), 2);
5234                insta::assert_json_snapshot!(metric.abc);
5235            },
5236        );
5237    }
5238
5239    #[test]
5240    fn tsx_conditions_comparison_operators() {
5241        check_metrics::<TsxParser>(
5242            "class C {
5243                m(x: number, y: number): boolean {
5244                    return x == y || x < y || x >= y;
5245                }
5246            }",
5247            "foo.tsx",
5248            |metric| {
5249                assert_eq!(metric.abc.conditions_sum(), 3);
5250                insta::assert_json_snapshot!(metric.abc);
5251            },
5252        );
5253    }
5254
5255    #[test]
5256    fn tsx_conditions_control_flow_arms() {
5257        check_metrics::<TsxParser>(
5258            "class C {
5259                m(x: number): number {
5260                    try {
5261                        if (x > 0) return 1;
5262                        else return -1;
5263                    } catch (e) {
5264                        return 0;
5265                    }
5266                }
5267            }",
5268            "foo.tsx",
5269            |metric| {
5270                assert_eq!(metric.abc.conditions_sum(), 4);
5271                insta::assert_json_snapshot!(metric.abc);
5272            },
5273        );
5274    }
5275
5276    #[test]
5277    fn tsx_conditions_switch_case() {
5278        check_metrics::<TsxParser>(
5279            "class C {
5280                m(x: number): number {
5281                    switch (x) {
5282                        case 1: return 1;       // +1
5283                        case 2: return 2;       // +1
5284                        default: return 0;      // +0 (fallthrough, #469)
5285                    }
5286                }
5287            }",
5288            "foo.tsx",
5289            |metric| {
5290                assert_eq!(metric.abc.conditions_sum(), 2);
5291                insta::assert_json_snapshot!(metric.abc);
5292            },
5293        );
5294    }
5295
5296    #[test]
5297    fn tsx_ternary_and_nullish() {
5298        check_metrics::<TsxParser>(
5299            "class C {
5300                m(x: number | null): number {
5301                    return x !== null ? x : 0;
5302                }
5303                n(x: number | null): number { return x ?? 0; }
5304            }",
5305            "foo.tsx",
5306            |metric| {
5307                assert_eq!(metric.abc.conditions_sum(), 3);
5308                insta::assert_json_snapshot!(metric.abc);
5309            },
5310        );
5311    }
5312
5313    #[test]
5314    fn tsx_instanceof_counts_as_condition() {
5315        check_metrics::<TsxParser>(
5316            "class C { m(o: unknown): boolean { return o instanceof C; } }",
5317            "foo.tsx",
5318            |metric| {
5319                assert_eq!(metric.abc.conditions_sum(), 1);
5320                insta::assert_json_snapshot!(metric.abc);
5321            },
5322        );
5323    }
5324
5325    #[test]
5326    fn tsx_generic_lt_gt_not_a_condition() {
5327        check_metrics::<TsxParser>(
5328            "class C<T> { xs: Array<number> = []; }",
5329            "foo.tsx",
5330            |metric| {
5331                assert_eq!(metric.abc.conditions_sum(), 0);
5332                insta::assert_json_snapshot!(metric.abc);
5333            },
5334        );
5335    }
5336
5337    // #1275 in the second expansion of `ts_abc_compute!`. TSX shares
5338    // TypeScript's `?` productions and its own `TernaryExpression` /
5339    // `QMARK` ids, so the gate is a distinct instantiation and needs its
5340    // own fixture — a passing TypeScript test says nothing about the
5341    // macro's other expansion. Four type-syntax `?` plus one `>` and one
5342    // ternary: 6 pre-fix, 2 after, 1 if the allowlist is misaimed.
5343    #[test]
5344    fn tsx_optional_type_syntax_is_not_a_condition() {
5345        check_metrics::<TsxParser>(
5346            "interface I { a?: string; m?(x: number): void; }
5347            class K { f?: number; }
5348            function h(x?: number, y: number = 0): number { return y > 1 ? 1 : 2; }",
5349            "foo.tsx",
5350            |metric| {
5351                assert_eq!(metric.abc.conditions_sum(), 2);
5352            },
5353        );
5354    }
5355
5356    // The TSX half of `typescript_conditional_type_is_not_a_condition`.
5357    // `ts_abc_compute!` expands twice and the two expansions are
5358    // independent code (grammar-dispatch §11): adding `ConditionalType`
5359    // to the allowlist "for symmetry" fails only the TypeScript test
5360    // without this one, which reads as the TSX expansion being fine
5361    // rather than untested. `conditional_type` is in the tsx grammar's
5362    // `?` set exactly as it is in typescript's.
5363    #[test]
5364    fn tsx_conditional_type_is_not_a_condition() {
5365        check_metrics::<TsxParser>(
5366            "type Cond<T> = T extends string ? number : boolean;
5367            function pick(a: number, b: number): number { return a > b ? 1 : 2; }",
5368            "foo.tsx",
5369            |metric| {
5370                assert_eq!(metric.abc.conditions_sum(), 2);
5371            },
5372        );
5373    }
5374
5375    // The TSX half of `typescript_ternary_qmark_alias_stays_unreachable`
5376    // — the tsx grammar declares the same `_ternary_qmark` external and
5377    // maps it back onto `anon_sym_QMARK` at its own id. Same
5378    // single-`?` fixture rule; see that test for why.
5379    #[test]
5380    fn tsx_ternary_qmark_alias_stays_unreachable() {
5381        let parser = TsxParser::new(
5382            "function f(a: boolean): number { return a ? 1 : 2; }\n"
5383                .as_bytes()
5384                .to_vec(),
5385            std::path::Path::new("foo.tsx"),
5386            None,
5387        );
5388        assert!(ast_has_kind_id(&parser, Tsx::QMARK as u16));
5389        assert!(!ast_has_kind_id(&parser, Tsx::QMARK2 as u16));
5390    }
5391
5392    #[test]
5393    fn tsx_abstract_class_abc() {
5394        check_metrics::<TsxParser>(
5395            "abstract class C {
5396                abstract a(): void;
5397                m(x: number): number {
5398                    if (x > 0) return 1;
5399                    return 0;
5400                }
5401            }",
5402            "foo.tsx",
5403            |metric| {
5404                assert_eq!(metric.abc.conditions_sum(), 1);
5405                assert_eq!(metric.abc.branches_sum(), 0);
5406                insta::assert_json_snapshot!(metric.abc);
5407            },
5408        );
5409    }
5410
5411    #[test]
5412    fn tsx_interface_abc_zero() {
5413        check_metrics::<TsxParser>(
5414            "interface I { a(): void; p: string; }",
5415            "foo.tsx",
5416            |metric| {
5417                assert_eq!(metric.abc.assignments_sum(), 0);
5418                assert_eq!(metric.abc.branches_sum(), 0);
5419                assert_eq!(metric.abc.conditions_sum(), 0);
5420                insta::assert_json_snapshot!(metric.abc);
5421            },
5422        );
5423    }
5424
5425    #[test]
5426    fn tsx_arrow_field_contributes_abc() {
5427        check_metrics::<TsxParser>(
5428            "class C {
5429                arrow = (x: number) => {
5430                    if (x > 0) return foo();
5431                    return 0;
5432                };
5433            }",
5434            "foo.tsx",
5435            |metric| {
5436                assert_eq!(metric.abc.conditions_sum(), 1);
5437                assert_eq!(metric.abc.branches_sum(), 1);
5438                insta::assert_json_snapshot!(metric.abc);
5439            },
5440        );
5441    }
5442
5443    #[test]
5444    fn tsx_asi_const_does_not_suppress_later_assignments() {
5445        // TSX half of the #1277 cluster; see
5446        // `typescript_asi_const_does_not_suppress_later_assignments`.
5447        // TSX has its own `Const` / `VariableDeclarator` /
5448        // `LexicalDeclaration` kind ids, so the predicate is generated
5449        // separately and needs its own fixture.
5450        check_metrics::<TsxParser>(
5451            "function f() {
5452                const a = 1
5453                x = 2
5454                y = 3
5455            }",
5456            "foo.tsx",
5457            |metric| {
5458                assert_eq!(metric.abc.assignments_sum(), 2);
5459            },
5460        );
5461    }
5462
5463    #[test]
5464    fn tsx_const_declarator_shapes_stay_suppressed() {
5465        // TSX half of `typescript_const_declarator_shapes_stay_suppressed`.
5466        check_metrics::<TsxParser>(
5467            "function f(o: any, xs: number[]) {
5468                const a = 1, b = 2
5469                const {c = 5, d: {e = 6} = {}} = o
5470                const [g = 7, ...[h = 8]] = xs
5471                let i = 3
5472                var j = 4
5473                for (const x of xs) { k(x) }
5474            }",
5475            "foo.tsx",
5476            |metric| {
5477                assert_eq!(metric.abc.assignments_sum(), 2);
5478            },
5479        );
5480    }
5481
5482    #[test]
5483    fn tsx_parameter_property_init_not_assignment() {
5484        // Parameter properties contribute no `=`; the body's `let z = 0`
5485        // and the field initializer do.
5486        check_metrics::<TsxParser>(
5487            "class C {
5488                f: number = 0;
5489                constructor(public x: number) { let z = 0; }
5490            }",
5491            "foo.tsx",
5492            |metric| {
5493                assert_eq!(metric.abc.assignments_sum(), 2);
5494                insta::assert_json_snapshot!(metric.abc);
5495            },
5496        );
5497    }
5498
5499    // --- Ruby ABC tests ---------------------------------------------------
5500    //
5501    // Each Ruby `assignment` / `operator_assignment` is one assignment
5502    // regardless of whether the LHS is a local, instance, or class
5503    // variable. Every `call` / `super` / `yield` is one branch. Every
5504    // comparison-operator token inside a `binary` node plus each
5505    // `else` / `elsif` / `when` / `then` / `?` / `rescue` clause is
5506    // one condition.
5507
5508    #[test]
5509    fn ruby_zero_abc() {
5510        check_metrics::<RubyParser>("\n", "foo.rb", |metric| {
5511            assert_eq!(metric.abc.assignments_sum(), 0);
5512            assert_eq!(metric.abc.branches_sum(), 0);
5513            assert_eq!(metric.abc.conditions_sum(), 0);
5514            insta::assert_json_snapshot!(metric.abc);
5515        });
5516    }
5517
5518    #[test]
5519    fn ruby_simple_assignment() {
5520        check_metrics::<RubyParser>("def f\n  a = 1\n  b = 2\nend\n", "foo.rb", |metric| {
5521            assert_eq!(metric.abc.assignments_sum(), 2);
5522            assert_eq!(metric.abc.branches_sum(), 0);
5523            assert_eq!(metric.abc.conditions_sum(), 0);
5524            insta::assert_json_snapshot!(metric.abc);
5525        });
5526    }
5527
5528    #[test]
5529    fn ruby_augmented_assignment() {
5530        // `+=`, `-=`, `*=` are `operator_assignment` nodes — each is
5531        // one assignment. Plain `=` to set the initial value adds one
5532        // more.
5533        check_metrics::<RubyParser>(
5534            "def f(x)\n  a = 0\n  a += x\n  a -= 1\n  a *= 2\nend\n",
5535            "foo.rb",
5536            |metric| {
5537                assert_eq!(metric.abc.assignments_sum(), 4);
5538                insta::assert_json_snapshot!(metric.abc);
5539            },
5540        );
5541    }
5542
5543    #[test]
5544    fn ruby_logical_augmented_assignment() {
5545        // `||=` and `&&=` are also `operator_assignment` nodes.
5546        check_metrics::<RubyParser>("def f\n  @x ||= 0\n  @x &&= 1\nend\n", "foo.rb", |metric| {
5547            assert_eq!(metric.abc.assignments_sum(), 2);
5548            insta::assert_json_snapshot!(metric.abc);
5549        });
5550    }
5551
5552    #[test]
5553    fn ruby_method_call_branch() {
5554        // Each method invocation is one branch.
5555        check_metrics::<RubyParser>(
5556            "def f(obj)\n  foo()\n  obj.bar(1)\nend\n",
5557            "foo.rb",
5558            |metric| {
5559                assert_eq!(metric.abc.branches_sum(), 2);
5560                insta::assert_json_snapshot!(metric.abc);
5561            },
5562        );
5563    }
5564
5565    #[test]
5566    fn ruby_super_and_yield_branches() {
5567        // `super` and `yield` both count as branches (control-pass).
5568        check_metrics::<RubyParser>("def f\n  super\n  yield\nend\n", "foo.rb", |metric| {
5569            assert_eq!(metric.abc.branches_sum(), 2);
5570            assert_eq!(metric.abc.assignments_sum(), 0);
5571            insta::assert_json_snapshot!(metric.abc);
5572        });
5573    }
5574
5575    #[test]
5576    fn ruby_attr_macro_is_branch() {
5577        // `attr_accessor` is a `Call3` node and registers as a branch
5578        // like any method invocation.
5579        check_metrics::<RubyParser>("class A\n  attr_accessor :x\nend\n", "foo.rb", |metric| {
5580            assert_eq!(metric.abc.branches_sum(), 1);
5581            insta::assert_json_snapshot!(metric.abc);
5582        });
5583    }
5584
5585    #[test]
5586    fn ruby_comparison_conditions() {
5587        // Each comparison operator is one condition.
5588        check_metrics::<RubyParser>(
5589            "def f(a, b)\n  a == b\n  a != b\n  a < b\n  a > b\n  a <= b\n  a >= b\nend\n",
5590            "foo.rb",
5591            |metric| {
5592                assert_eq!(metric.abc.conditions_sum(), 6);
5593                insta::assert_json_snapshot!(metric.abc);
5594            },
5595        );
5596    }
5597
5598    #[test]
5599    fn ruby_superclass_clause_is_not_a_condition() {
5600        // Regression for #1280: a superclass clause spells its `<` with the
5601        // same `LT` token as a comparison, but parents it under
5602        // `superclass` rather than `binary`, so the parent gate excludes
5603        // it. Before the gate every subclass declaration scored a phantom
5604        // condition.
5605        // expected: 0 conditions — the file contains no conditional at all;
5606        // the single assignment is `x = 1`.
5607        check_metrics::<RubyParser>(
5608            "class Foo < Bar\n  def plain\n    x = 1\n  end\nend\n",
5609            "foo.rb",
5610            |metric| {
5611                assert_eq!(metric.abc.conditions_sum(), 0);
5612                assert_eq!(metric.abc.assignments_sum(), 1);
5613            },
5614        );
5615    }
5616
5617    #[test]
5618    fn ruby_operator_method_name_is_not_a_condition() {
5619        // The `<` naming an operator method parents under `operator`, which
5620        // the `binary` gate likewise excludes (#1280). The body carries a
5621        // real comparison so the expected value is not the all-zero default:
5622        // 1 discriminates "only the name token is excluded" from both "the
5623        // gate excludes everything" (0) and "nothing is gated" (2).
5624        // expected: 1 condition — the `@v < other` comparison, not the `def <`.
5625        check_metrics::<RubyParser>("def <(other)\n  @v < other\nend\n", "foo.rb", |metric| {
5626            assert_eq!(metric.abc.conditions_sum(), 1);
5627        });
5628    }
5629
5630    #[test]
5631    fn ruby_every_comparison_operator_method_name_is_not_a_condition() {
5632        // The sibling half of #1280. `<` is not special: every comparison
5633        // and equality token Ruby lets you `def` parents under `operator`
5634        // in that position, so gating only `LT` / `GT` left `def ==`,
5635        // `def <=`, `def >=`, `def <=>`, `def !=` and `def =~` each
5636        // scoring a phantom condition — measured at 1 apiece for a body
5637        // containing no conditional at all.
5638        // expected: 0 conditions per definition; only the name token is on
5639        // the line, so a single non-zero total localises the regression.
5640        check_metrics::<RubyParser>(
5641            "def ==(o)\n  1\nend\n\
5642             def !=(o)\n  1\nend\n\
5643             def <=(o)\n  1\nend\n\
5644             def >=(o)\n  1\nend\n\
5645             def <=>(o)\n  1\nend\n\
5646             def =~(o)\n  1\nend\n\
5647             def <(o)\n  1\nend\n\
5648             def >(o)\n  1\nend\n",
5649            "foo.rb",
5650            |metric| {
5651                assert_eq!(metric.abc.conditions_sum(), 0);
5652            },
5653        );
5654        // The positive control: the same tokens inside a `binary` are real
5655        // comparisons and still count, so the gate is not blanket
5656        // suppression.
5657        check_metrics::<RubyParser>(
5658            "def cmp(a, b)\n  a == b || a <= b || a <=> b\nend\n",
5659            "foo.rb",
5660            |metric| {
5661                assert_eq!(metric.abc.conditions_sum(), 3);
5662            },
5663        );
5664    }
5665
5666    #[test]
5667    fn ruby_case_match_in_arms_are_conditions() {
5668        // Regression for #977: each non-wildcard `case … in` arm is one
5669        // ABC condition, matching Python's `case_clause` handling. Using
5670        // literal patterns (no comparison operators) isolates the
5671        // `in_clause` contribution from any operand tokens.
5672        // expected: 2 conditions — one per `in 1` / `in 2` arm.
5673        check_metrics::<RubyParser>(
5674            "def f(x)\n  case x\n  in 1 then :one\n  in 2 then :two\n  end\nend\n",
5675            "foo.rb",
5676            |metric| {
5677                assert_eq!(metric.abc.conditions_sum(), 2);
5678            },
5679        );
5680    }
5681
5682    #[test]
5683    fn ruby_case_match_guarded_wildcard_is_a_condition() {
5684        // Regression for #977: a guarded wildcard arm `in _ if x` is not a
5685        // bare default and counts as one ABC condition, while the trailing
5686        // bare `in _` adds none. The guard predicate here is a bare
5687        // identifier (no comparison operator), so the single counted
5688        // condition is the guarded `in_clause` itself.
5689        // expected: 1 condition — the guarded `in _ if x` arm only.
5690        check_metrics::<RubyParser>(
5691            "def f(x)\n  case x\n  in _ if x then :y\n  in _ then :default\n  end\nend\n",
5692            "foo.rb",
5693            |metric| {
5694                assert_eq!(metric.abc.conditions_sum(), 1);
5695            },
5696        );
5697    }
5698
5699    #[test]
5700    fn ruby_case_match_bare_wildcard_is_not_a_condition() {
5701        // Regression for #977: a `case … in` whose only arm is the bare
5702        // wildcard `in _` (no guard) is the default arm and contributes no
5703        // ABC condition, keeping ABC and cyclomatic in lockstep on the
5704        // same construct.
5705        // expected: 0 conditions.
5706        check_metrics::<RubyParser>(
5707            "def f(x)\n  case x\n  in _ then :default\n  end\nend\n",
5708            "foo.rb",
5709            |metric| {
5710                assert_eq!(metric.abc.conditions_sum(), 0);
5711            },
5712        );
5713    }
5714
5715    #[test]
5716    fn ruby_bare_predicate_control_flow_counts_one_condition() {
5717        // Regression for #696: idiomatic Ruby bare predicates
5718        // (`if flag` / `while flag` / `unless flag` / `until flag`) each
5719        // count one unary condition, matching Rust / C# / PHP / Python. The
5720        // condition field is read for both block and modifier forms.
5721        //
5722        // expected: 8 conditions — four block forms (`if`/`unless`/`while`/
5723        // `until`) plus the same four as modifiers, one each.
5724        check_metrics::<RubyParser>(
5725            "def f(flag)\n  if flag\n    a\n  end\n  unless flag\n    b\n  end\n  while flag\n    c\n  end\n  until flag\n    d\n  end\n  a if flag\n  b unless flag\n  c while flag\n  d until flag\nend\n",
5726            "foo.rb",
5727            |metric| {
5728                assert_eq!(metric.abc.conditions_sum(), 8);
5729            },
5730        );
5731    }
5732
5733    #[test]
5734    fn ruby_bare_predicate_does_not_double_count_comparison_or_chain() {
5735        // `if a == b` counts only the `==` comparison (the condition field
5736        // is a `binary` node, adding nothing). `if a && b` counts the two
5737        // chain operands via the `&&` walker, again with the condition-field
5738        // arm adding nothing — so neither shape is double-counted (#696).
5739        //
5740        // expected: 3 — `==` (1) + the `a`,`b` operands of `&&` (2).
5741        check_metrics::<RubyParser>(
5742            "def f(a, b)\n  if a == b\n    x\n  end\n  if a && b\n    y\n  end\nend\n",
5743            "foo.rb",
5744            |metric| {
5745                assert_eq!(metric.abc.conditions_sum(), 3);
5746            },
5747        );
5748    }
5749
5750    #[test]
5751    fn ruby_spaceship_and_case_equality() {
5752        // `<=>` and `===` are comparison operators (conditions).
5753        check_metrics::<RubyParser>(
5754            "def f(a, b)\n  a <=> b\n  a === b\nend\n",
5755            "foo.rb",
5756            |metric| {
5757                assert_eq!(metric.abc.conditions_sum(), 2);
5758                insta::assert_json_snapshot!(metric.abc);
5759            },
5760        );
5761    }
5762
5763    #[test]
5764    fn ruby_ternary_condition() {
5765        // The `?` ternary marker is one condition; the inner `==` is
5766        // another.
5767        check_metrics::<RubyParser>("def f(x)\n  x == 0 ? :z : :nz\nend\n", "foo.rb", |metric| {
5768            assert_eq!(metric.abc.conditions_sum(), 2);
5769            insta::assert_json_snapshot!(metric.abc);
5770        });
5771    }
5772
5773    // Issue #1161. Ruby's ternary carried only the `?` token arm, so
5774    // `a ? !b : !c` scored 1 against the 4 that Java, C#, Groovy, the C
5775    // family, the JS family, PHP and Perl all report for the same
5776    // expression (#1102) — and `ruby_inspect_container`'s `Conditional`
5777    // boolean-context seed was unreachable for the same reason.
5778    //
5779    // Every expectation below is the value its C++ sibling
5780    // (`cpp_ternary_operand_slots_count_as_unary_conditions`) already
5781    // asserts for the same expression, so the two read as one table.
5782    #[test]
5783    fn ruby_ternary_operand_slots_count_as_unary_conditions() {
5784        // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4.
5785        check_metrics::<RubyParser>("def f\n  x = a ? !b : !c\nend\n", "foo.rb", |metric| {
5786            assert_eq!(metric.abc.conditions_sum(), 4);
5787        });
5788        // No-double-count pin, and the assertion that catches the trap
5789        // this grammar sets: `-b` and `!b` are the SAME node kind
5790        // (`unary:284`), separated only by child(0). Routing the branch
5791        // slots through `ruby_inspect_container` — which tests for the
5792        // `!` token, not for the kind — is what keeps this at 2. An
5793        // implementation keying on `Unary` reads 3 here.
5794        // `?` (1) + `>` (1) = 2, unchanged by the fix: the parenthesised
5795        // condition unwraps to a `binary`, which is not a boolean
5796        // terminal, and neither branch is negated.
5797        check_metrics::<RubyParser>("def f\n  x = (a > 0) ? b : -b\nend\n", "foo.rb", |metric| {
5798            assert_eq!(metric.abc.conditions_sum(), 2);
5799        });
5800        // Nested — Ruby needs the inner ternary parenthesised. Outer `?`
5801        // (1) + outer condition `a` (1) + inner `?` (1) + inner
5802        // condition `b` (1) = 4. The outer consequence unwraps to the
5803        // inner `conditional`, which is neither a boolean terminal nor a
5804        // further paren / `!` layer, so it adds nothing on its own; the
5805        // inner ternary is reached by the walk, not by descent.
5806        check_metrics::<RubyParser>(
5807            "def f\n  x = a ? (b ? c : d) : e\nend\n",
5808            "foo.rb",
5809            |metric| {
5810                assert_eq!(metric.abc.conditions_sum(), 4);
5811            },
5812        );
5813        // A parenthesised condition, pinning the `is_parens` unwrap on
5814        // the condition slot: `(a)` is `parenthesized_statements`, not a
5815        // boolean terminal, so it reaches the walker's `else` fallback
5816        // and only `ruby_inspect_container` can resolve it.
5817        // `?` (1) + `(a)` (1) + `!b` (1) + `!c` (1) = 4; drop the
5818        // fallback and this reads 3 while every other case here holds.
5819        check_metrics::<RubyParser>("def f\n  x = (a) ? !b : !c\nend\n", "foo.rb", |metric| {
5820            assert_eq!(metric.abc.conditions_sum(), 4);
5821        });
5822        // A negated condition takes the same fallback through the `!`
5823        // unwrap rather than the paren one. `?` (1) + `!a` (1) = 2.
5824        check_metrics::<RubyParser>("def f\n  x = !a ? b : c\nend\n", "foo.rb", |metric| {
5825            assert_eq!(metric.abc.conditions_sum(), 2);
5826        });
5827    }
5828
5829    // The boolean-context seed must discriminate between the condition
5830    // slot and the two branch slots — not merely exist. None of the
5831    // fixtures above can tell the difference: their branches are either
5832    // `!`-unaries (which set the flag inside the unwrap loop regardless
5833    // of the seed) or kinds the loop breaks on before any terminal test.
5834    // A seed that returned `true` for every slot of a `Conditional`
5835    // leaves all five at their asserted values and fails nothing.
5836    //
5837    // A parenthesised *branch* is the input that separates them: the
5838    // unwrap reaches a bare terminal, so only the seed decides whether
5839    // it counts. `?` (1) + condition `a` (1) = 2 in both directions.
5840    #[test]
5841    fn ruby_ternary_branch_operands_are_not_double_counted() {
5842        check_metrics::<RubyParser>("def f\n  x = a ? (b) : c\nend\n", "foo.rb", |metric| {
5843            assert_eq!(metric.abc.conditions_sum(), 2);
5844        });
5845        check_metrics::<RubyParser>("def f\n  x = a ? b : (c)\nend\n", "foo.rb", |metric| {
5846            assert_eq!(metric.abc.conditions_sum(), 2);
5847        });
5848        // The same pair with a comment before the operand. Comments are
5849        // tree-sitter `extras`, so they become the branch's previous
5850        // sibling — which is why the seed asks the grammar which child
5851        // is the `condition` field rather than testing that sibling for
5852        // `?` / `:` as the C family does. Under the token form both of
5853        // these read 3.
5854        check_metrics::<RubyParser>(
5855            "def f\n  x = a ?\n    # note\n    (b) : c\nend\n",
5856            "foo.rb",
5857            |metric| {
5858                assert_eq!(metric.abc.conditions_sum(), 2);
5859            },
5860        );
5861        check_metrics::<RubyParser>(
5862            "def f\n  x = a ? b :\n    # note\n    (c)\nend\n",
5863            "foo.rb",
5864            |metric| {
5865                assert_eq!(metric.abc.conditions_sum(), 2);
5866            },
5867        );
5868    }
5869
5870    #[test]
5871    fn ruby_case_when_arms() {
5872        // Each `when` named clause and the `else` clause count as one
5873        // condition each; the `case` head and the implicit `then`
5874        // wrappers do not.
5875        check_metrics::<RubyParser>(
5876            "def f(x)\n  case x\n  when 1 then 'one'\n  when 2 then 'two'\n  else 'other'\n  end\nend\n",
5877            "foo.rb",
5878            |metric| {
5879                // 2 `when` + 1 `else` = 3 conditions.
5880                assert_eq!(metric.abc.conditions_sum(), 3);
5881                insta::assert_json_snapshot!(metric.abc);
5882            },
5883        );
5884    }
5885
5886    #[test]
5887    fn ruby_elsif_and_else() {
5888        // `elsif` and `else` named clauses are conditions; their inner
5889        // `then` wrappers are not.
5890        check_metrics::<RubyParser>(
5891            "def f(x)\n  if x > 0\n    1\n  elsif x < 0\n    -1\n  else\n    0\n  end\nend\n",
5892            "foo.rb",
5893            |metric| {
5894                // `>`(1) + `elsif`(1) + `<`(1) + `else`(1) = 4.
5895                assert_eq!(metric.abc.conditions_sum(), 4);
5896                insta::assert_json_snapshot!(metric.abc);
5897            },
5898        );
5899    }
5900
5901    #[test]
5902    fn ruby_rescue_clause_condition() {
5903        // The `rescue` named clause is one condition; the `rescue`
5904        // keyword token (`Rescue2`) is not counted on its own.
5905        // `do_it` without parens is an `identifier`, not a `call`, so
5906        // it contributes no branch. `handle(e)` is a `call` (1 branch).
5907        check_metrics::<RubyParser>(
5908            "def f\n  begin\n    do_it\n  rescue StandardError => e\n    handle(e)\n  end\nend\n",
5909            "foo.rb",
5910            |metric| {
5911                assert_eq!(metric.abc.conditions_sum(), 1);
5912                assert_eq!(metric.abc.branches_sum(), 1);
5913                insta::assert_json_snapshot!(metric.abc);
5914            },
5915        );
5916    }
5917
5918    #[test]
5919    fn ruby_class_complex_function() {
5920        // Mixed: assignment(=), branch(call), conditions(`>` and `==`).
5921        check_metrics::<RubyParser>(
5922            "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",
5923            "foo.rb",
5924            |metric| {
5925                assert_eq!(metric.abc.assignments_sum(), 1);
5926                assert_eq!(metric.abc.branches_sum(), 1);
5927                // `>`(1) + `==`(1) = 2 conditions. `if` is not a
5928                // token; `&&` is `AMPAMP` and is not counted (see
5929                // the module-level `Stats` doc-comment for the
5930                // cross-language policy; #395, walker tracked in
5931                // #403).
5932                assert_eq!(metric.abc.conditions_sum(), 2);
5933                insta::assert_json_snapshot!(metric.abc);
5934            },
5935        );
5936    }
5937
5938    #[test]
5939    fn ruby_unary_conditions_in_chain() {
5940        // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
5941        // `&&` / `||` chain is one condition. `a && b || c` → a, b, c each
5942        // contribute one. Ruby's `if` keyword is not a condition token.
5943        // expected: 3 unary conditions (matches the Java byte-equivalent).
5944        check_metrics::<RubyParser>(
5945            "def f(a, b, c)\n  if a && b || c\n    puts \"x\"\n  end\nend\n",
5946            "foo.rb",
5947            |metric| {
5948                assert_eq!(metric.abc.conditions_sum(), 3);
5949            },
5950        );
5951    }
5952
5953    #[test]
5954    fn ruby_keyword_and_or_chain_counts_operands() {
5955        // The keyword forms `and` / `or` get the same Rule 9 treatment as
5956        // `&&` / `||`. expected: 3 unary conditions (a, b, c).
5957        check_metrics::<RubyParser>(
5958            "def f(a, b, c)\n  if a and b or c\n    puts \"x\"\n  end\nend\n",
5959            "foo.rb",
5960            |metric| {
5961                assert_eq!(metric.abc.conditions_sum(), 3);
5962            },
5963        );
5964    }
5965
5966    #[test]
5967    fn ruby_negated_operand_is_unary_condition() {
5968        // A `!`-negated operand unwraps the `unary` node to the inner
5969        // identifier. expected: 2 (`a` and the `!b` operand).
5970        check_metrics::<RubyParser>(
5971            "def f(a, b)\n  if a && !b\n    puts \"x\"\n  end\nend\n",
5972            "foo.rb",
5973            |metric| {
5974                assert_eq!(metric.abc.conditions_sum(), 2);
5975            },
5976        );
5977    }
5978
5979    #[test]
5980    fn ruby_comparison_operands_add_nothing() {
5981        // Isolation for Rule 9 (issue #557): when the `&&` operands are
5982        // themselves comparisons, the unary-condition walker must add
5983        // nothing on top of the two `>` comparisons already counted as
5984        // conditions — distinguishing the gap (bare boolean operands)
5985        // from ordinary relational conditions. Mirrors the Kotlin and
5986        // Elixir isolation tests. expected: 2 (the two `>` comparisons).
5987        check_metrics::<RubyParser>(
5988            "def f(x, y)\n  if x > 0 && y > 0\n    puts \"x\"\n  end\nend\n",
5989            "foo.rb",
5990            |metric| {
5991                assert_eq!(metric.abc.conditions_sum(), 2);
5992            },
5993        );
5994    }
5995
5996    // ---------------------------------------------------------------
5997    // Default-impl placeholder smoke tests (audited in #188).
5998    //
5999    // These tests assert that the *current* default-impl languages
6000    // return ABC = 0/0/0 for source that DOES contain branches,
6001    // conditions, and assignments. When the real impl lands for any
6002    // of these languages, the corresponding assertion below will fire
6003    // — the implementer must update the expected values, which is the
6004    // gate. Tag the follow-up issue in each test.
6005    // ---------------------------------------------------------------
6006
6007    // --- Python ABC ---------------------------------------------------
6008
6009    #[test]
6010    fn python_empty_module_zero() {
6011        check_metrics::<PythonParser>("", "empty.py", |metric| {
6012            assert_eq!(metric.abc.assignments_sum(), 0);
6013            assert_eq!(metric.abc.branches_sum(), 0);
6014            assert_eq!(metric.abc.conditions_sum(), 0);
6015            insta::assert_json_snapshot!(metric.abc);
6016        });
6017    }
6018
6019    #[test]
6020    fn python_plain_assignments_count() {
6021        // Three plain `=` assignments → A=3. No branches, no conditions.
6022        check_metrics::<PythonParser>("x = 1\ny = 2\nz = x\n", "foo.py", |metric| {
6023            assert_eq!(metric.abc.assignments_sum(), 3);
6024            assert_eq!(metric.abc.branches_sum(), 0);
6025            assert_eq!(metric.abc.conditions_sum(), 0);
6026            insta::assert_json_snapshot!(metric.abc);
6027        });
6028    }
6029
6030    #[test]
6031    fn python_typed_assignment_counts_bare_annotation_does_not() {
6032        // `x: int = 1` carries an `=`, so it counts.
6033        // `y: int` is a bare annotation (no `=`) — declares a type but
6034        // binds nothing; it must NOT inflate the assignment count.
6035        check_metrics::<PythonParser>("x: int = 1\ny: int\n", "foo.py", |metric| {
6036            assert_eq!(metric.abc.assignments_sum(), 1);
6037            insta::assert_json_snapshot!(metric.abc);
6038        });
6039    }
6040
6041    #[test]
6042    fn python_augmented_assignments_count() {
6043        // Each augmented op counts once.
6044        check_metrics::<PythonParser>("x = 0\nx += 1\nx -= 1\nx *= 2\n", "foo.py", |metric| {
6045            // 1 plain `=` + 3 augmented = 4 assignments.
6046            assert_eq!(metric.abc.assignments_sum(), 4);
6047            insta::assert_json_snapshot!(metric.abc);
6048        });
6049    }
6050
6051    #[test]
6052    fn python_walrus_counts_as_assignment() {
6053        // `x := 10` is a `NamedExpression` (PEP 572). It binds a value
6054        // → one assignment under Fitzpatrick's rule.
6055        check_metrics::<PythonParser>("if (n := 10) > 5:\n    pass\n", "foo.py", |metric| {
6056            // 1 assignment (walrus) + 1 condition (`> 5` is a
6057            // ComparisonOperator).
6058            assert_eq!(metric.abc.assignments_sum(), 1);
6059            assert_eq!(metric.abc.conditions_sum(), 1);
6060            insta::assert_json_snapshot!(metric.abc);
6061        });
6062    }
6063
6064    #[test]
6065    fn python_calls_are_branches() {
6066        // `foo()`, `bar()`, `Baz()` (constructor) all parse as `Call`
6067        // → three branches.
6068        check_metrics::<PythonParser>(
6069            "def foo():\n    pass\ndef bar():\n    pass\nclass Baz:\n    pass\nfoo()\nbar()\nBaz()\n",
6070            "foo.py",
6071            |metric| {
6072                assert_eq!(metric.abc.branches_sum(), 3);
6073                assert_eq!(metric.abc.assignments_sum(), 0);
6074                insta::assert_json_snapshot!(metric.abc);
6075            },
6076        );
6077    }
6078
6079    #[test]
6080    fn python_comparisons_count_conditions() {
6081        // `x > 0`, `x == y`, `x is None` are each a single
6082        // `ComparisonOperator` node — three conditions.
6083        check_metrics::<PythonParser>(
6084            "def f(x, y):\n    a = x > 0\n    b = x == y\n    c = x is None\n",
6085            "foo.py",
6086            |metric| {
6087                assert_eq!(metric.abc.conditions_sum(), 3);
6088                // 3 plain assignments; the comparisons are operands.
6089                assert_eq!(metric.abc.assignments_sum(), 3);
6090                insta::assert_json_snapshot!(metric.abc);
6091            },
6092        );
6093    }
6094
6095    #[test]
6096    fn python_chained_comparison_counts_once() {
6097        // tree-sitter-python collapses `0 < x < 10` into a single
6098        // `ComparisonOperator` — one condition, not two.
6099        check_metrics::<PythonParser>("def f(x):\n    return 0 < x < 10\n", "foo.py", |metric| {
6100            assert_eq!(metric.abc.conditions_sum(), 1);
6101            insta::assert_json_snapshot!(metric.abc);
6102        });
6103    }
6104
6105    #[test]
6106    fn python_number_truthy_condition_counts() {
6107        // Regression for #772: Python treats every non-zero number as
6108        // truthy, so `if 5:` and `x and 5` should each count their
6109        // numeric literal as a Fitzpatrick unary condition. Pre-fix
6110        // `python_bool_terminal_kinds!()` listed `True` / `False` but
6111        // omitted `Integer` / `Float`, so the walker dropped every
6112        // numeric-truthy operand (mirrors the Lua `Number` fix).
6113        check_metrics::<PythonParser>(
6114            "def f(a):\n    if 5:\n        pass\n    return a and 2\n",
6115            "foo.py",
6116            |metric| {
6117                // `if 5:` → walker counts the Integer literal (+1).
6118                // `a and 2` → `and` walker counts both operands:
6119                //   identifier `a` (+1), Integer `2` (+1).
6120                // Total: 3.
6121                assert_eq!(metric.abc.conditions_sum(), 3);
6122                insta::assert_json_snapshot!(metric.abc);
6123            },
6124        );
6125    }
6126
6127    #[test]
6128    fn python_boolean_operators_not_counted_directly() {
6129        // Python's `and` / `or` are not counted as conditions on
6130        // their own (Fitzpatrick Rule 5; #395). Each operand is
6131        // instead counted as a unary conditional by the walker
6132        // (Rule 9; #403). `if a and b or c:` parses left-to-right
6133        // with `or` lower precedence: `(a and b) or c`. Walker
6134        // tallies: inner `and` counts `a`, `b` (+2); outer `or`
6135        // counts only the new outer operand `c` (+1; the inner
6136        // `(a and b)` BooleanOperator is not a terminal). Total
6137        // C = 3.
6138        check_metrics::<PythonParser>(
6139            "def f(a, b, c):\n    if a and b or c:\n        pass\n",
6140            "foo.py",
6141            |metric| {
6142                assert_eq!(metric.abc.conditions_sum(), 3);
6143                insta::assert_json_snapshot!(metric.abc);
6144            },
6145        );
6146    }
6147
6148    /// Python's unary `not` operator parses as `NotOperator` and now
6149    /// counts as one condition, matching Java's `!x` rule. Closes
6150    /// the parity gap noted in #214: without this, `if not flag:`
6151    /// reported 0 conditions while the Java equivalent reports 1.
6152    #[test]
6153    fn python_unary_not_counts_as_condition() {
6154        check_metrics::<PythonParser>(
6155            "def f(flag):\n    if not flag:\n        return 1\n    return 0\n",
6156            "foo.py",
6157            |metric| {
6158                // One `NotOperator` -> 1 condition. The `if` itself
6159                // is structural and doesn't add an Abc condition.
6160                assert_eq!(metric.abc.conditions_sum(), 1);
6161                insta::assert_json_snapshot!(metric.abc);
6162            },
6163        );
6164    }
6165
6166    /// `return not flag` — the unary `not` is the entire return
6167    /// expression. Without `NotOperator` counted, this reports zero
6168    /// conditions; with it, one. Java's `return !flag;` is one.
6169    #[test]
6170    fn python_return_unary_not_counts() {
6171        check_metrics::<PythonParser>("def f(flag):\n    return not flag\n", "foo.py", |metric| {
6172            assert_eq!(metric.abc.conditions_sum(), 1);
6173            insta::assert_json_snapshot!(metric.abc);
6174        });
6175    }
6176
6177    /// `foo(not ready, value)` — the unary `not` inside an argument
6178    /// list still contributes. Mirrors Java's
6179    /// `java_count_unary_conditions` walk over argument lists.
6180    #[test]
6181    fn python_unary_not_in_argument_list_counts() {
6182        check_metrics::<PythonParser>(
6183            "def f(ready, value):\n    log(not ready, value)\n",
6184            "foo.py",
6185            |metric| {
6186                // 1 Call (log) -> 1 branch.
6187                // 1 NotOperator (not ready) -> 1 condition.
6188                assert_eq!(metric.abc.branches_sum(), 1);
6189                assert_eq!(metric.abc.conditions_sum(), 1);
6190                insta::assert_json_snapshot!(metric.abc);
6191            },
6192        );
6193    }
6194
6195    /// Nested `not` + comparison counts each unique node once.
6196    /// `not (x > 0)` parses as `NotOperator(ParenthesizedExpression(
6197    /// ComparisonOperator))`; both the unary and the comparison
6198    /// contribute one condition (mirrors Java's `!(x > 0)` = 2
6199    /// conditions).
6200    #[test]
6201    fn python_unary_not_with_comparison_counts_each_once() {
6202        check_metrics::<PythonParser>(
6203            "def f(x):\n    if not (x > 0):\n        return 1\n    return 0\n",
6204            "foo.py",
6205            |metric| {
6206                // NotOperator (1) + ComparisonOperator (1) = 2.
6207                assert_eq!(metric.abc.conditions_sum(), 2);
6208                insta::assert_json_snapshot!(metric.abc);
6209            },
6210        );
6211    }
6212
6213    /// `not x and y` parses as `BooleanOperator(NotOperator(x), and,
6214    /// y)`. The `and` itself is NOT counted (Fitzpatrick Rule 5
6215    /// lists only comparison operators); the `NotOperator` is
6216    /// counted at the top level (Rule 7); and the `y` operand is
6217    /// counted by the Rule 9 walker (issue #403). Total: 2.
6218    /// `NotOperator` is intentionally not walked-into a second
6219    /// time — the walker skips it to avoid double-counting.
6220    #[test]
6221    fn python_unary_not_with_boolean_combinator_counts_each() {
6222        check_metrics::<PythonParser>(
6223            "def f(x, y):\n    if not x and y:\n        return 1\n    return 0\n",
6224            "foo.py",
6225            |metric| {
6226                // NotOperator (1) + walker on `and` finds `y` (1) = 2.
6227                assert_eq!(metric.abc.conditions_sum(), 2);
6228                insta::assert_json_snapshot!(metric.abc);
6229            },
6230        );
6231    }
6232
6233    #[test]
6234    fn python_control_flow_arms_count_conditions() {
6235        // `elif`, `else`, `except`, `finally`, `case` each contribute
6236        // one condition. The comparisons in the `if`/`elif`/`while`
6237        // headers contribute their own ComparisonOperator counts.
6238        check_metrics::<PythonParser>(
6239            "def f(x):\n    if x > 0:\n        a = 1\n    elif x > -1:\n        a = 2\n    else:\n        a = 3\n",
6240            "foo.py",
6241            |metric| {
6242                // 2 ComparisonOperator (`x > 0`, `x > -1`) + 1
6243                // ElifClause + 1 ElseClause = 4 conditions.
6244                assert_eq!(metric.abc.conditions_sum(), 4);
6245                insta::assert_json_snapshot!(metric.abc);
6246            },
6247        );
6248    }
6249
6250    #[test]
6251    fn python_ternary_counts_as_condition() {
6252        // `a if c else b` is `ConditionalExpression` → 1 condition.
6253        // `c > 0` adds 1 more (ComparisonOperator).
6254        check_metrics::<PythonParser>(
6255            "def f(c):\n    return 1 if c > 0 else 0\n",
6256            "foo.py",
6257            |metric| {
6258                assert_eq!(metric.abc.conditions_sum(), 2);
6259                insta::assert_json_snapshot!(metric.abc);
6260            },
6261        );
6262    }
6263
6264    // Issue #1161. Python counted the `conditional_expression` node but
6265    // never its condition slot, so `a if c() else b` reported 1 where
6266    // the equivalent `c() ? a : b` reports 2 everywhere else — and
6267    // `python_inspect_container`'s `ConditionalExpression` boolean-
6268    // context seed was unreachable, no call site having passed that
6269    // parent.
6270    #[test]
6271    fn python_ternary_condition_slot_counts_as_a_unary_condition() {
6272        // ternary (1) + condition `c()` (1) = 2. `c()` is a `Call`, a
6273        // boolean terminal; it also adds one *branch*, not a condition.
6274        check_metrics::<PythonParser>(
6275            "def f(a, b, c):\n    return a if c() else b\n",
6276            "foo.py",
6277            |metric| {
6278                assert_eq!(metric.abc.conditions_sum(), 2);
6279            },
6280        );
6281        // A parenthesised condition, pinning the seed line this fix made
6282        // reachable: `(c)` is a `parenthesized_expression`, so only
6283        // `python_inspect_container` can resolve it, and it counts the
6284        // unwrapped terminal only when the parent seeds boolean context.
6285        // ternary (1) + `(c)` (1) = 2.
6286        check_metrics::<PythonParser>(
6287            "def f(a, b, c):\n    return a if (c) else b\n",
6288            "foo.py",
6289            |metric| {
6290                assert_eq!(metric.abc.conditions_sum(), 2);
6291            },
6292        );
6293        // A negated condition is *not* counted by the new slot: it is a
6294        // `NotOperator`, which already has its own top-level dispatcher
6295        // arm. ternary (1) + `not c` (1) = 2, not 3.
6296        check_metrics::<PythonParser>(
6297            "def f(a, b, c):\n    return a if not c else b\n",
6298            "foo.py",
6299            |metric| {
6300                assert_eq!(metric.abc.conditions_sum(), 2);
6301            },
6302        );
6303        // The cross-language reference case. `(not b) if a else (not c)`
6304        // is the exact semantic equivalent of `a ? !b : !c`, which every
6305        // other language reports as 4: ternary (1) + condition `a` (1) +
6306        // two `NotOperator`s (2). The negated operands come from
6307        // Python's own arm, the condition from the slot added here.
6308        //
6309        // #1161's resolution plan predicted this would stay 3, having
6310        // measured the condition slot's contribution against the pre-fix
6311        // total. 3 would have left Python disagreeing with every other
6312        // language on the reference expression — the gap the issue was
6313        // filed about.
6314        check_metrics::<PythonParser>(
6315            "def f(a, b, c):\n    return (not b) if a else (not c)\n",
6316            "foo.py",
6317            |metric| {
6318                assert_eq!(metric.abc.conditions_sum(), 4);
6319            },
6320        );
6321    }
6322
6323    // The double-count pin, and the reason Python gets a condition-slot
6324    // helper rather than a copy of `cpp_walk_ternary`: Python's branch
6325    // operands are counted by the top-level `NotOperator` /
6326    // `ComparisonOperator` arms, a different mechanism from every other
6327    // language's walker. Routing the branch slots through
6328    // `python_inspect_container` as the C family does would count a
6329    // parenthesised operand that the identical unparenthesised
6330    // expression scores at zero.
6331    //
6332    // Both fixtures below are 2 today and 4 under such a copy, so a
6333    // later "make Python consistent with the others" change cannot land
6334    // silently.
6335    #[test]
6336    fn python_ternary_branch_operands_are_not_double_counted() {
6337        // ternary (1) + condition `a` (1) = 2. The two parenthesised
6338        // operands add nothing — an unnegated branch is type-free.
6339        check_metrics::<PythonParser>(
6340            "def f(a, b, c):\n    return (b) if a else (c)\n",
6341            "foo.py",
6342            |metric| {
6343                assert_eq!(metric.abc.conditions_sum(), 2);
6344            },
6345        );
6346        // The unparenthesised form must agree: nothing about `(b)`
6347        // versus `b` is a condition.
6348        check_metrics::<PythonParser>(
6349            "def f(a, b, c):\n    return b if a else c\n",
6350            "foo.py",
6351            |metric| {
6352                assert_eq!(metric.abc.conditions_sum(), 2);
6353            },
6354        );
6355    }
6356
6357    // Comments are tree-sitter `extras`, so they arrive as direct
6358    // children of `conditional_expression` and shift every positional
6359    // index after them. `python_count_ternary_condition` therefore
6360    // anchors on the `if` keyword and skips comments after it; both
6361    // halves are needed and each fixture below fails without one.
6362    #[test]
6363    fn python_ternary_condition_survives_an_interposed_comment() {
6364        // Comment before the keyword: `child(2)` is the `if` token here,
6365        // so a positional lookup reads 1. ternary (1) + `f()` (1) = 2.
6366        check_metrics::<PythonParser>(
6367            "def f(b, c):\n    return (b\n            # why\n            if f() else c)\n",
6368            "foo.py",
6369            |metric| {
6370                assert_eq!(metric.abc.conditions_sum(), 2);
6371            },
6372        );
6373        // Comment after the keyword: the child immediately following
6374        // `if` is the comment, so taking the first rather than the first
6375        // non-comment reads 1. ternary (1) + `f()` (1) = 2.
6376        check_metrics::<PythonParser>(
6377            "def f(b, c):\n    return (b if\n            # why\n            f() else c)\n",
6378            "foo.py",
6379            |metric| {
6380                assert_eq!(metric.abc.conditions_sum(), 2);
6381            },
6382        );
6383    }
6384
6385    #[test]
6386    fn python_try_except_finally_count_conditions() {
6387        // ExceptClause + FinallyClause → 2 conditions.
6388        check_metrics::<PythonParser>(
6389            "def f():\n    try:\n        pass\n    except ValueError:\n        pass\n    finally:\n        pass\n",
6390            "foo.py",
6391            |metric| {
6392                assert_eq!(metric.abc.conditions_sum(), 2);
6393                insta::assert_json_snapshot!(metric.abc);
6394            },
6395        );
6396    }
6397
6398    #[test]
6399    fn python_match_case_counts_conditions() {
6400        // Each non-wildcard `CaseClause` → 1 condition. The bare
6401        // `case _:` arm is the language-neutral `default:` equivalent
6402        // and is excluded (matches Rust's bare-`_` MatchArm filter and
6403        // Java/C#'s `default:` rule). Source has `case 1:` (counts) +
6404        // `case _:` (excluded) → C = 1.
6405        check_metrics::<PythonParser>(
6406            "def f(x):\n    match x:\n        case 1:\n            pass\n        case _:\n            pass\n",
6407            "foo.py",
6408            |metric| {
6409                assert_eq!(metric.abc.conditions_sum(), 1);
6410                insta::assert_json_snapshot!(metric.abc);
6411            },
6412        );
6413    }
6414
6415    #[test]
6416    fn python_match_case_guarded_wildcard_counts() {
6417        // `case _ if g:` is NOT a bare wildcard — the guard
6418        // contributes real branching, so the arm counts as a
6419        // condition. Mirrors Rust's `_ if g => ...` behavior.
6420        // Source: `case 1:` (counts) + `case _ if x > 0:` (guarded
6421        // wildcard, counts) + `case _:` (bare wildcard, excluded) →
6422        // C from CaseClause = 2; the guard's `x > 0` adds one
6423        // ComparisonOperator → total C = 3.
6424        check_metrics::<PythonParser>(
6425            "def f(x):\n    match x:\n        case 1:\n            pass\n        case _ if x > 0:\n            pass\n        case _:\n            pass\n",
6426            "foo.py",
6427            |metric| {
6428                assert_eq!(metric.abc.conditions_sum(), 3);
6429                insta::assert_json_snapshot!(metric.abc);
6430            },
6431        );
6432    }
6433
6434    #[test]
6435    fn python_complex_function_abc() {
6436        // Mixed-shape regression: assignments, calls, conditions all in
6437        // a single function.
6438        check_metrics::<PythonParser>(
6439            "def f(items, threshold):\n\
6440             \x20   result = []\n\
6441             \x20   for item in items:\n\
6442             \x20       if item > threshold:\n\
6443             \x20           result.append(item)\n\
6444             \x20   return result\n",
6445            "foo.py",
6446            |metric| {
6447                // assignments: `result = []` → 1
6448                // branches: `result.append(item)` is one call → 1
6449                // conditions: `item > threshold` is one
6450                // ComparisonOperator → 1
6451                assert_eq!(metric.abc.assignments_sum(), 1);
6452                assert_eq!(metric.abc.branches_sum(), 1);
6453                assert_eq!(metric.abc.conditions_sum(), 1);
6454                insta::assert_json_snapshot!(metric.abc);
6455            },
6456        );
6457    }
6458
6459    #[test]
6460    fn python_if_multiple_conditions() {
6461        // Fitzpatrick Rule 9 walker on `and` / `or` (issue #403).
6462        //   - `if a or b or c or d:` → 4 (each operand counted once)
6463        //   - `if a and b and c:`    → 3
6464        //   - `if not a and not b:`  → 2 (two `NotOperator`s counted
6465        //     by the top-level dispatcher arm; the walker SKIPS
6466        //     `NotOperator` children to avoid double-counting)
6467        // Total: 4 + 3 + 2 = 9.
6468        check_metrics::<PythonParser>(
6469            "def f(a, b, c, d):\n\
6470             \x20   if a or b or c or d:           # +4c\n\
6471             \x20       pass\n\
6472             \x20   if a and b and c:              # +3c\n\
6473             \x20       pass\n\
6474             \x20   if not a and not b:            # +2c (NotOperator x2)\n\
6475             \x20       pass\n",
6476            "foo.py",
6477            |metric| {
6478                assert_eq!(metric.abc.conditions_sum(), 9);
6479                insta::assert_json_snapshot!(metric.abc);
6480            },
6481        );
6482    }
6483
6484    #[test]
6485    fn python_while_conditions() {
6486        // Python has no `do { ... } while(cond);` construct, so this
6487        // mirrors only the `while` half of the Java suite. The
6488        // walker fires on each `and` / `or` token inside the loop
6489        // header.
6490        check_metrics::<PythonParser>(
6491            "def f(a, b):\n\
6492             \x20   while a or b:                  # +2c\n\
6493             \x20       break\n\
6494             \x20   while a and not b:             # +2c (a + NotOperator)\n\
6495             \x20       break\n",
6496            "foo.py",
6497            |metric| {
6498                assert_eq!(metric.abc.conditions_sum(), 4);
6499                insta::assert_json_snapshot!(metric.abc);
6500            },
6501        );
6502    }
6503
6504    #[test]
6505    fn python_short_circuit_with_boolean_literal_operand() {
6506        // `a and True` reports 2 conditions: one identifier, one
6507        // True literal. Confirms `True` / `False` are in the walker
6508        // terminal set.
6509        check_metrics::<PythonParser>("def f(a):\n    return a and True\n", "foo.py", |metric| {
6510            assert_eq!(metric.abc.conditions_sum(), 2);
6511            insta::assert_json_snapshot!(metric.abc);
6512        });
6513    }
6514
6515    #[test]
6516    fn python_await_expression_condition_counts() {
6517        // Regression for findings.md round-2 #2 (Python):
6518        // `if await ready(): pass` parses with `await` as the
6519        // condition node. Adding `Python::Await` to the
6520        // terminal-bool set mirrors the C# reference (lesson 19).
6521        check_metrics::<PythonParser>(
6522            "async def ready(): return True\n\
6523             async def f():\n    if await ready(): pass\n",
6524            "foo.py",
6525            |metric| {
6526                // ready() is a call (1 branch); await is the
6527                // condition (1).
6528                assert_eq!(metric.abc.branches_sum(), 1);
6529                assert_eq!(metric.abc.conditions_sum(), 1);
6530                insta::assert_json_snapshot!(metric.abc);
6531            },
6532        );
6533    }
6534
6535    #[test]
6536    fn python_if_call_terminal_condition_counts_once() {
6537        // Pins the Phase-2B behaviour for Python's `Call` terminal-bool
6538        // kind: `if foo():` is a Fitzpatrick Rule 6 unary conditional
6539        // (a bare boolean-evaluating call as the if-condition). The
6540        // walker's terminal-at-top check fires once per call-condition;
6541        // the call itself separately contributes 1 branch. Surfaced
6542        // (and verified intentional) by the code-review pass on
6543        // Phase 2B.
6544        check_metrics::<PythonParser>("def f():\n    if foo(): pass\n", "foo.py", |metric| {
6545            assert_eq!(metric.abc.branches_sum(), 1);
6546            assert_eq!(metric.abc.conditions_sum(), 1);
6547            insta::assert_json_snapshot!(metric.abc);
6548        });
6549    }
6550
6551    #[test]
6552    fn python_if_boolean_literal_condition() {
6553        // Phase 2B (issue #403): bare-boolean conditions count once.
6554        // Python has no paren wrap around if-conditions, so the
6555        // condition node is checked directly. The existing
6556        // NotOperator / ComparisonOperator arms continue to fire
6557        // for those shapes; only the bare-terminal cases (Identifier,
6558        // True, False, etc.) are added by the new arm.
6559        check_metrics::<PythonParser>(
6560            "def f(a):\n\
6561             \x20   if True: pass        # +1c\n\
6562             \x20   if False: pass       # +1c\n\
6563             \x20   while True: break    # +1c\n\
6564             \x20   if a: pass           # +1c (Rule 6 — bare identifier as condition)\n",
6565            "foo.py",
6566            |metric| {
6567                assert_eq!(metric.abc.conditions_sum(), 4);
6568                insta::assert_json_snapshot!(metric.abc);
6569            },
6570        );
6571    }
6572
6573    #[test]
6574    fn python_methods_arguments_with_conditions() {
6575        // `m(not a, not b)` reports 2 conditions — both `NotOperator`
6576        // nodes are counted by Python's pre-existing top-level
6577        // NotOperator dispatcher arm. The argument-list walker does
6578        // not need a separate Python arm.
6579        check_metrics::<PythonParser>(
6580            "def f(a, b):\n\
6581             \x20   m(a, b)             # +1b\n\
6582             \x20   m(not a, not b)     # +1b +2c\n",
6583            "foo.py",
6584            |metric| {
6585                assert_eq!(metric.abc.branches_sum(), 2);
6586                assert_eq!(metric.abc.conditions_sum(), 2);
6587                insta::assert_json_snapshot!(metric.abc);
6588            },
6589        );
6590    }
6591
6592    #[test]
6593    fn python_return_with_conditions() {
6594        // Phase 2B (issue #403). Python uses the pre-existing top-
6595        // level NotOperator / ComparisonOperator arms for return
6596        // expressions; no dedicated ReturnStatement walker arm is
6597        // needed.
6598        check_metrics::<PythonParser>(
6599            "def m1(z): return not (z >= 0)\n\
6600             def m2(x): return (((not x)))\n\
6601             def m3(x, y): return x and y\n",
6602            "foo.py",
6603            |metric| {
6604                // m1: NotOperator (1) + ComparisonOperator (1) = 2.
6605                // m2: NotOperator (1).
6606                // m3: walker on `and` counts both operands = 2.
6607                // Sum: 5.
6608                assert_eq!(metric.abc.conditions_sum(), 5);
6609                insta::assert_json_snapshot!(metric.abc);
6610            },
6611        );
6612    }
6613
6614    #[test]
6615    fn rust_empty_unit_zero() {
6616        // No code at all → A=B=C=0. Establishes the trait is wired up
6617        // and the per-language compute is reachable.
6618        check_metrics::<RustParser>("", "empty.rs", |metric| {
6619            assert_eq!(metric.abc.assignments_sum(), 0);
6620            assert_eq!(metric.abc.branches_sum(), 0);
6621            assert_eq!(metric.abc.conditions_sum(), 0);
6622            insta::assert_json_snapshot!(metric.abc);
6623        });
6624    }
6625
6626    #[test]
6627    fn rust_assignments_let_init_plain_and_compound() {
6628        // `let mut x = 0` is a `let_declaration` carrying an `=`
6629        // initializer → counts as 1 (matches Fitzpatrick's literal
6630        // "every `=` is an assignment" rule and the JS impl's
6631        // treatment of `let x = 5`). `x = 5` and `x = 7` are plain
6632        // `=` assignments → 2. `x += 2` is a compound assignment → 1.
6633        // Total A = 4.
6634        check_metrics::<RustParser>(
6635            "fn f() { let mut x = 0; x = 5; x += 2; x = 7; }",
6636            "foo.rs",
6637            |metric| {
6638                assert_eq!(metric.abc.assignments_sum(), 4);
6639                assert_eq!(metric.abc.branches_sum(), 0);
6640                assert_eq!(metric.abc.conditions_sum(), 0);
6641                insta::assert_json_snapshot!(metric.abc);
6642            },
6643        );
6644    }
6645
6646    #[test]
6647    fn rust_let_without_initializer_does_not_count() {
6648        // `let a;` is a `let_declaration` with NO `=` and no `value`
6649        // field — the binding is uninitialised. The arm only fires
6650        // when `value` is present, so this contributes zero to A.
6651        // `let _b;` is the same shape (the `_` pattern is still a
6652        // pattern, not a wildcard suppression of the binding).
6653        // Regression test for issue #393: only `=` counts, not the
6654        // bare declaration.
6655        check_metrics::<RustParser>(
6656            "fn f() { let a: i32; let _b: i32; a = 5; }",
6657            "foo.rs",
6658            |metric| {
6659                // Only `a = 5` (assignment_expression) → A = 1.
6660                assert_eq!(metric.abc.assignments_sum(), 1);
6661                insta::assert_json_snapshot!(metric.abc);
6662            },
6663        );
6664    }
6665
6666    #[test]
6667    fn rust_let_initializers_immutable_and_mutable_count() {
6668        // Issue #393: `let a = 1;`, `let b = 2;`, `let c = a + b;`,
6669        // `let mut d = 0;` are all `let_declaration` nodes carrying
6670        // an `=` initializer — each counts as 1 (Option B in the
6671        // issue body: literal Fitzpatrick, both `let` and `let mut`
6672        // count). `d = 5;` is one plain assignment_expression, `d
6673        // += 1;` is one compound. Total A = 4 + 1 + 1 = 6.
6674        check_metrics::<RustParser>(
6675            "fn f() { let a=1; let b=2; let c=a+b; let mut d=0; d=5; d+=1; }",
6676            "foo.rs",
6677            |metric| {
6678                assert_eq!(metric.abc.assignments_sum(), 6);
6679                insta::assert_json_snapshot!(metric.abc);
6680            },
6681        );
6682    }
6683
6684    #[test]
6685    fn rust_calls_are_branches() {
6686        // Free function call + method call (parses as call_expression
6687        // with a field_expression callee) + associated-fn call. All
6688        // three are `call_expression` → B = 3. Macro invocations like
6689        // `println!` parse as `macro_invocation`, NOT `call_expression`,
6690        // so they are not branches.
6691        check_metrics::<RustParser>(
6692            "fn f() { g(); 1.to_string(); String::new(); }\nfn g() {}\n",
6693            "foo.rs",
6694            |metric| {
6695                assert_eq!(metric.abc.branches_sum(), 3);
6696                assert_eq!(metric.abc.assignments_sum(), 0);
6697                assert_eq!(metric.abc.conditions_sum(), 0);
6698                insta::assert_json_snapshot!(metric.abc);
6699            },
6700        );
6701    }
6702
6703    #[test]
6704    fn rust_try_operator_is_branch() {
6705        // `?` parses as `try_expression` and counts as one branch
6706        // (short-circuit return on Err / None). The `Err(())` call
6707        // contributes one branch in addition (call_expression).
6708        check_metrics::<RustParser>(
6709            "fn f() -> Result<i32, ()> { let r: Result<i32, ()> = Err(()); Ok(r?) }",
6710            "foo.rs",
6711            |metric| {
6712                // Err(()) + Ok(...) + r? → 2 calls + 1 try = 3 branches.
6713                assert_eq!(metric.abc.branches_sum(), 3);
6714                insta::assert_json_snapshot!(metric.abc);
6715            },
6716        );
6717    }
6718
6719    #[test]
6720    fn rust_comparisons_count_conditions() {
6721        // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six
6722        // comparisons → C = 6.
6723        check_metrics::<RustParser>(
6724            "fn f(a: i32, b: i32) -> bool { a < b || a > b || a <= b || a >= b || a == b || a != b }",
6725            "foo.rs",
6726            |metric| {
6727                assert_eq!(metric.abc.conditions_sum(), 6);
6728                insta::assert_json_snapshot!(metric.abc);
6729            },
6730        );
6731    }
6732
6733    #[test]
6734    fn rust_generic_brackets_not_conditions() {
6735        // `<` / `>` in `Vec<i32>` are TypeArguments delimiters, not
6736        // comparison operators. The parent-check in the LT/GT arms
6737        // must filter them out. Expected C = 0.
6738        check_metrics::<RustParser>(
6739            "fn f() -> Vec<i32> { Vec::<i32>::new() }",
6740            "foo.rs",
6741            |metric| {
6742                assert_eq!(metric.abc.conditions_sum(), 0);
6743                insta::assert_json_snapshot!(metric.abc);
6744            },
6745        );
6746    }
6747
6748    #[test]
6749    fn rust_if_let_counts_as_condition() {
6750        // `if let Some(v) = opt { ... }` introduces a `let_condition`
6751        // → 1 condition. The `if` keyword itself does not add another
6752        // count — Fitzpatrick counts conditions, not branch keywords.
6753        check_metrics::<RustParser>(
6754            "fn f(opt: Option<i32>) { if let Some(_v) = opt { } }",
6755            "foo.rs",
6756            |metric| {
6757                assert_eq!(metric.abc.conditions_sum(), 1);
6758                insta::assert_json_snapshot!(metric.abc);
6759            },
6760        );
6761    }
6762
6763    #[test]
6764    fn rust_while_let_counts_as_condition() {
6765        // `while let Some(y) = it.next() { ... }` is also a
6766        // `let_condition` (the `while` form). One condition; the
6767        // `it.next()` call adds one branch.
6768        check_metrics::<RustParser>(
6769            "fn f(mut it: std::vec::IntoIter<i32>) { while let Some(_y) = it.next() { } }",
6770            "foo.rs",
6771            |metric| {
6772                assert_eq!(metric.abc.conditions_sum(), 1);
6773                assert_eq!(metric.abc.branches_sum(), 1);
6774                insta::assert_json_snapshot!(metric.abc);
6775            },
6776        );
6777    }
6778
6779    #[test]
6780    fn rust_match_arms_count_conditions_wildcard_excluded() {
6781        // Three arms: `0 => 1`, `n if n > 0 => n`, `_ => -1`. The
6782        // bare wildcard is the `default:` equivalent and is skipped.
6783        // The guarded arm has a `n if n > 0` pattern (more than one
6784        // child in the match_pattern) and still counts. Two non-wildcard
6785        // arms → C = 2 from MatchArm. Plus the comparison `n > 0`
6786        // adds one more → C = 3.
6787        check_metrics::<RustParser>(
6788            "fn f(x: i32) -> i32 { match x { 0 => 1, n if n > 0 => n, _ => -1, } }",
6789            "foo.rs",
6790            |metric| {
6791                assert_eq!(metric.abc.conditions_sum(), 3);
6792                insta::assert_json_snapshot!(metric.abc);
6793            },
6794        );
6795    }
6796
6797    #[test]
6798    fn rust_else_counts_as_condition() {
6799        // `if a > b { ... } else { ... }` → `a > b` is one condition,
6800        // `else` is one condition → C = 2.
6801        check_metrics::<RustParser>(
6802            "fn f(a: i32, b: i32) -> i32 { if a > b { a } else { b } }",
6803            "foo.rs",
6804            |metric| {
6805                assert_eq!(metric.abc.conditions_sum(), 2);
6806                insta::assert_json_snapshot!(metric.abc);
6807            },
6808        );
6809    }
6810
6811    #[test]
6812    fn rust_let_chain2_hidden_rule_drift_marker() {
6813        // Drift marker (findings.md round-2 #3): `Rust::LetChain2`
6814        // maps to the hidden grammar rule `_let_chain`. At the
6815        // pinned tree-sitter-rust version it is never emitted as a
6816        // concrete node — the visible `LetChain` (= 352) carries
6817        // every let-chain. We list `LetChain2` defensively in
6818        // `rust_inspect_container` and `rust_count_unary_conditions`
6819        // (lesson 34); if a future grammar bump promotes
6820        // `_let_chain` to a visible rule, this assertion fails
6821        // loudly so the maintainer knows to verify the walker still
6822        // counts correctly for the new shape.
6823        let src = "fn f(a: bool, b: Option<i32>) {\n\
6824                   \x20   if a && let Some(_) = b { }\n\
6825                   }\n";
6826        let parser = RustParser::new(
6827            src.as_bytes().to_vec(),
6828            &std::path::PathBuf::from("foo.rs"),
6829            None,
6830        );
6831        assert!(!ast_has_kind_id(&parser, Rust::LetChain2 as u16));
6832    }
6833
6834    #[test]
6835    fn rust_scoped_identifier_condition_counts() {
6836        // Regression for findings.md round-2 #1 (Rust):
6837        // `if crate::FLAG {}` parses with `scoped_identifier` as the
6838        // condition node. Pre-fix, `rust_bool_terminal_kinds!()`
6839        // listed only `Identifier` so the walker reached the
6840        // `scoped_identifier` child, found it non-terminal /
6841        // non-paren / non-unary, and broke without counting.
6842        // Mirrors the C# fix in #372 (lesson 19) for
6843        // `MemberAccessExpression`.
6844        check_metrics::<RustParser>("fn f() { if crate::FLAG { } }\n", "foo.rs", |metric| {
6845            assert_eq!(metric.abc.conditions_sum(), 1);
6846            insta::assert_json_snapshot!(metric.abc);
6847        });
6848    }
6849
6850    #[test]
6851    fn rust_await_expression_condition_counts() {
6852        // Regression for findings.md round-2 #2 (Rust):
6853        // `if ready().await {}` parses with `await_expression` as
6854        // the condition node. Adding `Rust::AwaitExpression` to the
6855        // terminal-bool set closes the parity gap with the C#
6856        // reference (`csharp_bool_terminal_kinds!()`).
6857        check_metrics::<RustParser>(
6858            "async fn ready() -> bool { true }\n\
6859             async fn f() { if ready().await { } }\n",
6860            "foo.rs",
6861            |metric| {
6862                // ready() is a call (1 branch); `ready().await` is
6863                // the unary boolean condition (1).
6864                assert_eq!(metric.abc.branches_sum(), 1);
6865                assert_eq!(metric.abc.conditions_sum(), 1);
6866                insta::assert_json_snapshot!(metric.abc);
6867            },
6868        );
6869    }
6870
6871    #[test]
6872    fn rust_complex_function_abc() {
6873        // Mixed-shape regression: assignments, calls, conditions, `?`,
6874        // `if let`, `match` in one body. Verified by hand:
6875        // - assignments: `let mut x = 0` (let init), `x = 5`, `x += 2`,
6876        //   `let _ = ...` (let init), `let r: ... = Err(())` (let init),
6877        //   `let _v = r?` (let init) → A = 6 (post-#393: every `=`
6878        //   initializer in a `let_declaration` is one assignment, in
6879        //   line with the literal Fitzpatrick reading).
6880        // - branches: `xs.iter()`, `.next()`, `Err(())`, `r?` → B = 4
6881        //   (3 calls + 1 try).
6882        // - conditions: `if let Some(v) = opt` → 1, `match x` arms
6883        //   `0`, `n if n>0` (wildcard excluded) → 2, `n > 0` → 1.
6884        //   Total C = 4.
6885        check_metrics::<RustParser>(
6886            "fn f(opt: Option<i32>, xs: Vec<i32>) -> Result<i32, ()> {\n\
6887             \x20   let mut x = 0;\n\
6888             \x20   x = 5;\n\
6889             \x20   x += 2;\n\
6890             \x20   if let Some(_v) = opt { }\n\
6891             \x20   let _ = xs.iter().next();\n\
6892             \x20   let r: Result<i32, ()> = Err(());\n\
6893             \x20   let _v = r?;\n\
6894             \x20   Ok(match x {\n\
6895             \x20       0 => 1,\n\
6896             \x20       n if n > 0 => n,\n\
6897             \x20       _ => -1,\n\
6898             \x20   })\n\
6899             }\n",
6900            "foo.rs",
6901            |metric| {
6902                assert_eq!(metric.abc.assignments_sum(), 6);
6903                // calls: xs.iter(), .next(), Err(()), Ok(...) → 4 calls
6904                // plus 1 try (`r?`) → 5 branches.
6905                assert_eq!(metric.abc.branches_sum(), 5);
6906                // 1 let_condition + 2 non-wildcard match_arms + 1
6907                // comparison (`n > 0`) → 4.
6908                assert_eq!(metric.abc.conditions_sum(), 4);
6909                insta::assert_json_snapshot!(metric.abc);
6910            },
6911        );
6912    }
6913
6914    #[test]
6915    fn rust_let_chain_bare_identifier_operand_counts() {
6916        // Regression: pre-fix, `if a && let Some(_z) = y { }` reported
6917        // 1 condition (only the LetCondition). The bare-identifier
6918        // `a` operand was lost because Rust 2024 wraps let-chain
6919        // `&&` operands in a `LetChain` node (not `BinaryExpression`)
6920        // and `rust_count_unary_conditions` only counted terminals
6921        // under a `BinaryExpression` parent. Allowing `LetChain` /
6922        // `LetChain2` as known-bool list parents fixes the loss.
6923        // Expected: LetCondition (1) + walker on `a` (1) = 2.
6924        check_metrics::<RustParser>(
6925            "fn f(a: bool, y: Option<i32>) {\n\
6926             \x20   if a && let Some(_z) = y { }\n\
6927             }\n",
6928            "foo.rs",
6929            |metric| {
6930                assert_eq!(metric.abc.conditions_sum(), 2);
6931                insta::assert_json_snapshot!(metric.abc);
6932            },
6933        );
6934    }
6935
6936    #[test]
6937    fn rust_if_multiple_conditions() {
6938        // Fitzpatrick Rule 7 / Listing 2 (issue #403): every operand of
6939        // a `&&` / `||` chain is one condition. Mirrors
6940        // `java_if_multiple_conditions`. Rust's `if` head has no
6941        // parentheses, but the walker fires on each `&&` / `||` token
6942        // and walks the parent `binary_expression` regardless.
6943        check_metrics::<RustParser>(
6944            "fn f(a: bool, b: bool, c: bool, d: bool) -> i32 {\n\
6945             \x20   if a || b || c || d { return 1; }    // +4c\n\
6946             \x20   if a && b && c { return 2; }         // +3c\n\
6947             \x20   if !a && !b { return 3; }            // +2c\n\
6948             \x20   0\n\
6949             }\n",
6950            "foo.rs",
6951            |metric| {
6952                // 4 + 3 + 2 = 9
6953                assert_eq!(metric.abc.conditions_sum(), 9);
6954                insta::assert_json_snapshot!(metric.abc);
6955            },
6956        );
6957    }
6958
6959    #[test]
6960    fn rust_while_conditions() {
6961        // Rust has no `do { ... } while(cond);` construct, so this
6962        // mirrors only the `while` half of `java_while_and_do_while_conditions`.
6963        // Each operand of the `&&` / `||` chain in the loop condition
6964        // counts as one Fitzpatrick condition (Rule 7).
6965        check_metrics::<RustParser>(
6966            "fn f(a: bool, b: bool) {\n\
6967             \x20   while a || b { break; }       // +2c\n\
6968             \x20   while a && !b { break; }      // +2c\n\
6969             }\n",
6970            "foo.rs",
6971            |metric| {
6972                assert_eq!(metric.abc.conditions_sum(), 4);
6973                insta::assert_json_snapshot!(metric.abc);
6974            },
6975        );
6976    }
6977
6978    #[test]
6979    fn rust_if_boolean_literal_condition() {
6980        // Phase 2B (issue #403): a condition whose entire body is a
6981        // boolean literal counts as one Fitzpatrick condition.
6982        // `if true {}` → 1, `if !false {}` → 1 (unary unwrap), and
6983        // `while true { break }` → 1.
6984        check_metrics::<RustParser>(
6985            "fn f() {\n\
6986             \x20   if true { }                  // +1c\n\
6987             \x20   if !false { }                // +1c\n\
6988             \x20   while true { break; }        // +1c\n\
6989             }\n",
6990            "foo.rs",
6991            |metric| {
6992                assert_eq!(metric.abc.conditions_sum(), 3);
6993                insta::assert_json_snapshot!(metric.abc);
6994            },
6995        );
6996    }
6997
6998    #[test]
6999    fn rust_methods_arguments_with_conditions() {
7000        // Phase 2B (issue #403): unary-conditional arguments to a
7001        // call each count once. `m(!a, !b)` → 2 conditions + 1
7002        // branch (the call itself). Bare identifier arguments do
7003        // NOT count (they reach the count_unary_conditions list with
7004        // list_kind = Arguments, not BinaryExpression).
7005        check_metrics::<RustParser>(
7006            "fn f(a: bool, b: bool) {\n\
7007             \x20   m(a, b);                     // +1b\n\
7008             \x20   m(!a, !b);                   // +1b +2c\n\
7009             \x20   m(!a, b, !a);                // +1b +2c\n\
7010             }\n",
7011            "foo.rs",
7012            |metric| {
7013                assert_eq!(metric.abc.branches_sum(), 3);
7014                assert_eq!(metric.abc.conditions_sum(), 4);
7015                insta::assert_json_snapshot!(metric.abc);
7016            },
7017        );
7018    }
7019
7020    #[test]
7021    fn rust_return_with_conditions() {
7022        // Phase 2B (issue #403). Mirrors `java_return_with_conditions`
7023        // — `return !a` / `return x && y` count their unary
7024        // conditional operands. Per Fitzpatrick Rule 7, a `!`-wrapped
7025        // relational expression contributes ONE condition (the
7026        // relational op itself) — the `!` does not add a second
7027        // count when its operand is already a comparison.
7028        check_metrics::<RustParser>(
7029            "fn m1(z: i32) -> bool { return !(z >= 0); }\n\
7030             fn m2(x: bool) -> bool { return (((!x))); }\n\
7031             fn m3(x: bool, y: bool) -> bool { return x && y; }\n\
7032             fn m4(y: bool, z: i32) -> bool { return y || (z < 0); }\n",
7033            "foo.rs",
7034            |metric| {
7035                // m1: !(z >= 0) → the `>=` contributes 1; the unary
7036                //     `!` wraps a paren'd BinaryExpression, which
7037                //     inspect_container does not unwrap further →
7038                //     no walker count. Total: 1.
7039                // m2: (((!x))) → ReturnExpression arm walks (((!x))).
7040                //     inspect_container unwraps three parens and one
7041                //     unary, reaches Identifier `x`, has_boolean_content
7042                //     was seeded true by the unary-not flip. +1.
7043                // m3: x && y → `&&` walker counts both terminals → 2.
7044                // m4: y || (z < 0) → `||` walker counts `y` (terminal,
7045                //     +1); the `<` contributes 1 via its own arm; the
7046                //     paren'd BinaryExpression `(z < 0)` is not
7047                //     terminal under the walker → no extra count.
7048                //     Total: 2.
7049                // Sum: 1 + 1 + 2 + 2 = 6.
7050                assert_eq!(metric.abc.conditions_sum(), 6);
7051                insta::assert_json_snapshot!(metric.abc);
7052            },
7053        );
7054    }
7055
7056    #[test]
7057    fn rust_short_circuit_with_boolean_literal_operand() {
7058        // `if a && true` reports 2 conditions: one for the identifier
7059        // operand, one for the boolean-literal operand. Confirms the
7060        // walker terminal set includes `BooleanLiteral`.
7061        check_metrics::<RustParser>(
7062            "fn f(a: bool) -> bool { a && true }\n",
7063            "foo.rs",
7064            |metric| {
7065                assert_eq!(metric.abc.conditions_sum(), 2);
7066                insta::assert_json_snapshot!(metric.abc);
7067            },
7068        );
7069    }
7070
7071    // ----- Go -----
7072
7073    #[test]
7074    fn go_empty_unit_zero() {
7075        // Package declaration only — no Fitzpatrick events. Confirms the
7076        // GoCode Abc trait is wired up and emits zero counts.
7077        check_metrics::<GoParser>("package main\n", "empty.go", |metric| {
7078            assert_eq!(metric.abc.assignments_sum(), 0);
7079            assert_eq!(metric.abc.branches_sum(), 0);
7080            assert_eq!(metric.abc.conditions_sum(), 0);
7081            insta::assert_json_snapshot!(metric.abc);
7082        });
7083    }
7084
7085    #[test]
7086    fn go_assignments_count_plain_compound_short_var_and_incdec() {
7087        // `x := 0` (short var decl), `x = 5` and `x = 7` (plain `=`),
7088        // `x += 2` (compound), `x++` (inc), and the initialized
7089        // declaration `var y = 1` — which is counted, matching the Rust
7090        // and Java rules for `let y = 1` / `int y = 1` (both measured at
7091        // one assignment each). The comment here previously claimed the
7092        // opposite and pinned Go at 6 (#1278).
7093        check_metrics::<GoParser>(
7094            "package main\nfunc f() { var y = 1; _ = y; x := 0; x = 5; x += 2; x = 7; x++ }\n",
7095            "foo.go",
7096            |metric| {
7097                // `_ = y` is itself an assignment_statement → +1.
7098                // var y=1 + _=y + x:= + x=5 + x+=2 + x=7 + x++ → 7
7099                assert_eq!(metric.abc.assignments_sum(), 7);
7100                assert_eq!(metric.abc.branches_sum(), 0);
7101                assert_eq!(metric.abc.conditions_sum(), 0);
7102                insta::assert_json_snapshot!(metric.abc);
7103            },
7104        );
7105    }
7106
7107    #[test]
7108    fn go_var_declarations_count_only_when_initialized() {
7109        // Regression for #1278: a `var` declaration with an initializer is
7110        // a `var_spec` carrying a `value` field, not an
7111        // `assignment_statement` or `short_var_declaration`, so it scored
7112        // zero — `var x = 5` and `x := 5` are the same binding spelled two
7113        // ways. Both typed and untyped initializers count; an
7114        // uninitialized `var z int` and a `const` do not.
7115        // expected: 3 assignments — `var x = 5`, `var y int = 6`, `z := 7`;
7116        // `var w int` and `const c = 1` contribute nothing.
7117        check_metrics::<GoParser>(
7118            "package main\nfunc f() int {\n\tvar x = 5\n\tvar y int = 6\n\tvar w int\n\tconst c = 1\n\tz := 7\n\treturn x + y + w + c + z\n}\n",
7119            "foo.go",
7120            |metric| {
7121                assert_eq!(metric.abc.assignments_sum(), 3);
7122                assert_eq!(metric.abc.branches_sum(), 0);
7123            },
7124        );
7125    }
7126
7127    #[test]
7128    fn go_grouped_var_block_counts_each_initialized_spec() {
7129        // A grouped `var ( … )` block is one `var_declaration` holding one
7130        // `var_spec` per line, so matching the spec counts each initialized
7131        // line on its own. A multi-name spec is still one binding
7132        // statement, matching `p, q := 1, 2` (#1278).
7133        // expected: 3 assignments — `a = 1`, `p, q = 1, 2`, and `r := 0`;
7134        // the uninitialized `b int` contributes nothing.
7135        check_metrics::<GoParser>(
7136            "package main\nfunc f() {\n\tvar (\n\t\ta = 1\n\t\tb int\n\t)\n\tvar p, q = 1, 2\n\tr := 0\n\t_ = a + b + p + q + r\n}\n",
7137            "foo.go",
7138            |metric| {
7139                // The trailing `_ = …` is itself an assignment_statement.
7140                assert_eq!(metric.abc.assignments_sum(), 4);
7141            },
7142        );
7143    }
7144
7145    #[test]
7146    fn go_calls_are_branches() {
7147        // Three calls: free function `g()`, method call `r.Inc()`, and
7148        // builtin call `len(s)`. All parse as `call_expression` → B = 3.
7149        // Composite literal `Foo{}` is NOT a call.
7150        check_metrics::<GoParser>(
7151            "package main\n\
7152             type R struct{}\n\
7153             func (r R) Inc() {}\n\
7154             func g() {}\n\
7155             func f(s string) { g(); var r R = R{}; r.Inc(); _ = len(s) }\n",
7156            "foo.go",
7157            |metric| {
7158                assert_eq!(metric.abc.branches_sum(), 3);
7159                insta::assert_json_snapshot!(metric.abc);
7160            },
7161        );
7162    }
7163
7164    #[test]
7165    fn go_comparisons_count_conditions() {
7166        // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six
7167        // comparisons → C = 6.
7168        check_metrics::<GoParser>(
7169            "package main\nfunc f(a, b int) bool { return a < b || a > b || a <= b || a >= b || a == b || a != b }\n",
7170            "foo.go",
7171            |metric| {
7172                assert_eq!(metric.abc.conditions_sum(), 6);
7173                insta::assert_json_snapshot!(metric.abc);
7174            },
7175        );
7176    }
7177
7178    #[test]
7179    fn go_generic_brackets_not_conditions() {
7180        // Generic instantiation `Min[int](a, b)` puts `int` inside
7181        // `TypeArguments`, not `BinaryExpression`. The parent guard on
7182        // `<` / `>` must not count these. Expected C = 0; B = 1 (one call).
7183        check_metrics::<GoParser>(
7184            "package main\nfunc Min[T int | float64](a, b T) T { return a }\nfunc f() { _ = Min[int](1, 2) }\n",
7185            "foo.go",
7186            |metric| {
7187                assert_eq!(metric.abc.conditions_sum(), 0);
7188                assert_eq!(metric.abc.branches_sum(), 1);
7189                insta::assert_json_snapshot!(metric.abc);
7190            },
7191        );
7192    }
7193
7194    #[test]
7195    fn go_switch_arms_count_conditions_default_excluded() {
7196        // Four arms: `case 1:`, `case 2:`, `case 3:`, `default:`. The
7197        // bare `default` is the C/Java `default:` equivalent and is
7198        // excluded — 3 conditions from ExpressionCase. The switch
7199        // expression `x` is bare (no comparison), so no extra
7200        // condition from `==`-style operators.
7201        check_metrics::<GoParser>(
7202            "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",
7203            "foo.go",
7204            |metric| {
7205                assert_eq!(metric.abc.conditions_sum(), 3);
7206                insta::assert_json_snapshot!(metric.abc);
7207            },
7208        );
7209    }
7210
7211    #[test]
7212    fn go_type_switch_arms_count_conditions() {
7213        // Type switch: `case int:`, `case string:`, `default:`. Two
7214        // non-default type-case arms → C = 2.
7215        check_metrics::<GoParser>(
7216            "package main\nfunc f(v interface{}) { switch v.(type) { case int: return; case string: return; default: return } }\n",
7217            "foo.go",
7218            |metric| {
7219                assert_eq!(metric.abc.conditions_sum(), 2);
7220                insta::assert_json_snapshot!(metric.abc);
7221            },
7222        );
7223    }
7224
7225    #[test]
7226    fn go_select_arms_count_conditions() {
7227        // `select { case <-ch: ...; case ch <- 1: ...; default: ... }`.
7228        // Two non-default communication cases → C = 2.
7229        check_metrics::<GoParser>(
7230            "package main\nfunc f(ch chan int) { select { case <-ch: return; case ch <- 1: return; default: return } }\n",
7231            "foo.go",
7232            |metric| {
7233                assert_eq!(metric.abc.conditions_sum(), 2);
7234                insta::assert_json_snapshot!(metric.abc);
7235            },
7236        );
7237    }
7238
7239    #[test]
7240    fn go_else_counts_as_condition() {
7241        // `if a > b { ... } else { ... }` → `a > b` is one condition,
7242        // `else` is one condition → C = 2.
7243        check_metrics::<GoParser>(
7244            "package main\nfunc f(a, b int) int { if a > b { return a } else { return b } }\n",
7245            "foo.go",
7246            |metric| {
7247                assert_eq!(metric.abc.conditions_sum(), 2);
7248                insta::assert_json_snapshot!(metric.abc);
7249            },
7250        );
7251    }
7252
7253    #[test]
7254    fn go_complex_function_abc() {
7255        // Mixed shape, verified by hand:
7256        // - Assignments: `var x = 10` (an initialized declaration, #1278),
7257        //   `_ = x`, `n := 0`, `n = n + 1`, `n += 2`, `n++`,
7258        //   `_ = len(s)` → A = 7. Every `_ = ...` IS counted as an
7259        //   assignment_statement.
7260        // - Branches: `len(s)` → B = 1.
7261        // - Conditions: `n < 10` → 1, `else` → 1, switch arms `case 0:`
7262        //   and `case 1:` (default excluded) → 2 → total C = 4.
7263        check_metrics::<GoParser>(
7264            "package main\nfunc f(s string) int {\n\
7265             \x20   var x = 10\n\
7266             \x20   _ = x\n\
7267             \x20   n := 0\n\
7268             \x20   if n < 10 { n = n + 1 } else { n += 2 }\n\
7269             \x20   n++\n\
7270             \x20   _ = len(s)\n\
7271             \x20   switch n {\n\
7272             \x20   case 0: return 0\n\
7273             \x20   case 1: return 1\n\
7274             \x20   default: return n\n\
7275             \x20   }\n\
7276             }\n",
7277            "foo.go",
7278            |metric| {
7279                assert_eq!(metric.abc.assignments_sum(), 7);
7280                assert_eq!(metric.abc.branches_sum(), 1);
7281                assert_eq!(metric.abc.conditions_sum(), 4);
7282                insta::assert_json_snapshot!(metric.abc);
7283            },
7284        );
7285    }
7286
7287    #[test]
7288    fn go_if_multiple_conditions() {
7289        // Fitzpatrick Rule 7 walker fan-out (issue #403). Mirrors
7290        // `rust_if_multiple_conditions`.
7291        check_metrics::<GoParser>(
7292            "package p\n\
7293             func F(a, b, c, d bool) int {\n\
7294             \x20   if a || b || c || d { return 1 }    // +4c\n\
7295             \x20   if a && b && c { return 2 }         // +3c\n\
7296             \x20   if !a && !b { return 3 }            // +2c\n\
7297             \x20   return 0\n\
7298             }\n",
7299            "foo.go",
7300            |metric| {
7301                assert_eq!(metric.abc.conditions_sum(), 9);
7302                insta::assert_json_snapshot!(metric.abc);
7303            },
7304        );
7305    }
7306
7307    #[test]
7308    fn go_for_with_conditions() {
7309        // Go has no `while` or `do { … } while(…);` — the `for` loop
7310        // header is the sole condition slot. Each operand of the
7311        // `&&` / `||` chain in the for-condition counts as one
7312        // Fitzpatrick condition.
7313        check_metrics::<GoParser>(
7314            "package p\n\
7315             func F(a, b bool) {\n\
7316             \x20   for a || b { break }       // +2c\n\
7317             \x20   for a && !b { break }      // +2c\n\
7318             }\n",
7319            "foo.go",
7320            |metric| {
7321                assert_eq!(metric.abc.conditions_sum(), 4);
7322                insta::assert_json_snapshot!(metric.abc);
7323            },
7324        );
7325    }
7326
7327    #[test]
7328    fn go_for_bare_condition_counts() {
7329        // Regression for findings.md #1: `for true {}` / `for !ready {}`
7330        // are Go's only loop-condition slot. Pre-fix, the Phase-2B
7331        // dispatcher had no `G::ForStatement` arm, so bare-boolean
7332        // and `!`-wrapped `for` conditions silently reported zero.
7333        // `go_count_condition`'s terminal-bool / paren / unary filter
7334        // makes the walker safe across all three for-statement shapes:
7335        // bare condition, `for_clause` (init; cond; post) — whose own
7336        // `condition` field #1276 taught the walker to read — and
7337        // `range_clause`, which has no such field and contributes
7338        // nothing.
7339        check_metrics::<GoParser>(
7340            "package p\n\
7341             func F(ready bool) {\n\
7342             \x20   for true { break }      // +1c\n\
7343             \x20   for !ready { break }    // +1c\n\
7344             \x20   for i := 0; i < 3; i++ { _ = i }    // +1c (the `<`)\n\
7345             }\n",
7346            "foo.go",
7347            |metric| {
7348                // `for true`: walker counts True (+1).
7349                // `for !ready`: walker on unary unwraps to `ready`
7350                //   (+1).
7351                // `for_clause`'s condition is `i < 3`, a
7352                //   `binary_expression` that `go_count_condition`
7353                //   filters out; the `<` itself contributes 1 via the
7354                //   pre-existing LT/GT arm.
7355                // Total: 3.
7356                assert_eq!(metric.abc.conditions_sum(), 3);
7357                insta::assert_json_snapshot!(metric.abc);
7358            },
7359        );
7360    }
7361
7362    // Issue #1276, Go's share. `for_statement`'s child(1) is the
7363    // condition only in the `for cond {}` spelling; the three-clause
7364    // form puts it one level down, in the `for_clause`'s `condition`
7365    // field. Letting the `for_clause` fall through — which the arm's
7366    // own comment used to call harmless — scored a bare three-clause
7367    // condition zero while `for a {}` scored one.
7368    #[test]
7369    fn go_three_clause_for_condition_counts() {
7370        // Bare identifier in the three-clause header: no comparison
7371        // token, so only the `for_clause` lookup can count it.
7372        check_metrics::<GoParser>(
7373            "package p\n\
7374             func F(a bool) {\n\
7375             \x20   for i := 0; a; i++ { _ = i }\n\
7376             }\n",
7377            "foo.go",
7378            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
7379        );
7380        // Negation, through `go_inspect_container`'s `!` unwrap.
7381        check_metrics::<GoParser>(
7382            "package p\n\
7383             func F(a bool) {\n\
7384             \x20   for i := 0; !a; i++ { _ = i }\n\
7385             }\n",
7386            "foo.go",
7387            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
7388        );
7389        // Empty condition: the `for_clause` exposes no `condition`
7390        // field, so zero — the same answer as Go's bare `for {}` and
7391        // as every other language since #1276.
7392        check_metrics::<GoParser>(
7393            "package p\n\
7394             func F() {\n\
7395             \x20   for i := 0; ; i++ { break }\n\
7396             }\n",
7397            "foo.go",
7398            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
7399        );
7400        // A `range_clause` carries no condition either, and the walker
7401        // must not mistake the clause itself for one.
7402        check_metrics::<GoParser>(
7403            "package p\n\
7404             func F(xs []int) {\n\
7405             \x20   for _, v := range xs { _ = v }\n\
7406             }\n",
7407            "foo.go",
7408            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
7409        );
7410    }
7411
7412    // Go's `for_statement` is the one grammar here that exposes no
7413    // `condition` field, so its header slot is located structurally and
7414    // has to skip what the field-addressed siblings get for free. Two
7415    // things it must skip, each of which `node.child(1)` got wrong:
7416    // a leading comment (tree-sitter counts comments among a node's
7417    // children — the #1181 failure), and the body of a bare `for {}`,
7418    // which IS child(1) and would otherwise be offered to
7419    // `go_count_condition` as though it were a condition.
7420    #[test]
7421    fn go_for_header_slot_skips_comments_and_the_body() {
7422        // Each pair is (source, expected conditions). The commented
7423        // spelling must agree with its bare twin.
7424        let cases = [
7425            ("for a { break }", 1),
7426            ("for /* n */ a { break }", 1),
7427            ("for i := 0; a; i++ { _ = i }", 1),
7428            ("for /* n */ i := 0; a; i++ { _ = i }", 1),
7429            ("for i := 0; /* n */ a; i++ { _ = i }", 1),
7430            // Bare infinite loop: the body is child(1) and must not be
7431            // read as the condition.
7432            ("for { break }", 0),
7433            ("for /* n */ { break }", 0),
7434            ("for _, v := range xs { _ = v }", 0),
7435            ("for /* n */ _, v := range xs { _ = v }", 0),
7436        ];
7437        let mut ran = 0;
7438        for (body, expected) in cases {
7439            let src = format!("package p\nfunc F(a bool, xs []int) {{\n\t{body}\n}}\n");
7440            assert_eq!(abc_conditions(LANG::Go, &src), expected, "`{body}`");
7441            ran += 1;
7442        }
7443        // Non-vacuity, both halves: the loop must actually have run
7444        // every row, and the rows must carry both answers — a walker
7445        // stuck at 0 or at 1 would otherwise pass half the table
7446        // silently.
7447        assert_eq!(ran, cases.len());
7448        assert!(cases.iter().any(|&(_, n)| n == 1));
7449        assert!(cases.iter().any(|&(_, n)| n == 0));
7450    }
7451
7452    #[test]
7453    fn go_if_init_statement_condition_counts() {
7454        // Regression for the code-review finding: Go's
7455        // `if x := f(); x { ... }` init-statement form puts the
7456        // short-var declaration at child(1) and the condition at
7457        // child(2). Pre-fix, the dispatcher used child(1) and
7458        // counted zero conditions for this idiomatic Go shape.
7459        // The fix uses `child_by_field_name("condition")` which
7460        // returns the condition regardless of init presence.
7461        check_metrics::<GoParser>(
7462            "package p\nfunc F() { if x := g(); x { } }\n",
7463            "foo.go",
7464            |metric| {
7465                // `x` bare-identifier condition contributes 1
7466                // (Rule 6 — bare boolean identifier in if-condition).
7467                // `g()` call contributes 1 branch but no condition.
7468                assert_eq!(metric.abc.branches_sum(), 1);
7469                assert_eq!(metric.abc.conditions_sum(), 1);
7470                insta::assert_json_snapshot!(metric.abc);
7471            },
7472        );
7473    }
7474
7475    #[test]
7476    fn go_if_boolean_literal_condition() {
7477        check_metrics::<GoParser>(
7478            "package p\n\
7479             func F() {\n\
7480             \x20   if true {}                  // +1c\n\
7481             \x20   if !false {}                // +1c\n\
7482             }\n",
7483            "foo.go",
7484            |metric| {
7485                assert_eq!(metric.abc.conditions_sum(), 2);
7486                insta::assert_json_snapshot!(metric.abc);
7487            },
7488        );
7489    }
7490
7491    #[test]
7492    fn go_methods_arguments_with_conditions() {
7493        check_metrics::<GoParser>(
7494            "package p\n\
7495             func F(a, b bool) {\n\
7496             \x20   m(a, b)                     // +1b\n\
7497             \x20   m(!a, !b)                   // +1b +2c\n\
7498             }\n",
7499            "foo.go",
7500            |metric| {
7501                assert_eq!(metric.abc.branches_sum(), 2);
7502                assert_eq!(metric.abc.conditions_sum(), 2);
7503                insta::assert_json_snapshot!(metric.abc);
7504            },
7505        );
7506    }
7507
7508    #[test]
7509    fn go_return_with_conditions() {
7510        check_metrics::<GoParser>(
7511            "package p\n\
7512             func M1(z int) bool { return !(z >= 0) }\n\
7513             func M2(x bool) bool { return !x }\n\
7514             func M3(x, y bool) bool { return x && y }\n",
7515            "foo.go",
7516            |metric| {
7517                // M1: `>=` (1). `!(z >= 0)` walker on the unary
7518                //     doesn't reach a terminal — stops at the
7519                //     BinaryExpression z>=0 inside the parens. +1.
7520                // M2: walker on `!x` → 1.
7521                // M3: `&&` walker counts both → 2.
7522                // Sum: 1 + 1 + 2 = 4.
7523                assert_eq!(metric.abc.conditions_sum(), 4);
7524                insta::assert_json_snapshot!(metric.abc);
7525            },
7526        );
7527    }
7528
7529    #[test]
7530    fn go_short_circuit_with_boolean_literal_operand() {
7531        // `a && true` reports 2 conditions: one identifier, one
7532        // boolean literal. Confirms the terminal set includes
7533        // `True` / `False`.
7534        check_metrics::<GoParser>(
7535            "package p\nfunc F(a bool) bool { return a && true }\n",
7536            "foo.go",
7537            |metric| {
7538                assert_eq!(metric.abc.conditions_sum(), 2);
7539                insta::assert_json_snapshot!(metric.abc);
7540            },
7541        );
7542    }
7543
7544    // ----- Elixir -----
7545
7546    // No top-level Calls and no operators → all three vectors are
7547    // zero. Uses a bare expression rather than a `defmodule` wrapper
7548    // (which would itself be a Call → 1 branch). Confirms the
7549    // ElixirCode Abc trait is wired up and the metric emits.
7550    #[test]
7551    fn elixir_empty_unit_zero() {
7552        check_metrics::<ElixirParser>(":ok\n", "foo.ex", |metric| {
7553            assert_eq!(metric.abc.assignments_sum(), 0);
7554            assert_eq!(metric.abc.branches_sum(), 0);
7555            assert_eq!(metric.abc.conditions_sum(), 0);
7556            insta::assert_json_snapshot!(metric.abc);
7557        });
7558    }
7559
7560    // An empty `defmodule Foo do ... end` is itself ONE `Call` →
7561    // Documents that module-/function-defining macros (`defmodule`,
7562    // `def`, `defp`, `defmacro`, `defmacrop`) and declarative
7563    // directives (`alias`, `import`, `require`, `use`) are NOT
7564    // runtime dispatch and therefore do NOT inflate `branches`,
7565    // matching Cognitive's treatment.
7566    #[test]
7567    fn elixir_defmodule_is_zero_branches() {
7568        check_metrics::<ElixirParser>("defmodule Foo do\nend\n", "foo.ex", |metric| {
7569            assert_eq!(metric.abc.branches_sum(), 0);
7570            assert_eq!(metric.abc.assignments_sum(), 0);
7571            assert_eq!(metric.abc.conditions_sum(), 0);
7572            insta::assert_json_snapshot!(metric.abc);
7573        });
7574    }
7575
7576    // Pattern-match `=` counts as an assignment. Two bindings → A = 2.
7577    // `defmodule` and `def` are declarative-Call wrappers and are
7578    // filtered out of branches; the assertion focuses on assignments
7579    // so we only pin that vector.
7580    #[test]
7581    fn elixir_pattern_match_is_assignment() {
7582        check_metrics::<ElixirParser>(
7583            "defmodule Foo do\n  def f do\n    x = 1\n    y = x + 1\n    y\n  end\nend\n",
7584            "foo.ex",
7585            |metric| {
7586                assert_eq!(metric.abc.assignments_sum(), 2);
7587                insta::assert_json_snapshot!(metric.abc);
7588            },
7589        );
7590    }
7591
7592    // `|>` pipeline operator: each `|>` token contributes one branch.
7593    // Two `|>` ops → +2 from the pipe operator itself. Each pipeline
7594    // step also dispatches a Call (`String.upcase(...)`,
7595    // `String.trim(...)`) — these are wrapped inside the outer
7596    // pipeline Call tree, contributing additional Call branches.
7597    // The headline assertion confirms (a) `|>` is detected and (b)
7598    // pipeline steps are not silently dropped.
7599    #[test]
7600    fn elixir_pipeline_each_step_is_branch() {
7601        check_metrics::<ElixirParser>(
7602            "defmodule Foo do\n  def normalize(s) do\n    s |> String.trim() |> String.upcase()\n  end\nend\n",
7603            "foo.ex",
7604            |metric| {
7605                // Pipeline yields 2 `|>` branches plus Calls for
7606                // String.trim, String.upcase, and the outer pipeline
7607                // (which surfaces as a Call wrapping the binary
7608                // operator). `def` and `defmodule` are declarative
7609                // and excluded. Empirical total: B = 5.
7610                assert_eq!(metric.abc.branches_sum(), 5);
7611                assert_eq!(metric.abc.assignments_sum(), 0);
7612                insta::assert_json_snapshot!(metric.abc);
7613            },
7614        );
7615    }
7616
7617    // Comparison operators all count as conditions. Six comparisons
7618    // (`==`, `!=`, `<`, `>`, `<=`, `>=`) → C = 6.
7619    #[test]
7620    fn elixir_comparisons_are_conditions() {
7621        check_metrics::<ElixirParser>(
7622            "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",
7623            "foo.ex",
7624            |metric| {
7625                assert_eq!(metric.abc.conditions_sum(), 6);
7626                insta::assert_json_snapshot!(metric.abc);
7627            },
7628        );
7629    }
7630
7631    // Strict-equality operators `===` / `!==` count as conditions too.
7632    #[test]
7633    fn elixir_strict_equality_is_condition() {
7634        check_metrics::<ElixirParser>(
7635            "defmodule Foo do\n  def f(a, b) do\n    a === b or a !== b\n  end\nend\n",
7636            "foo.ex",
7637            |metric| {
7638                assert_eq!(metric.abc.conditions_sum(), 2);
7639                insta::assert_json_snapshot!(metric.abc);
7640            },
7641        );
7642    }
7643
7644    // Guard `when` clause counts as a condition. One `when` → +1.
7645    // `def f(x) when x > 0` also has `>` → +1, totalling 2.
7646    #[test]
7647    fn elixir_guard_when_is_condition() {
7648        check_metrics::<ElixirParser>(
7649            "defmodule Foo do\n  def f(x) when x > 0 do\n    :pos\n  end\nend\n",
7650            "foo.ex",
7651            |metric| {
7652                // when (+1) + > (+1) = 2
7653                assert_eq!(metric.abc.conditions_sum(), 2);
7654                insta::assert_json_snapshot!(metric.abc);
7655            },
7656        );
7657    }
7658
7659    // Keyword-shaped Calls (`case`, `cond`, `if`, `with`) each count
7660    // as one condition AND one branch. `case` here adds 1 condition
7661    // (the keyword Call) + 1 branch (the Call itself).
7662    #[test]
7663    fn elixir_case_is_condition_and_branch() {
7664        check_metrics::<ElixirParser>(
7665            "defmodule Foo do\n  def f(x) do\n    case x do\n      1 -> :one\n      _ -> :other\n    end\n  end\nend\n",
7666            "foo.ex",
7667            |metric| {
7668                // conditions: case → 1
7669                assert_eq!(metric.abc.conditions_sum(), 1);
7670                insta::assert_json_snapshot!(metric.abc);
7671            },
7672        );
7673    }
7674
7675    // `cond` is structurally identical to `case` for Abc.
7676    #[test]
7677    fn elixir_cond_is_condition() {
7678        check_metrics::<ElixirParser>(
7679            "defmodule Foo do\n  def f(x) do\n    cond do\n      x > 0 -> :pos\n      true -> :other\n    end\n  end\nend\n",
7680            "foo.ex",
7681            |metric| {
7682                // conditions: cond (+1) + > (+1) = 2
7683                assert_eq!(metric.abc.conditions_sum(), 2);
7684                insta::assert_json_snapshot!(metric.abc);
7685            },
7686        );
7687    }
7688
7689    // `for` is a comprehension/loop, NOT in the issue's condition
7690    // list. It is still a Call so it contributes one branch, but no
7691    // condition.
7692    #[test]
7693    fn elixir_for_is_branch_not_condition() {
7694        check_metrics::<ElixirParser>(
7695            "defmodule Foo do\n  def f(xs) do\n    for x <- xs, do: x * 2\n  end\nend\n",
7696            "foo.ex",
7697            |metric| {
7698                assert_eq!(metric.abc.conditions_sum(), 0);
7699                insta::assert_json_snapshot!(metric.abc);
7700            },
7701        );
7702    }
7703
7704    // Mixed shape, verified by hand: defmodule Call + def Call + if Call
7705    // + Call to side_effect/0 + assignment `x = 1` + comparison `x > 0`.
7706    // - Assignments: `x = 1` → A = 1.
7707    // - Branches: `defmodule` and `def` are declarative and excluded;
7708    //   `if` Call + `side_effect()` Call → 2 Calls, plus 0 `|>` → B = 2.
7709    // - Conditions: `if` keyword → 1, `x > 0` → 1 → C = 2.
7710    #[test]
7711    fn elixir_mixed_abc() {
7712        check_metrics::<ElixirParser>(
7713            "defmodule Foo do\n  def f do\n    x = 1\n    if x > 0 do\n      side_effect()\n    end\n  end\nend\n",
7714            "foo.ex",
7715            |metric| {
7716                assert_eq!(metric.abc.assignments_sum(), 1);
7717                assert_eq!(metric.abc.branches_sum(), 2);
7718                assert_eq!(metric.abc.conditions_sum(), 2);
7719                insta::assert_json_snapshot!(metric.abc);
7720            },
7721        );
7722    }
7723
7724    #[test]
7725    fn elixir_unary_conditions_in_chain() {
7726        // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
7727        // `&&` / `||` chain is one condition. For `if a && b || c`: the
7728        // `if` keyword Call contributes 1 condition, and the walker adds
7729        // a, b, c → 3. expected: 4 conditions, consistent with the
7730        // function's cyclomatic complexity of 4 (base 1 + if + && + ||).
7731        check_metrics::<ElixirParser>(
7732            "defmodule Foo do\n  def f(a, b, c) do\n    if a && b || c do\n      IO.puts(\"x\")\n    end\n  end\nend\n",
7733            "foo.ex",
7734            |metric| {
7735                assert_eq!(metric.abc.conditions_sum(), 4);
7736            },
7737        );
7738    }
7739
7740    #[test]
7741    fn elixir_comparison_operands_add_nothing() {
7742        // Isolation check: comparison operands of a `&&` chain are nested
7743        // `binary_operator` nodes, not bare boolean leaves, so the walker
7744        // adds nothing. expected: 3 = `if` (1) + `>` (1) + `>` (1); the
7745        // `&&` walker contributes 0.
7746        check_metrics::<ElixirParser>(
7747            "defmodule Foo do\n  def f(x, y) do\n    if x > 0 && y > 0 do\n      IO.puts(\"x\")\n    end\n  end\nend\n",
7748            "foo.ex",
7749            |metric| {
7750                assert_eq!(metric.abc.conditions_sum(), 3);
7751            },
7752        );
7753    }
7754
7755    #[test]
7756    fn elixir_keyword_and_or_chain_counts_operands() {
7757        // The keyword forms `and` / `or` get the same Rule 9 treatment as
7758        // `&&` / `||`. expected: 4 = `if` (1) + operands a, b, c (3).
7759        check_metrics::<ElixirParser>(
7760            "defmodule Foo do\n  def f(a, b, c) do\n    if a and b or c do\n      IO.puts(\"x\")\n    end\n  end\nend\n",
7761            "foo.ex",
7762            |metric| {
7763                assert_eq!(metric.abc.conditions_sum(), 4);
7764            },
7765        );
7766    }
7767
7768    // Sigil delimiter choice must not move ABC: `~s<hi>` and `~s(hi)`
7769    // are the same value spelled differently, but the `<` / `>`
7770    // delimiter tokens carry the comparison kind ids, so the unguarded
7771    // condition arm scored `~s<hi>` as 2 conditions and `~s(hi)` as 0.
7772    // The parent-is-`Sigil` guard (mirroring the Halstead getter's,
7773    // #1256) suppresses the delimiter case. expected, for each
7774    // spelling: A = 1 (the `x =` pattern match), B = 0 (a bare sigil
7775    // is not a `Call` node — verified by AST dump: `binary_operator`
7776    // wrapping `identifier`, `=`, `sigil`), C = 0 (no comparison, no
7777    // guard, no keyword Call).
7778    #[test]
7779    fn elixir_sigil_delimiter_choice_is_abc_invariant() {
7780        for src in ["x = ~s<hi>\n", "x = ~s(hi)\n"] {
7781            check_metrics::<ElixirParser>(src, "foo.ex", |metric| {
7782                assert_eq!(metric.abc.assignments_sum(), 1);
7783                assert_eq!(metric.abc.branches_sum(), 0);
7784                assert_eq!(metric.abc.conditions_sum(), 0);
7785            });
7786        }
7787    }
7788
7789    // Control for the guard above: `<` *outside* a sigil is a genuine
7790    // comparison and must keep counting even with a `<`-delimited
7791    // sigil in the same unit. expected: A = 2 (`x =`, `y =`), C = 1
7792    // (only `a < b`; the sigil's `<` / `>` delimiters are guarded).
7793    #[test]
7794    fn elixir_lt_comparison_still_counts_beside_sigil() {
7795        check_metrics::<ElixirParser>("x = ~s<hi>\ny = a < b\n", "foo.ex", |metric| {
7796            assert_eq!(metric.abc.assignments_sum(), 2);
7797            assert_eq!(metric.abc.conditions_sum(), 1);
7798        });
7799    }
7800
7801    // ----- C++ -----
7802
7803    #[test]
7804    fn cpp_empty_unit_zero() {
7805        // No code → A=B=C=0. Wires up the trait and exercises the
7806        // per-language compute reachability.
7807        check_metrics::<CppParser>("", "empty.cpp", |metric| {
7808            assert_eq!(metric.abc.assignments_sum(), 0);
7809            assert_eq!(metric.abc.branches_sum(), 0);
7810            assert_eq!(metric.abc.conditions_sum(), 0);
7811            insta::assert_json_snapshot!(metric.abc);
7812        });
7813    }
7814
7815    #[test]
7816    fn cpp_plain_and_compound_assignments_count() {
7817        // `int x = 0` is an `init_declarator` carrying an `=` token
7818        // and counts as 1 (post-#393: the literal Fitzpatrick rule
7819        // counts every `=` operator, matching the JS impl's
7820        // `let x = 5` treatment). `x = 5`, `x += 2`, `x = 7` all
7821        // parse as `assignment_expression` → 3. Total A = 4.
7822        check_metrics::<CppParser>(
7823            "void f() { int x = 0; x = 5; x += 2; x = 7; }",
7824            "foo.cpp",
7825            |metric| {
7826                assert_eq!(metric.abc.assignments_sum(), 4);
7827                assert_eq!(metric.abc.branches_sum(), 0);
7828                assert_eq!(metric.abc.conditions_sum(), 0);
7829                insta::assert_json_snapshot!(metric.abc);
7830            },
7831        );
7832    }
7833
7834    #[test]
7835    fn cpp_increment_and_decrement_count_as_assignment() {
7836        // `x++` / `--x` / prefix and postfix forms each parse as
7837        // `update_expression` and count as 1 assignment per
7838        // Fitzpatrick — 4. `int x = 0` (init_declarator with `=`)
7839        // adds 1 (post-#393). Total A = 5.
7840        check_metrics::<CppParser>(
7841            "void f() { int x = 0; x++; --x; ++x; x--; }",
7842            "foo.cpp",
7843            |metric| {
7844                assert_eq!(metric.abc.assignments_sum(), 5);
7845                insta::assert_json_snapshot!(metric.abc);
7846            },
7847        );
7848    }
7849
7850    #[test]
7851    fn cpp_init_declarators_count_as_assignments() {
7852        // Issue #393 regression: `int a=1;`, `int b=2;`, `int c=a+b;`,
7853        // `int d=0;` are all `init_declarator` nodes with `=` → 4
7854        // assignments. `d=5;` is one plain `assignment_expression`,
7855        // `d+=1;` is one compound. Total A = 4 + 1 + 1 = 6.
7856        check_metrics::<CppParser>(
7857            "void f() { int a=1; int b=2; int c=a+b; int d=0; d=5; d+=1; }",
7858            "foo.cpp",
7859            |metric| {
7860                assert_eq!(metric.abc.assignments_sum(), 6);
7861                insta::assert_json_snapshot!(metric.abc);
7862            },
7863        );
7864    }
7865
7866    #[test]
7867    fn cpp_declaration_without_initializer_does_not_count() {
7868        // `int a;` parses as a plain declarator inside `declaration`,
7869        // NOT an `init_declarator` (the latter only appears when an
7870        // initializer is present). Regression test for issue #393:
7871        // un-initialised declarations contribute zero to A.
7872        check_metrics::<CppParser>("void f() { int a; a = 5; }", "foo.cpp", |metric| {
7873            // Only `a = 5` (assignment_expression) → A = 1.
7874            assert_eq!(metric.abc.assignments_sum(), 1);
7875            insta::assert_json_snapshot!(metric.abc);
7876        });
7877    }
7878
7879    #[test]
7880    fn cpp_init_declarator_brace_paren_init_does_not_count() {
7881        // `init_declarator` has two grammar forms: `declarator = value`
7882        // (the `=` form) and `declarator argument_list_or_initializer_list`
7883        // (the `int x(5);` / `int x{5};` direct-init forms). Only the
7884        // first form contains an `=` token, so only it should count.
7885        // Regression test pinning that distinction so that
7886        // refactorings of the init_declarator arm don't accidentally
7887        // start counting direct-init too.
7888        check_metrics::<CppParser>(
7889            "void f() { int x(5); int y{7}; x = 1; }",
7890            "foo.cpp",
7891            |metric| {
7892                // Only `x = 1` (assignment_expression) → A = 1.
7893                assert_eq!(metric.abc.assignments_sum(), 1);
7894                insta::assert_json_snapshot!(metric.abc);
7895            },
7896        );
7897    }
7898
7899    #[test]
7900    fn cpp_calls_are_branches() {
7901        // Free call + member-fn call (parses as `call_expression` with
7902        // a `field_expression` callee) + `new` allocation. All three
7903        // are branches → B = 3. `auto* p = new int(5)` is also an
7904        // `init_declarator` with `=` so it contributes one assignment
7905        // (post-#393); the snapshot pins that magnitude.
7906        check_metrics::<CppParser>(
7907            "struct S { void m(); }; void g(); void f() { g(); S s; s.m(); auto* p = new int(5); }",
7908            "foo.cpp",
7909            |metric| {
7910                assert_eq!(metric.abc.branches_sum(), 3);
7911                assert_eq!(metric.abc.assignments_sum(), 1);
7912                insta::assert_json_snapshot!(metric.abc);
7913            },
7914        );
7915    }
7916
7917    #[test]
7918    fn cpp_comparisons_count_conditions() {
7919        // `<`, `>`, `<=`, `>=`, `==`, `!=`, and the C++20 spaceship
7920        // `<=>` each contribute one condition. The `||` short-
7921        // circuits add 0 (Fitzpatrick Rule 5, issue #395). Six
7922        // comparisons in the `||` chain plus `<=>` (1) plus the
7923        // outer `== 0` (1) → C = 8.
7924        check_metrics::<CppParser>(
7925            "#include <compare>\n\
7926             bool f(int a, int b) {\n\
7927                 return a < b || a > b || a <= b || a >= b || a == b || a != b || (a <=> b) == 0;\n\
7928             }\n",
7929            "foo.cpp",
7930            |metric| {
7931                // `<`, `>`, `<=`, `>=`, `==`, `!=` → 6 comparisons
7932                // from the chained `||` expression. `(a <=> b) == 0`
7933                // adds the spaceship `<=>` (1) + the outer `== 0`
7934                // (1) → 8 total. The six `||` short-circuits add 0
7935                // (Fitzpatrick Rule 5; issue #395).
7936                assert_eq!(metric.abc.conditions_sum(), 8);
7937                insta::assert_json_snapshot!(metric.abc);
7938            },
7939        );
7940    }
7941
7942    #[test]
7943    fn cpp_short_circuit_ops_not_counted_directly() {
7944        // `&&` and `||` do NOT count on their own (see the
7945        // module-level `Stats` doc-comment; #395). Phase-2 walker
7946        // counts each operand of a logical chain once (#403), but
7947        // when every operand is itself a relational expression
7948        // (`a == b`, `a > 0`, `b < 0`) the walker doesn't add
7949        // anything on top of the existing comparison-token tally
7950        // — relational sub-expressions are not in
7951        // `cpp_bool_terminal_kinds!()` and `cpp_inspect_container`
7952        // does not recurse into them.
7953        check_metrics::<CppParser>(
7954            "bool f(int a, int b) { return a == b && a > 0 || b < 0; }",
7955            "foo.cpp",
7956            |metric| {
7957                // == 1, > 1, < 1; the walker on && and || finds
7958                // BinaryExpression operands (not terminal-bool) and
7959                // adds nothing. Total: 3.
7960                assert_eq!(metric.abc.conditions_sum(), 3);
7961                insta::assert_json_snapshot!(metric.abc);
7962            },
7963        );
7964    }
7965
7966    #[test]
7967    fn cpp_generic_brackets_not_conditions() {
7968        // `<` / `>` in `std::vector<int>` are `template_argument_list`
7969        // delimiters, NOT comparison operators. The `binary_expression`
7970        // parent check must filter them out → C = 0.
7971        check_metrics::<CppParser>(
7972            "#include <vector>\nstd::vector<int> f() { return std::vector<int>{}; }",
7973            "foo.cpp",
7974            |metric| {
7975                assert_eq!(metric.abc.conditions_sum(), 0);
7976                insta::assert_json_snapshot!(metric.abc);
7977            },
7978        );
7979    }
7980
7981    #[test]
7982    fn cpp_else_and_ternary_count_conditions() {
7983        // `if (cond) ... else ...` + ternary `cond ? a : b`. The
7984        // `if`-keyword is NOT a condition (its condition is the
7985        // comparison inside, which counts separately). `else` adds 1,
7986        // `?` adds 1. Two comparisons (`a > b`, `b < 0`) → 2. Total = 4.
7987        check_metrics::<CppParser>(
7988            "int f(int a, int b) {\n\
7989                 if (a > b) { return a; } else { return b; }\n\
7990                 return (b < 0) ? -b : b;\n\
7991             }\n",
7992            "foo.cpp",
7993            |metric| {
7994                assert_eq!(metric.abc.conditions_sum(), 4);
7995                insta::assert_json_snapshot!(metric.abc);
7996            },
7997        );
7998    }
7999
8000    // Issue #1102. A ternary's condition and both branch operands are
8001    // Fitzpatrick Rule 9 unary conditions, exactly as `java_walk_ternary`
8002    // has always counted them. Before the fix the C family scored
8003    // `a ? !b : !c` as 1 — the `?` token alone — against Java's 4.
8004    #[test]
8005    fn cpp_ternary_operand_slots_count_as_unary_conditions() {
8006        // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4.
8007        check_metrics::<CppParser>("void f() { x = a ? !b : !c; }", "foo.cpp", |metric| {
8008            assert_eq!(metric.abc.conditions_sum(), 4);
8009        });
8010        // No-double-count pin: `?` (1) + `>` (1) = 2, unchanged by the
8011        // fix. The parenthesised condition unwraps to a
8012        // `binary_expression`, which is not a boolean terminal, and
8013        // neither branch is negated — the `!` is the type-free proxy for
8014        // "this operand is boolean", so an unnegated branch contributes
8015        // nothing.
8016        check_metrics::<CppParser>("void f() { x = (a > 0) ? b : -b; }", "foo.cpp", |metric| {
8017            assert_eq!(metric.abc.conditions_sum(), 2);
8018        });
8019        // Nested: outer `?` (1) + outer condition `a` (1) + inner `?`
8020        // (1) + inner condition `b` (1) = 4. The outer consequence is
8021        // the inner ternary — neither a boolean terminal nor a
8022        // paren / `!` wrapper — so it adds nothing on its own and the
8023        // inner one is reached by the walk, not by descent.
8024        check_metrics::<CppParser>("void f() { x = a ? b ? c : d : e; }", "foo.cpp", |metric| {
8025            assert_eq!(metric.abc.conditions_sum(), 4);
8026        });
8027        // A negated *condition* is the only input that reaches the
8028        // walker's `else` fallback: `!a` is neither a boolean terminal
8029        // (so the terminal arm skips it) nor an operand slot (so
8030        // `cpp_inspect_container` is never called on it from anywhere
8031        // else). Every other condition fixture in this file wraps a
8032        // comparison, which the fallback resolves to 0 — delete the
8033        // fallback and only this case moves. `?` (1) + `!a` (1) = 2.
8034        check_metrics::<CppParser>("void f() { x = !a ? b : c; }", "foo.cpp", |metric| {
8035            assert_eq!(metric.abc.conditions_sum(), 2);
8036        });
8037    }
8038
8039    // The GNU short-ternary `a ?: b` elides the consequence, so the
8040    // C-family grammar marks that field optional and the alternative
8041    // lands at child(3) rather than child(4). Addressing the operand
8042    // slots by grammar field name — never by index — is what keeps `!b`
8043    // counted here; a fixed `child(4)` reads `None` and scores 2.
8044    #[test]
8045    fn cpp_elided_ternary_consequence_still_walks_the_alternative() {
8046        // `?` (1) + condition `a` (1) + `!b` (1) = 3.
8047        check_metrics::<CppParser>("void f() { x = a ?: !b; }", "foo.cpp", |metric| {
8048            assert_eq!(metric.abc.conditions_sum(), 3);
8049        });
8050    }
8051
8052    // `cpp_walk_ternary` is shared by the C, ObjC, and Mozcpp ABC impls
8053    // exactly as `cpp_inspect_container` is, so each needs its own
8054    // dispatcher arm. Mozcpp owns no file extension and so gets no
8055    // integration-snapshot coverage at all — this parity assertion is
8056    // its only guard.
8057    //
8058    // The expected value is *derived from the C++ run*, not hardcoded,
8059    // so the four languages cannot silently drift apart if the C++
8060    // expectation ever legitimately moves.
8061    #[test]
8062    fn c_family_ternary_operand_slots_agree_with_cpp() {
8063        const SRC: &str = "void f() { x = a ? !b : !c; }\n";
8064        let conditions = abc_conditions;
8065
8066        let cpp = conditions(LANG::Cpp, SRC);
8067        // Non-degenerate: a zeroed reference would make every
8068        // comparison below vacuous.
8069        assert_eq!(cpp, 4, "C++ reference value for `a ? !b : !c`");
8070
8071        assert_eq!(conditions(LANG::C, SRC), cpp, "C must match C++");
8072        assert_eq!(conditions(LANG::Mozcpp, SRC), cpp, "Mozcpp must match C++");
8073        assert_eq!(
8074            conditions(
8075                LANG::Objc,
8076                "@implementation Foo\n\
8077                 - (void)bar {\n\
8078                     x = a ? !b : !c;\n\
8079                 }\n\
8080                 @end\n",
8081            ),
8082            cpp,
8083            "ObjC must match C++"
8084        );
8085    }
8086
8087    // Issue #1276, C-family half. `cpp_walk_for_statement` is the
8088    // `for` header's counterpart to the `if` / `while` arms: the slot
8089    // is a bare expression rather than a `condition_clause`, so it
8090    // needs the top-level terminal check `cpp_walk_ternary` already
8091    // had. Every fixture is a shape only that walker can classify —
8092    // a comparison-shaped condition proves nothing, the `<` token arm
8093    // counts it either way (grammar-dispatch §11).
8094    #[test]
8095    fn cpp_for_condition_slot_counts_unary_conditions() {
8096        // Bare identifier: the whole condition, no operator token.
8097        check_metrics::<CppParser>("void f(int a) { for (; a; ) {} }", "foo.cpp", |metric| {
8098            assert_eq!(metric.abc.conditions_sum(), 1);
8099        });
8100        // Negation: reaches the terminal through
8101        // `cpp_inspect_container`'s `!` unwrap.
8102        check_metrics::<CppParser>("void f(int a) { for (; !a; ) {} }", "foo.cpp", |metric| {
8103            assert_eq!(metric.abc.conditions_sum(), 1);
8104        });
8105        // Parentheses: counts only because the `for_statement` parent
8106        // seeds `has_boolean_content` — the seed #1276 found dead.
8107        check_metrics::<CppParser>("void f(int a) { for (; (a); ) {} }", "foo.cpp", |metric| {
8108            assert_eq!(metric.abc.conditions_sum(), 1);
8109        });
8110        // No-double-count pin: the `<` token arm already counted this
8111        // shape before the fix and the walker must not add a second.
8112        // The two assignments confirm the header parsed as the
8113        // three-clause form rather than degenerating.
8114        check_metrics::<CppParser>(
8115            "void f(int n) { for (int i = 0; i < n; i++) {} }",
8116            "foo.cpp",
8117            |metric| {
8118                assert_eq!(metric.abc.conditions_sum(), 1);
8119                assert_eq!(metric.abc.assignments_sum(), 2);
8120            },
8121        );
8122        // Empty condition: no `condition` field, no decision, zero.
8123        check_metrics::<CppParser>("void f() { for (;;) { break; } }", "foo.cpp", |metric| {
8124            assert_eq!(metric.abc.conditions_sum(), 0);
8125        });
8126    }
8127
8128    // `cpp_walk_for_statement` is shared by the C, ObjC and Mozcpp ABC
8129    // impls the way `cpp_walk_ternary` is, so each needs its own
8130    // dispatcher arm — and Mozcpp, which owns no file extension, has no
8131    // integration-snapshot coverage at all, making this its only guard.
8132    // The expected value is derived from the C++ run rather than
8133    // hardcoded, so the four cannot silently drift apart.
8134    #[test]
8135    fn c_family_for_condition_slot_agrees_with_cpp() {
8136        const SRC: &str = "void f(int a) { for (; !a; ) {} }\n";
8137        let conditions = abc_conditions;
8138
8139        let cpp = conditions(LANG::Cpp, SRC);
8140        // Non-degenerate: a zeroed reference makes every comparison
8141        // below vacuous.
8142        assert_eq!(cpp, 1, "C++ reference value for `for (; !a; )`");
8143
8144        assert_eq!(conditions(LANG::C, SRC), cpp, "C must match C++");
8145        assert_eq!(conditions(LANG::Mozcpp, SRC), cpp, "Mozcpp must match C++");
8146        assert_eq!(
8147            conditions(
8148                LANG::Objc,
8149                "@implementation Foo\n\
8150                 - (void)bar {\n\
8151                     for (; !a; ) {}\n\
8152                 }\n\
8153                 @end\n",
8154            ),
8155            cpp,
8156            "ObjC must match C++"
8157        );
8158    }
8159
8160    #[test]
8161    fn cpp_switch_cases_count_default_excluded() {
8162        // `case 1`, `case 2` → 2 conditions. `default` is intentionally
8163        // excluded (the unconditional fallthrough, mirroring cyclomatic's
8164        // `Case`-only count). Since #469 every C-family language —
8165        // Java, C#, Groovy, JS, TS — agrees on this; C++ already did.
8166        // C = 2.
8167        check_metrics::<CppParser>(
8168            "void f(int x) {\n\
8169                 switch (x) {\n\
8170                     case 1: break;\n\
8171                     case 2: break;\n\
8172                     default: break;\n\
8173                 }\n\
8174             }\n",
8175            "foo.cpp",
8176            |metric| {
8177                assert_eq!(metric.abc.conditions_sum(), 2);
8178                insta::assert_json_snapshot!(metric.abc);
8179            },
8180        );
8181    }
8182
8183    #[test]
8184    fn cpp_try_catch_count_conditions() {
8185        // `try` and `catch` each add one condition (Fitzpatrick's rule;
8186        // Java's impl above counts them too).
8187        check_metrics::<CppParser>(
8188            "void f() { try { } catch (int) { } catch (...) { } }",
8189            "foo.cpp",
8190            |metric| {
8191                // 1 `try` + 2 `catch` arms = 3.
8192                assert_eq!(metric.abc.conditions_sum(), 3);
8193                insta::assert_json_snapshot!(metric.abc);
8194            },
8195        );
8196    }
8197
8198    #[test]
8199    fn cpp_complex_function_abc() {
8200        // Mixed-shape regression: assignments, calls, conditions,
8201        // ternary, switch, new. Verified by hand:
8202        // - assignments: `int x = 0` (init_declarator with `=`),
8203        //   `x = 5`, `x += 2`, `x++`, `x = (a > b) ? a : b`, `x = b`,
8204        //   `auto* p = new int(5)` (init_declarator with `=`) → A = 7
8205        //   (post-#393: every `=` in an init_declarator counts).
8206        // - branches: `f(a, b)` self-call + `new int(5)` → B = 2.
8207        // - conditions: `a == b` (1) + `a > 0` (1) inside the if;
8208        //   `&&` itself is NOT a condition (Fitzpatrick Rule 5,
8209        //   issue #395). `a > b` (1) + `?` (1) in the ternary.
8210        //   `else` (1, from the `else if` keyword) + `a < b` (1)
8211        //   in the else-if. `!x` contributes 1 via the unary-
8212        //   conditional walker (Fitzpatrick Rule 9, issue #403):
8213        //   the `||` walker treats `!x` as a unary boolean operand
8214        //   and counts the wrapped Identifier once. `case 1`,
8215        //   `case 2` → 2. `default` excluded. Total C = 9.
8216        check_metrics::<CppParser>(
8217            "int f(int a, int b) {\n\
8218                 int x = 0;\n\
8219                 x = 5;\n\
8220                 x += 2;\n\
8221                 x++;\n\
8222                 if (a == b && a > 0) {\n\
8223                     x = (a > b) ? a : b;\n\
8224                 } else if (a < b || !x) {\n\
8225                     x = b;\n\
8226                 }\n\
8227                 switch (x) {\n\
8228                     case 1: break;\n\
8229                     case 2: break;\n\
8230                     default: break;\n\
8231                 }\n\
8232                 auto* p = new int(5);\n\
8233                 return f(a, b);\n\
8234             }\n",
8235            "foo.cpp",
8236            |metric| {
8237                assert_eq!(metric.abc.assignments_sum(), 7);
8238                assert_eq!(metric.abc.branches_sum(), 2);
8239                assert_eq!(metric.abc.conditions_sum(), 9);
8240                insta::assert_json_snapshot!(metric.abc);
8241            },
8242        );
8243    }
8244
8245    #[test]
8246    fn cpp_if_multiple_conditions() {
8247        // Fitzpatrick Rule 9 walker (issue #403): each operand of a
8248        // `&&` / `||` chain is one condition.
8249        check_metrics::<CppParser>(
8250            "void f(bool a, bool b, bool c, bool d) {\n\
8251             \x20   if (a || b || c || d) {}        // +4c\n\
8252             \x20   if (a && b && c) {}             // +3c\n\
8253             \x20   if (!a && !b) {}                // +2c\n\
8254             }\n",
8255            "foo.cpp",
8256            |metric| {
8257                assert_eq!(metric.abc.conditions_sum(), 9);
8258                insta::assert_json_snapshot!(metric.abc);
8259            },
8260        );
8261    }
8262
8263    #[test]
8264    fn cpp_while_and_do_while_conditions() {
8265        // Exercise both the WhileStatement and DoStatement arms via
8266        // the walker on the `&&` / `||` tokens inside their parens.
8267        check_metrics::<CppParser>(
8268            "void f(bool a, bool b) {\n\
8269             \x20   while (a || b) {}              // +2c\n\
8270             \x20   do {} while (a && !b);         // +2c\n\
8271             }\n",
8272            "foo.cpp",
8273            |metric| {
8274                assert_eq!(metric.abc.conditions_sum(), 4);
8275                insta::assert_json_snapshot!(metric.abc);
8276            },
8277        );
8278    }
8279
8280    #[test]
8281    fn cpp_if_constexpr_condition_counts() {
8282        // Regression for the code-review finding: C++ `if constexpr
8283        // (cond)` puts the `constexpr` keyword at child(1) and the
8284        // condition_clause at child(2). Pre-fix, the dispatcher used
8285        // child(1) and counted zero conditions for the `constexpr`
8286        // form. The fix uses `child_by_field_name("condition")`
8287        // which returns the condition_clause regardless of the
8288        // optional `constexpr` keyword.
8289        check_metrics::<CppParser>(
8290            "template <int N> void f() {\n\
8291             \x20   if constexpr (true) { }      // +1c\n\
8292             \x20   if (false) { }               // +1c\n\
8293             }\n",
8294            "foo.cpp",
8295            |metric| {
8296                assert_eq!(metric.abc.conditions_sum(), 2);
8297                insta::assert_json_snapshot!(metric.abc);
8298            },
8299        );
8300    }
8301
8302    #[test]
8303    fn cpp_cast_expression_in_logical_chain_counts() {
8304        // Regression for findings.md round-2 #1 (C++):
8305        // `if ((bool)ptr && ready) {}` had the `||` walker missing
8306        // the `(bool)ptr` operand because `CastExpression` was not
8307        // in `cpp_bool_terminal_kinds!()`. Mirrors C#'s
8308        // `csharp_bool_terminal_kinds!()` which lists
8309        // `CastExpression` (lesson 19, #372).
8310        check_metrics::<CppParser>(
8311            "void f(void* ptr, bool ready) { if ((bool)ptr && ready) { } }\n",
8312            "foo.cpp",
8313            |metric| {
8314                // `&&` walker counts both operands: `(bool)ptr` (1)
8315                // and `ready` (1). Total: 2.
8316                assert_eq!(metric.abc.conditions_sum(), 2);
8317                insta::assert_json_snapshot!(metric.abc);
8318            },
8319        );
8320    }
8321
8322    #[test]
8323    fn cpp_qualified_identifier_condition_counts() {
8324        // Regression for findings.md #3 (C++): tree-sitter-cpp emits
8325        // `qualified_identifier` under four kind_ids (573..576) per
8326        // the production-rule path; runtime kind for `ns::flag` is
8327        // 574 (`QualifiedIdentifier2`). Pre-fix the
8328        // `cpp_bool_terminal_kinds!()` macro listed neither the
8329        // primary nor any alias, so `if (n::flag) {}` reported zero
8330        // conditions. The macro now includes all four variants
8331        // (lesson #2).
8332        check_metrics::<CppParser>(
8333            "namespace n { extern bool flag; }\n\
8334             void f() { if (n::flag) { } }\n",
8335            "foo.cpp",
8336            |metric| {
8337                assert_eq!(metric.abc.conditions_sum(), 1);
8338                insta::assert_json_snapshot!(metric.abc);
8339            },
8340        );
8341    }
8342
8343    #[test]
8344    fn cpp_if_boolean_literal_condition() {
8345        check_metrics::<CppParser>(
8346            "void f() {\n\
8347             \x20   if (true) {}                 // +1c\n\
8348             \x20   if (!false) {}               // +1c\n\
8349             \x20   while (true) {}              // +1c\n\
8350             \x20   do {} while (false);         // +1c\n\
8351             }\n",
8352            "foo.cpp",
8353            |metric| {
8354                assert_eq!(metric.abc.conditions_sum(), 4);
8355                insta::assert_json_snapshot!(metric.abc);
8356            },
8357        );
8358    }
8359
8360    #[test]
8361    fn cpp_methods_arguments_with_conditions() {
8362        check_metrics::<CppParser>(
8363            "void f(bool a, bool b) {\n\
8364             \x20   m(a, b);                     // +1b\n\
8365             \x20   m(!a, !b);                   // +1b +2c\n\
8366             }\n",
8367            "foo.cpp",
8368            |metric| {
8369                assert_eq!(metric.abc.branches_sum(), 2);
8370                assert_eq!(metric.abc.conditions_sum(), 2);
8371                insta::assert_json_snapshot!(metric.abc);
8372            },
8373        );
8374    }
8375
8376    #[test]
8377    fn cpp_return_with_conditions() {
8378        check_metrics::<CppParser>(
8379            "bool m1(int z) { return !(z >= 0); }\n\
8380             bool m2(bool x) { return (((!x))); }\n\
8381             bool m3(bool x, bool y) { return x && y; }\n",
8382            "foo.cpp",
8383            |metric| {
8384                // m1: !(z >= 0) → `>=` (1). `!` wraps a paren'd
8385                //     BinaryExpression — inspect_container reaches
8386                //     the inner BinaryExpression and stops, no
8387                //     walker count. +1.
8388                // m2: (((!x))) → ReturnStatement → inspect_container
8389                //     unwraps three parens + one unary → reaches `x`
8390                //     in has_boolean_content=true (seeded by the
8391                //     unary `!`). +1.
8392                // m3: x && y → `&&` walker counts both → +2.
8393                // Sum: 1 + 1 + 2 = 4.
8394                assert_eq!(metric.abc.conditions_sum(), 4);
8395                insta::assert_json_snapshot!(metric.abc);
8396            },
8397        );
8398    }
8399
8400    #[test]
8401    fn cpp_short_circuit_with_boolean_literal_operand() {
8402        // `a && true` reports 2 conditions: one for the identifier
8403        // operand, one for the `True` literal operand.
8404        check_metrics::<CppParser>(
8405            "bool f(bool a) { return a && true; }\n",
8406            "foo.cpp",
8407            |metric| {
8408                assert_eq!(metric.abc.conditions_sum(), 2);
8409                insta::assert_json_snapshot!(metric.abc);
8410            },
8411        );
8412    }
8413
8414    #[test]
8415    fn javascript_empty_unit_zero() {
8416        // No code → A=B=C=0. Wires up the trait and exercises the
8417        // per-language compute reachability.
8418        check_metrics::<JavascriptParser>("", "empty.js", |metric| {
8419            assert_eq!(metric.abc.assignments_sum(), 0);
8420            assert_eq!(metric.abc.branches_sum(), 0);
8421            assert_eq!(metric.abc.conditions_sum(), 0);
8422            insta::assert_json_snapshot!(metric.abc);
8423        });
8424    }
8425
8426    #[test]
8427    fn javascript_plain_and_compound_assignments_count() {
8428        // `let` / `var` declarations behave like TypeScript: only a
8429        // `const` initializer is suppressed. So `let x = 0` does count as
8430        // A=+1; only `const PI = 3.14` would be elided. Plain `x = 5`,
8431        // `x += 2`, `x = 7` all count → A = 4 total here.
8432        check_metrics::<JavascriptParser>(
8433            "function f() { let x = 0; x = 5; x += 2; x = 7; }",
8434            "foo.js",
8435            |metric| {
8436                assert_eq!(metric.abc.assignments_sum(), 4);
8437                assert_eq!(metric.abc.branches_sum(), 0);
8438                assert_eq!(metric.abc.conditions_sum(), 0);
8439                insta::assert_json_snapshot!(metric.abc);
8440            },
8441        );
8442    }
8443
8444    #[test]
8445    fn javascript_const_initializer_not_assignment() {
8446        // `const PI = 3.14` must NOT count as an assignment — its `=`
8447        // initialises a `const` binding. `let x = 1` and `var y = 2`
8448        // still count (matches the TS impl: only `const` suppresses).
8449        check_metrics::<JavascriptParser>(
8450            "function f() { const PI = 3.14; let x = 1; var y = 2; x = 9; }",
8451            "foo.js",
8452            |metric| {
8453                // `const PI` suppressed; `let x = 1`, `var y = 2`,
8454                // `x = 9` all count → A = 3.
8455                assert_eq!(metric.abc.assignments_sum(), 3);
8456                insta::assert_json_snapshot!(metric.abc);
8457            },
8458        );
8459    }
8460
8461    #[test]
8462    fn javascript_asi_const_does_not_suppress_later_assignments() {
8463        // The issue #1277 reproducer verbatim. JavaScript half of the
8464        // cluster documented at
8465        // `typescript_asi_const_does_not_suppress_later_assignments`.
8466        check_metrics::<JavascriptParser>(
8467            "function f() {
8468                const a = 1
8469                x = 2
8470                return x
8471            }",
8472            "foo.js",
8473            |metric| {
8474                // Pre-#1277 this reported 0; the semicolon-terminated
8475                // spelling reported 1.
8476                assert_eq!(metric.abc.assignments_sum(), 1);
8477            },
8478        );
8479    }
8480
8481    #[test]
8482    fn javascript_nested_arrow_const_does_not_leak() {
8483        // The ASI leak beside a nested space: the arrow body opens its
8484        // own space, so `y = 1` was always counted there, but
8485        // `const f = () => …` has no `;`, so `h`'s sentinel stayed live
8486        // and suppressed `z = 2`. The stack was per-space state that
8487        // `Stats::merge` never carried, so this is the terminator defect
8488        // and not a second route.
8489        check_metrics::<JavascriptParser>(
8490            "function h() {
8491                const f = () => { y = 1 }
8492                z = 2
8493            }",
8494            "foo.js",
8495            |metric| {
8496                // `y = 1` and `z = 2`; the `const` initializer is
8497                // suppressed. Pre-#1277: 1.
8498                assert_eq!(metric.abc.assignments_sum(), 2);
8499            },
8500        );
8501    }
8502
8503    #[test]
8504    fn javascript_non_declarator_equals_still_count() {
8505        // An `=` that does not belong to a `const` declarator is always
8506        // an assignment: a class `field_definition` initializer, a
8507        // default-parameter `assignment_pattern`, and a destructured
8508        // parameter's defaults, whose climb crosses the same pattern
8509        // layers as a `const` pattern's but ends at `formal_parameters`.
8510        // A predicate that accepted any pattern ancestor as a declarator
8511        // would zero the last three.
8512        check_metrics::<JavascriptParser>(
8513            "class K { f = 1; g(p = 2, {q = 3} = {}) { let d = 4 } }",
8514            "foo.js",
8515            |metric| {
8516                // `f = 1`, `p = 2`, `q = 3`, `= {}`, `let d = 4`.
8517                assert_eq!(metric.abc.assignments_sum(), 5);
8518            },
8519        );
8520    }
8521
8522    #[test]
8523    fn javascript_const_initializer_value_assignments_still_count() {
8524        // An `=` inside a `const` initializer's *value* is an
8525        // `assignment_expression`: its climb reaches no pattern layer and
8526        // no declarator, so it counts. The pre-#1277 sentinel
8527        // blanket-suppressed every `=` between `const` and `;`, which is
8528        // the shape that moved the pdf.js corpus snapshots
8529        // (`const bbox = (this.data.rect = …)`).
8530        check_metrics::<JavascriptParser>(
8531            "function m(o, a, b) { const x = (o.p = 1); const y = a || (b = 2); }",
8532            "foo.js",
8533            |metric| {
8534                // `o.p = 1` and `b = 2`; the two `const` initializers are
8535                // suppressed. Pre-#1277: 0.
8536                assert_eq!(metric.abc.assignments_sum(), 2);
8537            },
8538        );
8539    }
8540
8541    #[test]
8542    fn javascript_const_declarator_shapes_stay_suppressed() {
8543        // JavaScript half of
8544        // `typescript_const_declarator_shapes_stay_suppressed`.
8545        check_metrics::<JavascriptParser>(
8546            "function f(o, xs) {
8547                const a = 1, b = 2
8548                const {c = 5, d: {e = 6} = {}} = o
8549                const [g = 7, ...[h = 8]] = xs
8550                let i = 3
8551                var j = 4
8552                for (const x of xs) { k(x) }
8553            }",
8554            "foo.js",
8555            |metric| {
8556                assert_eq!(metric.abc.assignments_sum(), 2);
8557            },
8558        );
8559    }
8560
8561    #[test]
8562    fn javascript_increment_and_decrement_count_as_assignment() {
8563        // `x++` (post) and `--x` (pre) both update an lvalue and so
8564        // count as assignments. Combined with the `let x = 0`
8565        // initializer (which counts under the JS/TS rule — only `const`
8566        // suppresses), A = 3.
8567        check_metrics::<JavascriptParser>(
8568            "function f() { let x = 0; x++; --x; }",
8569            "foo.js",
8570            |metric| {
8571                assert_eq!(metric.abc.assignments_sum(), 3);
8572                insta::assert_json_snapshot!(metric.abc);
8573            },
8574        );
8575    }
8576
8577    #[test]
8578    fn javascript_calls_are_branches() {
8579        // `g(1)` is a `call_expression` → B = 1. `new Foo(2)` is a
8580        // `new_expression` → B = 1. Total B = 2.
8581        check_metrics::<JavascriptParser>(
8582            "function f() { g(1); new Foo(2); }",
8583            "foo.js",
8584            |metric| {
8585                assert_eq!(metric.abc.branches_sum(), 2);
8586                assert_eq!(metric.abc.conditions_sum(), 0);
8587                insta::assert_json_snapshot!(metric.abc);
8588            },
8589        );
8590    }
8591
8592    #[test]
8593    fn javascript_comparisons_count_conditions() {
8594        // `==`, `===`, `!=`, `!==`, `<`, `>`, `<=`, `>=` each count
8595        // once. The `&&` / `||` short-circuit operators are NOT
8596        // counted as conditions in this impl (matches the TS
8597        // precedent — short-circuit ops are folded into the
8598        // surrounding `if` / control-flow arm, not separately).
8599        // Total C = 8.
8600        check_metrics::<JavascriptParser>(
8601            "function f(a, b) { return a == b && a === b && a != b && a !== b && a < b && a > b && a <= b && a >= b; }",
8602            "foo.js",
8603            |metric| {
8604                assert_eq!(metric.abc.conditions_sum(), 8);
8605                insta::assert_json_snapshot!(metric.abc);
8606            },
8607        );
8608    }
8609
8610    #[test]
8611    fn javascript_number_truthy_condition_counts() {
8612        // Regression for #772: JS treats every non-zero number as
8613        // truthy, so `while (5)` and `x && 5` should each count their
8614        // numeric literal as a Fitzpatrick unary condition. Pre-fix
8615        // `javascript_bool_terminal_kinds!()` listed `True` / `False`
8616        // but omitted `Number`, so the walker dropped every numeric-
8617        // truthy operand (mirrors the Lua `Number` fix).
8618        check_metrics::<JavascriptParser>(
8619            "function f(x) { while (5) {} return x && 5; }",
8620            "foo.js",
8621            |metric| {
8622                // `while (5)` → Number literal (+1). `x && 5` → both
8623                // operands count: identifier `x` (+1), Number `5` (+1).
8624                // Total: 3.
8625                assert_eq!(metric.abc.conditions_sum(), 3);
8626                insta::assert_json_snapshot!(metric.abc);
8627            },
8628        );
8629    }
8630
8631    #[test]
8632    fn typescript_number_truthy_condition_counts() {
8633        // Regression for #772: TS shares the JS truthy semantics. The
8634        // numeric *literal* `5` (kind `Number`) counts; the type-keyword
8635        // `number` (kind `Number2`, the `predefined_type`) must not —
8636        // see `typescript_bool_terminal_kinds!`.
8637        check_metrics::<TypescriptParser>(
8638            "function f(x: number) { while (5) {} return x && 5; }",
8639            "foo.ts",
8640            |metric| {
8641                // `while (5)` → +1; `x && 5` → `x` (+1) + `5` (+1).
8642                // Total: 3. The `: number` annotation contributes 0.
8643                assert_eq!(metric.abc.conditions_sum(), 3);
8644                insta::assert_json_snapshot!(metric.abc);
8645            },
8646        );
8647    }
8648
8649    #[test]
8650    fn javascript_nullish_coalescing_counts_condition() {
8651        // `a ?? b` is one nullish-coalescing operator → C = 1.
8652        check_metrics::<JavascriptParser>(
8653            "function f(a, b) { return a ?? b; }",
8654            "foo.js",
8655            |metric| {
8656                assert_eq!(metric.abc.conditions_sum(), 1);
8657                insta::assert_json_snapshot!(metric.abc);
8658            },
8659        );
8660    }
8661
8662    #[test]
8663    fn javascript_else_ternary_case_default_try_catch() {
8664        // `else`, `?` (ternary), `case`, `try`, `catch` all count.
8665        // `default` is the unconditional fallthrough → +0 (#469).
8666        // With the comparisons:
8667        //   - `a > 0` → 1
8668        //   - `else` opens an else_clause → 1
8669        //   - `?` ternary → 1
8670        //   - the ternary's bare-identifier condition `a` → 1 (#1102)
8671        //   - `case 1` → 1
8672        //   - `default` → 0 (fallthrough, #469)
8673        //   - `try` + `catch` → 2
8674        // Total C = 7.
8675        check_metrics::<JavascriptParser>(
8676            "function f(a) { if (a > 0) {} else {} let x = a ? 1 : 2; switch (x) { case 1: break; default: break; } try { } catch (e) { } }",
8677            "foo.js",
8678            |metric| {
8679                assert_eq!(metric.abc.conditions_sum(), 7);
8680                insta::assert_json_snapshot!(metric.abc);
8681            },
8682        );
8683    }
8684
8685    // Issue #1102, JS-family half. See
8686    // `cpp_ternary_operand_slots_count_as_unary_conditions` for the
8687    // rule; the two families were behind Java by the same three units.
8688    #[test]
8689    fn javascript_ternary_operand_slots_count_as_unary_conditions() {
8690        // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4.
8691        check_metrics::<JavascriptParser>(
8692            "function f() { x = a ? !b : !c; }",
8693            "foo.js",
8694            |metric| assert_eq!(metric.abc.conditions_sum(), 4),
8695        );
8696        // No-double-count pin: `?` (1) + `>` (1) = 2, unchanged by the
8697        // fix — the parenthesised condition unwraps to a
8698        // `binary_expression` (not a boolean terminal) and neither
8699        // branch is negated.
8700        check_metrics::<JavascriptParser>(
8701            "function f() { x = (a > 0) ? b : -b; }",
8702            "foo.js",
8703            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
8704        );
8705        // Nested: two `?` tokens plus the two bare-identifier
8706        // conditions = 4.
8707        check_metrics::<JavascriptParser>(
8708            "function f() { x = a ? b ? c : d : e; }",
8709            "foo.js",
8710            |metric| assert_eq!(metric.abc.conditions_sum(), 4),
8711        );
8712        // A negated condition is the only input reaching the walker's
8713        // `else` fallback — see the C++ sibling for why. `?` (1) +
8714        // `!a` (1) = 2.
8715        check_metrics::<JavascriptParser>("function f() { x = !a ? b : c; }", "foo.js", |metric| {
8716            assert_eq!(metric.abc.conditions_sum(), 2);
8717        });
8718    }
8719
8720    // TypeScript expands the same `ts_abc_compute!` arm from a separate
8721    // macro body than JavaScript's `js_abc_compute!`, so wiring one and
8722    // not the other is a live failure mode; TSX and Mozjs are clones of
8723    // these two.
8724    #[test]
8725    fn typescript_ternary_operand_slots_count_as_unary_conditions() {
8726        check_metrics::<TypescriptParser>(
8727            "function f() { x = a ? !b : !c; }",
8728            "foo.ts",
8729            |metric| assert_eq!(metric.abc.conditions_sum(), 4),
8730        );
8731        check_metrics::<TypescriptParser>(
8732            "function f() { x = (a > 0) ? b : -b; }",
8733            "foo.ts",
8734            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
8735        );
8736    }
8737
8738    // Issue #1276, JS-family half. See
8739    // `cpp_for_condition_slot_counts_unary_conditions` for the rule.
8740    // The JS grammar marks the `condition` field on both the expression
8741    // and the `;` closing it, so `child_by_field_name` is the only
8742    // addressing that lands on the expression for every header shape.
8743    #[test]
8744    fn javascript_for_condition_slot_counts_unary_conditions() {
8745        // Bare identifier: no operator token anywhere in the header.
8746        check_metrics::<JavascriptParser>("function f(a) { for (; a; ) {} }", "foo.js", |metric| {
8747            assert_eq!(metric.abc.conditions_sum(), 1);
8748        });
8749        // Negation, through the `!` unwrap.
8750        check_metrics::<JavascriptParser>(
8751            "function f(a) { for (; !a; ) {} }",
8752            "foo.js",
8753            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
8754        );
8755        // Parentheses: counts only via the `ForStatement`
8756        // boolean-context seed #1276 found dead.
8757        check_metrics::<JavascriptParser>(
8758            "function f(a) { for (; (a); ) {} }",
8759            "foo.js",
8760            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
8761        );
8762        // No-double-count pin: the `<` arm already counted this shape.
8763        // `let i = 0` and `i++` are the two assignments, which also
8764        // confirms the header parsed as the three-clause form.
8765        check_metrics::<JavascriptParser>(
8766            "function f(n) { for (let i = 0; i < n; i++) {} }",
8767            "foo.js",
8768            |metric| {
8769                assert_eq!(metric.abc.conditions_sum(), 1);
8770                assert_eq!(metric.abc.assignments_sum(), 2);
8771            },
8772        );
8773        // Empty condition: the slot holds an `empty_statement`, which
8774        // is neither a terminal nor a wrapper, so it counts nothing.
8775        check_metrics::<JavascriptParser>(
8776            "function f() { for (;;) { break; } }",
8777            "foo.js",
8778            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
8779        );
8780    }
8781
8782    // TypeScript expands the `ForStatement` arm from `ts_abc_compute!`,
8783    // a separate macro body from JavaScript's `js_abc_compute!`, so
8784    // wiring one and not the other is a live failure mode; TSX and
8785    // Mozjs are the clones of those two. The expected values are
8786    // derived from the JavaScript run rather than hardcoded.
8787    #[test]
8788    fn js_family_for_condition_slot_agrees_with_javascript() {
8789        const BARE: &str = "function f(a) { for (; a; ) {} }\n";
8790        const EMPTY: &str = "function f() { for (;;) { break; } }\n";
8791        let conditions = abc_conditions;
8792
8793        let bare = conditions(LANG::Javascript, BARE);
8794        // Non-degenerate: a zeroed reference makes the comparisons
8795        // below vacuous.
8796        assert_eq!(bare, 1, "JavaScript reference value for `for (; a; )`");
8797        assert_eq!(
8798            conditions(LANG::Javascript, EMPTY),
8799            0,
8800            "JavaScript `for (;;)`"
8801        );
8802
8803        for lang in [LANG::Mozjs, LANG::Typescript, LANG::Tsx] {
8804            assert_eq!(conditions(lang, BARE), bare, "{lang:?} bare for-condition");
8805            assert_eq!(conditions(lang, EMPTY), 0, "{lang:?} empty for-condition");
8806        }
8807    }
8808
8809    #[test]
8810    fn js_family_ternary_operand_slots_agree_with_javascript() {
8811        // `a ? !b : !c` is the one shape that tells the ternary walker
8812        // from the `for`-header walker: both read the `condition` field
8813        // and share the `(&Node, &mut f64)` signature, so a transposed
8814        // pair in a `ts_abc_compute!` / `js_abc_compute!` invocation
8815        // compiles, passes every condition-slot test, and drops only the
8816        // two branch operands. TypeScript alone pinned those before; the
8817        // other three expansions now do too.
8818        const SRC: &str = "function f(a, b, c) { x = a ? !b : !c; }\n";
8819        let javascript = abc_conditions(LANG::Javascript, SRC);
8820        // expected: 4 — the `?`, the `a` condition slot and both negated
8821        // branch operands; non-degenerate by construction.
8822        assert_eq!(
8823            javascript, 4,
8824            "JavaScript reference value for `a ? !b : !c`"
8825        );
8826        for lang in [LANG::Mozjs, LANG::Typescript, LANG::Tsx] {
8827            assert_eq!(
8828                abc_conditions(lang, SRC),
8829                javascript,
8830                "{lang:?} ternary operand slots"
8831            );
8832        }
8833    }
8834
8835    #[test]
8836    fn javascript_instanceof_counts_condition() {
8837        // `x instanceof Foo` is a binary expression whose operator is
8838        // the `instanceof` keyword token → C = 1.
8839        check_metrics::<JavascriptParser>(
8840            "function f(x) { return x instanceof Foo; }",
8841            "foo.js",
8842            |metric| {
8843                assert_eq!(metric.abc.conditions_sum(), 1);
8844                insta::assert_json_snapshot!(metric.abc);
8845            },
8846        );
8847    }
8848
8849    #[test]
8850    fn javascript_complex_function_abc() {
8851        // Mixed-shape regression. Verified by hand:
8852        // - assignments: `let x = 0` (a `let` initializer counts)
8853        //   + `x = 5`, `x += 2`, `x++`, `x = (a>b)?a:b`, `x = b`,
8854        //   `let p = ...` (likewise) → A = 7.
8855        // - branches: `f(a, b)` self-call + `new Bar()` → B = 2.
8856        // - conditions: `a == b`, `a > 0` → 2 inside the if header
8857        //   (`&&` is not counted directly). `else` (1) + `a > b`,
8858        //   `?` → 2 in the ternary. `a < b` → 1 in the else-if.
8859        //   `!x` → 1 from the Fitzpatrick Rule 9 walker on `||`
8860        //   (issue #403): the wrapped Identifier counts once.
8861        //   `case 1` → 1 in the switch; `default` → 0 (fallthrough,
8862        //   #469). Total C = 8.
8863        check_metrics::<JavascriptParser>(
8864            "function f(a, b) {\n\
8865                 let x = 0;\n\
8866                 x = 5;\n\
8867                 x += 2;\n\
8868                 x++;\n\
8869                 if (a == b && a > 0) {\n\
8870                     x = (a > b) ? a : b;\n\
8871                 } else if (a < b || !x) {\n\
8872                     x = b;\n\
8873                 }\n\
8874                 switch (x) {\n\
8875                     case 1: break;\n\
8876                     default: break;\n\
8877                 }\n\
8878                 let p = new Bar();\n\
8879                 return f(a, b);\n\
8880             }\n",
8881            "foo.js",
8882            |metric| {
8883                assert_eq!(metric.abc.assignments_sum(), 7);
8884                assert_eq!(metric.abc.branches_sum(), 2);
8885                assert_eq!(metric.abc.conditions_sum(), 8);
8886                insta::assert_json_snapshot!(metric.abc);
8887            },
8888        );
8889    }
8890
8891    #[test]
8892    fn mozjs_asi_const_does_not_suppress_later_assignments() {
8893        // Mozjs half of the #1277 cluster; the fork carries its own
8894        // kind-id numbering, so it needs its own fixture. See
8895        // `typescript_asi_const_does_not_suppress_later_assignments`.
8896        check_metrics::<MozjsParser>(
8897            "function f() {
8898                const a = 1
8899                x = 2
8900                y = 3
8901            }",
8902            "foo.jsm",
8903            |metric| {
8904                assert_eq!(metric.abc.assignments_sum(), 2);
8905            },
8906        );
8907    }
8908
8909    #[test]
8910    fn mozjs_const_declarator_shapes_stay_suppressed() {
8911        // Mozjs half of
8912        // `javascript_const_declarator_shapes_stay_suppressed`.
8913        check_metrics::<MozjsParser>(
8914            "function f(o, xs) {
8915                const a = 1, b = 2
8916                const {c = 5, d: {e = 6} = {}} = o
8917                const [g = 7, ...[h = 8]] = xs
8918                let i = 3
8919                var j = 4
8920                for (const x of xs) { k(x) }
8921            }",
8922            "foo.jsm",
8923            |metric| {
8924                assert_eq!(metric.abc.assignments_sum(), 2);
8925            },
8926        );
8927    }
8928
8929    #[test]
8930    fn mozjs_complex_function_abc() {
8931        // Mozjs shares JavaScript's expression / statement vocabulary;
8932        // the `js_abc_compute!` macro expands identical token-level
8933        // rules for both. This test pins parity against the JS impl.
8934        check_metrics::<MozjsParser>(
8935            "function f(a, b) {\n\
8936                 let x = 0;\n\
8937                 x = 5;\n\
8938                 x += 2;\n\
8939                 x++;\n\
8940                 if (a == b && a > 0) {\n\
8941                     x = (a > b) ? a : b;\n\
8942                 } else if (a < b || !x) {\n\
8943                     x = b;\n\
8944                 }\n\
8945                 switch (x) {\n\
8946                     case 1: break;\n\
8947                     default: break;\n\
8948                 }\n\
8949                 let p = new Bar();\n\
8950                 return f(a, b);\n\
8951             }\n",
8952            "foo.js",
8953            |metric| {
8954                assert_eq!(metric.abc.assignments_sum(), 7);
8955                assert_eq!(metric.abc.branches_sum(), 2);
8956                assert_eq!(metric.abc.conditions_sum(), 8);
8957                insta::assert_json_snapshot!(metric.abc);
8958            },
8959        );
8960    }
8961
8962    // ----- JS / TS / Tsx / Mozjs Phase-2B condition slots -----
8963
8964    #[test]
8965    fn javascript_await_expression_condition_counts() {
8966        // Regression for findings.md round-2 #2 (JS):
8967        // `if (await ready()) {}` parses with `await_expression` as
8968        // the condition node inside the `parenthesized_expression`.
8969        // `javascript_inspect_container` unwraps the paren but the
8970        // await child was not in the terminal-bool set, so the
8971        // walker broke without counting. Mirrors C# (lesson 19).
8972        check_metrics::<JavascriptParser>(
8973            "async function ready() { return true; }\n\
8974             async function f() { if (await ready()) { } }\n",
8975            "foo.js",
8976            |metric| {
8977                assert_eq!(metric.abc.branches_sum(), 1);
8978                assert_eq!(metric.abc.conditions_sum(), 1);
8979                insta::assert_json_snapshot!(metric.abc);
8980            },
8981        );
8982    }
8983
8984    #[test]
8985    fn javascript_member_expression_condition_counts() {
8986        // Regression for findings.md #3 (JS-family): tree-sitter-
8987        // javascript emits `member_expression` under three kind_ids
8988        // (191 primary, 208, 228 — `MemberExpression2/3`) depending
8989        // on the production rule path. The verifier in this audit
8990        // confirmed runtime kind for `o.x` is 208. Pre-fix the
8991        // shared `js_family_bool_terminal_kinds!()` macro listed
8992        // only the primary, so every `if (o.x) {}` / `o.x && o.y`
8993        // condition silently reported zero. The per-language macro
8994        // now includes all three aliases (lesson #2).
8995        check_metrics::<JavascriptParser>(
8996            "function f(o) {\n\
8997             \x20   if (o.x) {}                  // +1c\n\
8998             \x20   return o.x && o.y;           // +2c (walker on &&)\n\
8999             }\n",
9000            "foo.js",
9001            |metric| {
9002                assert_eq!(metric.abc.conditions_sum(), 3);
9003                insta::assert_json_snapshot!(metric.abc);
9004            },
9005        );
9006    }
9007
9008    #[test]
9009    fn javascript_if_boolean_literal_condition() {
9010        check_metrics::<JavascriptParser>(
9011            "function f() {\n\
9012             \x20   if (true) {}                 // +1c\n\
9013             \x20   if (!false) {}               // +1c\n\
9014             \x20   while (true) {}              // +1c\n\
9015             \x20   do {} while (false);         // +1c\n\
9016             }\n",
9017            "foo.js",
9018            |metric| {
9019                assert_eq!(metric.abc.conditions_sum(), 4);
9020                insta::assert_json_snapshot!(metric.abc);
9021            },
9022        );
9023    }
9024
9025    #[test]
9026    fn javascript_methods_arguments_with_conditions() {
9027        check_metrics::<JavascriptParser>(
9028            "function f(a, b) {\n\
9029             \x20   m(a, b);                     // +1b\n\
9030             \x20   m(!a, !b);                   // +1b +2c\n\
9031             }\n",
9032            "foo.js",
9033            |metric| {
9034                assert_eq!(metric.abc.branches_sum(), 2);
9035                assert_eq!(metric.abc.conditions_sum(), 2);
9036                insta::assert_json_snapshot!(metric.abc);
9037            },
9038        );
9039    }
9040
9041    #[test]
9042    fn javascript_return_with_conditions() {
9043        check_metrics::<JavascriptParser>(
9044            "function m1(z) { return !(z >= 0); }\n\
9045             function m2(x) { return (((!x))); }\n\
9046             function m3(x, y) { return x && y; }\n",
9047            "foo.js",
9048            |metric| {
9049                // m1: 1 (`>=`). m2: 1 (walker unwraps to `x`).
9050                // m3: 2 (`&&` walker counts both terminals).
9051                assert_eq!(metric.abc.conditions_sum(), 4);
9052                insta::assert_json_snapshot!(metric.abc);
9053            },
9054        );
9055    }
9056
9057    #[test]
9058    fn typescript_if_boolean_literal_condition() {
9059        check_metrics::<TypescriptParser>(
9060            "function f() {\n\
9061             \x20   if (true) {}\n\
9062             \x20   if (!false) {}\n\
9063             \x20   while (true) {}\n\
9064             \x20   do {} while (false);\n\
9065             }\n",
9066            "foo.ts",
9067            |metric| {
9068                assert_eq!(metric.abc.conditions_sum(), 4);
9069                insta::assert_json_snapshot!(metric.abc);
9070            },
9071        );
9072    }
9073
9074    #[test]
9075    fn typescript_methods_arguments_with_conditions() {
9076        check_metrics::<TypescriptParser>(
9077            "function f(a: boolean, b: boolean) {\n\
9078             \x20   m(a, b);\n\
9079             \x20   m(!a, !b);\n\
9080             }\n",
9081            "foo.ts",
9082            |metric| {
9083                assert_eq!(metric.abc.branches_sum(), 2);
9084                assert_eq!(metric.abc.conditions_sum(), 2);
9085                insta::assert_json_snapshot!(metric.abc);
9086            },
9087        );
9088    }
9089
9090    #[test]
9091    fn typescript_return_with_conditions() {
9092        check_metrics::<TypescriptParser>(
9093            "function m1(z: number): boolean { return !(z >= 0); }\n\
9094             function m2(x: boolean): boolean { return (((!x))); }\n\
9095             function m3(x: boolean, y: boolean): boolean { return x && y; }\n",
9096            "foo.ts",
9097            |metric| {
9098                assert_eq!(metric.abc.conditions_sum(), 4);
9099                insta::assert_json_snapshot!(metric.abc);
9100            },
9101        );
9102    }
9103
9104    #[test]
9105    fn tsx_if_boolean_literal_condition() {
9106        check_metrics::<TsxParser>(
9107            "function f() {\n\
9108             \x20   if (true) {}\n\
9109             \x20   if (!false) {}\n\
9110             \x20   while (true) {}\n\
9111             \x20   do {} while (false);\n\
9112             }\n",
9113            "foo.tsx",
9114            |metric| {
9115                assert_eq!(metric.abc.conditions_sum(), 4);
9116                insta::assert_json_snapshot!(metric.abc);
9117            },
9118        );
9119    }
9120
9121    #[test]
9122    fn tsx_methods_arguments_with_conditions() {
9123        check_metrics::<TsxParser>(
9124            "function f(a: boolean, b: boolean) {\n\
9125             \x20   m(a, b);\n\
9126             \x20   m(!a, !b);\n\
9127             }\n",
9128            "foo.tsx",
9129            |metric| {
9130                assert_eq!(metric.abc.branches_sum(), 2);
9131                assert_eq!(metric.abc.conditions_sum(), 2);
9132                insta::assert_json_snapshot!(metric.abc);
9133            },
9134        );
9135    }
9136
9137    #[test]
9138    fn tsx_return_with_conditions() {
9139        check_metrics::<TsxParser>(
9140            "function m1(z: number): boolean { return !(z >= 0); }\n\
9141             function m2(x: boolean): boolean { return (((!x))); }\n\
9142             function m3(x: boolean, y: boolean): boolean { return x && y; }\n",
9143            "foo.tsx",
9144            |metric| {
9145                assert_eq!(metric.abc.conditions_sum(), 4);
9146                insta::assert_json_snapshot!(metric.abc);
9147            },
9148        );
9149    }
9150
9151    #[test]
9152    fn mozjs_if_boolean_literal_condition() {
9153        check_metrics::<MozjsParser>(
9154            "function f() {\n\
9155             \x20   if (true) {}\n\
9156             \x20   if (!false) {}\n\
9157             \x20   while (true) {}\n\
9158             \x20   do {} while (false);\n\
9159             }\n",
9160            "foo.js",
9161            |metric| {
9162                assert_eq!(metric.abc.conditions_sum(), 4);
9163                insta::assert_json_snapshot!(metric.abc);
9164            },
9165        );
9166    }
9167
9168    #[test]
9169    fn mozjs_methods_arguments_with_conditions() {
9170        check_metrics::<MozjsParser>(
9171            "function f(a, b) {\n\
9172             \x20   m(a, b);\n\
9173             \x20   m(!a, !b);\n\
9174             }\n",
9175            "foo.js",
9176            |metric| {
9177                assert_eq!(metric.abc.branches_sum(), 2);
9178                assert_eq!(metric.abc.conditions_sum(), 2);
9179                insta::assert_json_snapshot!(metric.abc);
9180            },
9181        );
9182    }
9183
9184    #[test]
9185    fn mozjs_return_with_conditions() {
9186        check_metrics::<MozjsParser>(
9187            "function m1(z) { return !(z >= 0); }\n\
9188             function m2(x) { return (((!x))); }\n\
9189             function m3(x, y) { return x && y; }\n",
9190            "foo.js",
9191            |metric| {
9192                assert_eq!(metric.abc.conditions_sum(), 4);
9193                insta::assert_json_snapshot!(metric.abc);
9194            },
9195        );
9196    }
9197
9198    // ----- JS / TS / Tsx / Mozjs unary-conditional walker -----
9199
9200    #[test]
9201    fn javascript_if_multiple_conditions() {
9202        check_metrics::<JavascriptParser>(
9203            "function f(a, b, c, d) {\n\
9204             \x20   if (a || b || c || d) {}        // +4c\n\
9205             \x20   if (a && b && c) {}             // +3c\n\
9206             \x20   if (!a && !b) {}                // +2c\n\
9207             }\n",
9208            "foo.js",
9209            |metric| {
9210                assert_eq!(metric.abc.conditions_sum(), 9);
9211                insta::assert_json_snapshot!(metric.abc);
9212            },
9213        );
9214    }
9215
9216    #[test]
9217    fn javascript_while_and_do_while_conditions() {
9218        check_metrics::<JavascriptParser>(
9219            "function f(a, b) {\n\
9220             \x20   while (a || b) {}              // +2c\n\
9221             \x20   do {} while (a && !b);         // +2c\n\
9222             }\n",
9223            "foo.js",
9224            |metric| {
9225                assert_eq!(metric.abc.conditions_sum(), 4);
9226                insta::assert_json_snapshot!(metric.abc);
9227            },
9228        );
9229    }
9230
9231    #[test]
9232    fn javascript_short_circuit_with_boolean_literal_operand() {
9233        check_metrics::<JavascriptParser>(
9234            "function f(a) { return a && true; }\n",
9235            "foo.js",
9236            |metric| {
9237                assert_eq!(metric.abc.conditions_sum(), 2);
9238                insta::assert_json_snapshot!(metric.abc);
9239            },
9240        );
9241    }
9242
9243    #[test]
9244    fn typescript_if_multiple_conditions() {
9245        check_metrics::<TypescriptParser>(
9246            "function f(a: boolean, b: boolean, c: boolean, d: boolean) {\n\
9247             \x20   if (a || b || c || d) {}        // +4c\n\
9248             \x20   if (a && b && c) {}             // +3c\n\
9249             \x20   if (!a && !b) {}                // +2c\n\
9250             }\n",
9251            "foo.ts",
9252            |metric| {
9253                assert_eq!(metric.abc.conditions_sum(), 9);
9254                insta::assert_json_snapshot!(metric.abc);
9255            },
9256        );
9257    }
9258
9259    #[test]
9260    fn typescript_while_and_do_while_conditions() {
9261        check_metrics::<TypescriptParser>(
9262            "function f(a: boolean, b: boolean) {\n\
9263             \x20   while (a || b) {}              // +2c\n\
9264             \x20   do {} while (a && !b);         // +2c\n\
9265             }\n",
9266            "foo.ts",
9267            |metric| {
9268                assert_eq!(metric.abc.conditions_sum(), 4);
9269                insta::assert_json_snapshot!(metric.abc);
9270            },
9271        );
9272    }
9273
9274    #[test]
9275    fn typescript_short_circuit_with_boolean_literal_operand() {
9276        check_metrics::<TypescriptParser>(
9277            "function f(a: boolean): boolean { return a && true; }\n",
9278            "foo.ts",
9279            |metric| {
9280                assert_eq!(metric.abc.conditions_sum(), 2);
9281                insta::assert_json_snapshot!(metric.abc);
9282            },
9283        );
9284    }
9285
9286    #[test]
9287    fn tsx_if_multiple_conditions() {
9288        check_metrics::<TsxParser>(
9289            "function f(a: boolean, b: boolean, c: boolean, d: boolean) {\n\
9290             \x20   if (a || b || c || d) {}        // +4c\n\
9291             \x20   if (a && b && c) {}             // +3c\n\
9292             \x20   if (!a && !b) {}                // +2c\n\
9293             }\n",
9294            "foo.tsx",
9295            |metric| {
9296                assert_eq!(metric.abc.conditions_sum(), 9);
9297                insta::assert_json_snapshot!(metric.abc);
9298            },
9299        );
9300    }
9301
9302    #[test]
9303    fn tsx_while_and_do_while_conditions() {
9304        check_metrics::<TsxParser>(
9305            "function f(a: boolean, b: boolean) {\n\
9306             \x20   while (a || b) {}              // +2c\n\
9307             \x20   do {} while (a && !b);         // +2c\n\
9308             }\n",
9309            "foo.tsx",
9310            |metric| {
9311                assert_eq!(metric.abc.conditions_sum(), 4);
9312                insta::assert_json_snapshot!(metric.abc);
9313            },
9314        );
9315    }
9316
9317    #[test]
9318    fn tsx_short_circuit_with_boolean_literal_operand() {
9319        check_metrics::<TsxParser>(
9320            "function f(a: boolean): boolean { return a && true; }\n",
9321            "foo.tsx",
9322            |metric| {
9323                assert_eq!(metric.abc.conditions_sum(), 2);
9324                insta::assert_json_snapshot!(metric.abc);
9325            },
9326        );
9327    }
9328
9329    #[test]
9330    fn mozjs_if_multiple_conditions() {
9331        check_metrics::<MozjsParser>(
9332            "function f(a, b, c, d) {\n\
9333             \x20   if (a || b || c || d) {}        // +4c\n\
9334             \x20   if (a && b && c) {}             // +3c\n\
9335             \x20   if (!a && !b) {}                // +2c\n\
9336             }\n",
9337            "foo.js",
9338            |metric| {
9339                assert_eq!(metric.abc.conditions_sum(), 9);
9340                insta::assert_json_snapshot!(metric.abc);
9341            },
9342        );
9343    }
9344
9345    #[test]
9346    fn mozjs_while_and_do_while_conditions() {
9347        check_metrics::<MozjsParser>(
9348            "function f(a, b) {\n\
9349             \x20   while (a || b) {}              // +2c\n\
9350             \x20   do {} while (a && !b);         // +2c\n\
9351             }\n",
9352            "foo.js",
9353            |metric| {
9354                assert_eq!(metric.abc.conditions_sum(), 4);
9355                insta::assert_json_snapshot!(metric.abc);
9356            },
9357        );
9358    }
9359
9360    #[test]
9361    fn mozjs_short_circuit_with_boolean_literal_operand() {
9362        check_metrics::<MozjsParser>(
9363            "function f(a) { return a && true; }\n",
9364            "foo.js",
9365            |metric| {
9366                assert_eq!(metric.abc.conditions_sum(), 2);
9367                insta::assert_json_snapshot!(metric.abc);
9368            },
9369        );
9370    }
9371
9372    // ---------- Perl ABC tests ----------
9373
9374    #[test]
9375    fn perl_empty_unit_zero() {
9376        // Empty source produces zero ABC magnitude — pins the trait
9377        // wiring without exercising any compute branch.
9378        check_metrics::<PerlParser>("", "empty.pl", |metric| {
9379            assert_eq!(metric.abc.assignments_sum(), 0);
9380            assert_eq!(metric.abc.branches_sum(), 0);
9381            assert_eq!(metric.abc.conditions_sum(), 0);
9382            insta::assert_json_snapshot!(metric.abc);
9383        });
9384    }
9385
9386    #[test]
9387    fn perl_plain_and_compound_assignments_count() {
9388        // `my $x = 0` parses as a `binary_expression` with an `=`
9389        // token, so the initialiser counts (Perl has no equivalent of
9390        // the JS `const` initialiser-suppression rule). Each
9391        // assignment operator token contributes one assignment:
9392        // `=`, `=`, `+=`, `.=`, `**=` → A = 5. Two of those `=` come
9393        // from the `my $x = 0` initialiser and the later `$x = 5`
9394        // reassignment.
9395        check_metrics::<PerlParser>(
9396            "sub f { my $x = 0; $x = 5; $x += 2; $x .= \"a\"; $x **= 3; }",
9397            "foo.pl",
9398            |metric| {
9399                assert_eq!(metric.abc.assignments_sum(), 5);
9400                assert_eq!(metric.abc.branches_sum(), 0);
9401                assert_eq!(metric.abc.conditions_sum(), 0);
9402                insta::assert_json_snapshot!(metric.abc);
9403            },
9404        );
9405    }
9406
9407    #[test]
9408    fn perl_calls_are_branches() {
9409        // `foo()` parses as `call_expression_with_args_with_brackets`
9410        // wrapping an inner `call_expression_with_bareword(foo)`;
9411        // `bar 1, 2` wraps `bar` likewise under spaced-args; `shift`
9412        // appears as a standalone bareword. The bareword-inside-
9413        // wrapper case must NOT double-count — only the outer wrapper
9414        // contributes a branch. So B = 3 (foo, bar, shift), not 5.
9415        check_metrics::<PerlParser>(
9416            "sub f { foo(); bar 1, 2; my $a = shift; }",
9417            "foo.pl",
9418            |metric| {
9419                // shift's `my $a = shift` initialiser contributes one
9420                // assignment via the `=` token.
9421                assert_eq!(metric.abc.assignments_sum(), 1);
9422                assert_eq!(metric.abc.branches_sum(), 3);
9423                assert_eq!(metric.abc.conditions_sum(), 0);
9424                insta::assert_json_snapshot!(metric.abc);
9425            },
9426        );
9427    }
9428
9429    #[test]
9430    fn perl_method_invocation_counts_as_branch() {
9431        // `$obj->method(...)` parses as `method_invocation`. Any
9432        // arrow-dispatch counts as one branch regardless of how the
9433        // arguments are passed.
9434        check_metrics::<PerlParser>(
9435            "sub f { my $obj = shift; $obj->run($x); $obj->ping; }",
9436            "foo.pl",
9437            |metric| {
9438                // `my $obj = shift` → A=1, B=1 (shift bareword).
9439                // `$obj->run($x)` and `$obj->ping` → 2 more branches.
9440                assert_eq!(metric.abc.assignments_sum(), 1);
9441                assert_eq!(metric.abc.branches_sum(), 3);
9442                assert_eq!(metric.abc.conditions_sum(), 0);
9443                insta::assert_json_snapshot!(metric.abc);
9444            },
9445        );
9446    }
9447
9448    #[test]
9449    fn perl_numeric_and_string_comparisons_count_conditions() {
9450        // Numeric ops `==`, `!=`, `<`, `>`, `<=`, `>=`, `<=>` and
9451        // string ops `eq`, `ne`, `lt`, `gt`, `le`, `ge`, `cmp` each
9452        // fire once per token. The sample below uses one of each →
9453        // C = 14. No assignments, no branches.
9454        check_metrics::<PerlParser>(
9455            "sub f {\n\
9456                 my $r;\n\
9457                 $r = $a == $b;\n\
9458                 $r = $a != $b;\n\
9459                 $r = $a <  $b;\n\
9460                 $r = $a >  $b;\n\
9461                 $r = $a <= $b;\n\
9462                 $r = $a >= $b;\n\
9463                 $r = $a <=> $b;\n\
9464                 $r = $a eq $b;\n\
9465                 $r = $a ne $b;\n\
9466                 $r = $a lt $b;\n\
9467                 $r = $a gt $b;\n\
9468                 $r = $a le $b;\n\
9469                 $r = $a ge $b;\n\
9470                 $r = $a cmp $b;\n\
9471             }",
9472            "foo.pl",
9473            |metric| {
9474                // 15 `=` tokens: one declaration `my $r` (no `=`),
9475                // then 14 `$r = …` plus there's no `=` in `my $r;`.
9476                // Actually: `my $r;` has no `=`; the 14 `$r = …` are
9477                // 14 `=` tokens. So A=14, C=14.
9478                assert_eq!(metric.abc.assignments_sum(), 14);
9479                assert_eq!(metric.abc.branches_sum(), 0);
9480                assert_eq!(metric.abc.conditions_sum(), 14);
9481                insta::assert_json_snapshot!(metric.abc);
9482            },
9483        );
9484    }
9485
9486    #[test]
9487    fn perl_short_circuit_not_counted_directly_ternary_counts() {
9488        // `&&`, `||`, `//`, low-precedence `and`, `or`, `xor` are
9489        // NOT counted as conditions on their own (Fitzpatrick Rule
9490        // 5; #395) — instead each operand is counted as a unary
9491        // conditional by the walker (Rule 9; #403). At the pinned
9492        // tree-sitter-perl grammar version, only the four
9493        // punctuation forms plus one keyword form parse under a
9494        // `binary_expression` parent that triggers the walker; the
9495        // other two keyword forms parse under a distinct grammar
9496        // node and contribute zero. Net: 4 walker-firing lines × 2
9497        // scalar-variable operands + 1 ternary node + 1 for the
9498        // ternary's bare `$a` condition operand (#1102) = 10. The
9499        // exact mix of "which two keyword forms are silent" is
9500        // grammar-version-dependent; a future grammar bump that
9501        // normalises the keyword forms' parent kind will shift this
9502        // count to 14. See follow-up note above the test name.
9503        check_metrics::<PerlParser>(
9504            "sub f {\n\
9505                 my $r;\n\
9506                 $r = $a && $b;\n\
9507                 $r = $a || $b;\n\
9508                 $r = $a // $b;\n\
9509                 $r = $a and $b;\n\
9510                 $r = $a or  $b;\n\
9511                 $r = $a xor $b;\n\
9512                 $r = $a ? 1 : 2;\n\
9513             }",
9514            "foo.pl",
9515            |metric| {
9516                // 7 `=` tokens (one per reassignment line).
9517                assert_eq!(metric.abc.assignments_sum(), 7);
9518                assert_eq!(metric.abc.branches_sum(), 0);
9519                // 4 walker-triggered lines × 2 operands + 1 ternary
9520                // node + 1 for its bare `$a` condition operand = 10.
9521                // The two remaining low-precedence keyword forms (one
9522                // of `and`/`or`/`xor`) fall under a
9523                // non-binary_expression parent in this grammar
9524                // version and contribute zero via the walker.
9525                assert_eq!(metric.abc.conditions_sum(), 10);
9526                insta::assert_json_snapshot!(metric.abc);
9527            },
9528        );
9529    }
9530
9531    // Issue #1102, Perl half. See
9532    // `cpp_ternary_operand_slots_count_as_unary_conditions` for the
9533    // rule. Like PHP, Perl's ABC dispatcher has no `?`-token arm — the
9534    // grammar does emit the token, but the `ternary_expression` node is
9535    // what carries the tally's +1. tree-sitter-perl names the branch
9536    // fields `true` / `false` rather than the C-family `consequence` /
9537    // `alternative`, so a copied C-family gate would match nothing.
9538    #[test]
9539    fn perl_ternary_operand_slots_count_as_unary_conditions() {
9540        // ternary (1) + condition `$a` (1) + `!$b` (1) + `!$c` (1) = 4.
9541        check_metrics::<PerlParser>("sub f { my $x = $a ? !$b : !$c; }", "foo.pl", |metric| {
9542            assert_eq!(metric.abc.conditions_sum(), 4);
9543        });
9544        // No-double-count pin: ternary (1) + `>` (1) = 2, unchanged by
9545        // the fix.
9546        check_metrics::<PerlParser>(
9547            "sub f { my $x = ($a > 0) ? $b : -$b; }",
9548            "foo.pl",
9549            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
9550        );
9551        // A negated *condition* takes the walker's `else` fallback —
9552        // `!$a` is neither a boolean terminal nor a paren wrapper, so
9553        // only `perl_inspect_container` can classify it. Delete the
9554        // fallback and this reads 1. ternary (1) + `!$a` (1) = 2.
9555        check_metrics::<PerlParser>("sub f { my $x = !$a ? $b : $c; }", "foo.pl", |metric| {
9556            assert_eq!(metric.abc.conditions_sum(), 2);
9557        });
9558        // Nested: two ternary nodes plus the two bare-variable
9559        // conditions = 4.
9560        check_metrics::<PerlParser>(
9561            "sub f { my $x = $a ? ($b ? $c : $d) : $e; }",
9562            "foo.pl",
9563            |metric| assert_eq!(metric.abc.conditions_sum(), 4),
9564        );
9565    }
9566
9567    #[test]
9568    fn perl_elsif_and_else_count_conditions() {
9569        // `if (… == …) { … } elsif (… < …) { … } else { … }` →
9570        // 2 comparison tokens (`==`, `<`), plus `elsif_clause` and
9571        // `else_clause` each + 1 → C = 4. Branches: 0 (only
9572        // assignments). Assignments: just the `=` initialisers /
9573        // reassignments — there are 4 here (`$x` init plus three
9574        // `$x = …` reassigns).
9575        check_metrics::<PerlParser>(
9576            "sub f {\n\
9577                 my $x = 0;\n\
9578                 if ($a == $b) {\n\
9579                     $x = 1;\n\
9580                 } elsif ($a < $b) {\n\
9581                     $x = 2;\n\
9582                 } else {\n\
9583                     $x = 3;\n\
9584                 }\n\
9585             }",
9586            "foo.pl",
9587            |metric| {
9588                assert_eq!(metric.abc.assignments_sum(), 4);
9589                assert_eq!(metric.abc.branches_sum(), 0);
9590                assert_eq!(metric.abc.conditions_sum(), 4);
9591                insta::assert_json_snapshot!(metric.abc);
9592            },
9593        );
9594    }
9595
9596    #[test]
9597    fn perl_regex_match_operators_count_conditions() {
9598        // `=~` and `!~` are pattern-match operators; we count both
9599        // as conditions because they evaluate the regex match in a
9600        // boolean context.
9601        check_metrics::<PerlParser>(
9602            "sub f { my $s = shift; my $m = $s =~ /foo/; my $n = $s !~ /bar/; }",
9603            "foo.pl",
9604            |metric| {
9605                // 3 `=` tokens, 0 branches except `shift` bareword.
9606                assert_eq!(metric.abc.assignments_sum(), 3);
9607                assert_eq!(metric.abc.branches_sum(), 1);
9608                assert_eq!(metric.abc.conditions_sum(), 2);
9609                insta::assert_json_snapshot!(metric.abc);
9610            },
9611        );
9612    }
9613
9614    #[test]
9615    fn perl_complex_function_abc() {
9616        // Mixed program exercising every category. Computed
9617        // expected:
9618        //   Assignments: `my $i = 0` (1), `$i++` is a unary
9619        //     increment — Perl's grammar emits `PLUSPLUS` not an `=`
9620        //     operator, so it does NOT count under the operator-
9621        //     token rule. The for-loop's `$i++` is similarly
9622        //     uncounted.
9623        //     Total A: 1 from `my $i = 0`, 1 from `$total += $i`
9624        //     (the `+=` token) → A = 2.
9625        //   Branches: `do_work($i)` → 1; `print "done\n"` is a
9626        //     call_expression_with_spaced_args → 1; `return $total`
9627        //     uses the `return` keyword not a call → 0. B = 2.
9628        //   Conditions: `$i < 10` (`<`) → 1; `$i % 2 == 0` (`==`) →
9629        //     1; `else_clause` → 1. C = 3.
9630        check_metrics::<PerlParser>(
9631            "sub run {\n\
9632                 my $total = 0;\n\
9633                 for (my $i = 0; $i < 10; $i++) {\n\
9634                     if ($i % 2 == 0) {\n\
9635                         do_work($i);\n\
9636                     } else {\n\
9637                         $total += $i;\n\
9638                     }\n\
9639                 }\n\
9640                 print \"done\\n\";\n\
9641                 return $total;\n\
9642             }",
9643            "foo.pl",
9644            |metric| {
9645                // `my $total = 0` is one `=`; `my $i = 0` is another
9646                // `=`; `$total += $i` is one `+=`. Total = 3.
9647                assert_eq!(metric.abc.assignments_sum(), 3);
9648                assert_eq!(metric.abc.branches_sum(), 2);
9649                assert_eq!(metric.abc.conditions_sum(), 3);
9650                insta::assert_json_snapshot!(metric.abc);
9651            },
9652        );
9653    }
9654
9655    #[test]
9656    fn perl_if_multiple_conditions() {
9657        // Fitzpatrick Rule 9 walker (issue #403): each operand of a
9658        // `&&` / `||` / `//` / `and` / `or` / `xor` chain is one
9659        // condition. ScalarVariable operands ($a, $b, …) qualify as
9660        // terminal-bool kinds for the walker.
9661        check_metrics::<PerlParser>(
9662            "sub f {\n\
9663                 my ($a, $b, $c, $d) = @_;\n\
9664                 if ($a || $b || $c || $d) { return 1; }    # +4c\n\
9665                 if ($a && $b && $c) { return 2; }          # +3c\n\
9666                 if (!$a && !$b) { return 3; }              # +2c\n\
9667                 return 0;\n\
9668             }",
9669            "foo.pl",
9670            |metric| {
9671                assert_eq!(metric.abc.conditions_sum(), 9);
9672                insta::assert_json_snapshot!(metric.abc);
9673            },
9674        );
9675    }
9676
9677    #[test]
9678    fn perl_while_and_until_conditions() {
9679        // Perl has no `do { ... } while(cond);` shape in this grammar
9680        // — `while` and `until` are the loop forms with a condition
9681        // slot. The walker fires on each `&&` / `||` token inside
9682        // those headers.
9683        check_metrics::<PerlParser>(
9684            "sub f {\n\
9685                 my ($a, $b) = @_;\n\
9686                 while ($a || $b) { last; }            # +2c\n\
9687                 until ($a && !$b) { last; }           # +2c\n\
9688             }",
9689            "foo.pl",
9690            |metric| {
9691                assert_eq!(metric.abc.conditions_sum(), 4);
9692                insta::assert_json_snapshot!(metric.abc);
9693            },
9694        );
9695    }
9696
9697    #[test]
9698    fn perl_for_header_condition_slot_counts_unary_conditions() {
9699        // The Perl half of #1276. The C-style `for` header's condition
9700        // slot is a Rule 9 unary condition like the `if` slot: a bare
9701        // `$ok`, a negated `!$ok` and a parenthesised `($ok)` each count
9702        // one, an empty header and a `foreach` count zero, and a
9703        // comparison-shaped `$i < $n` stays at the one the `<` arm
9704        // already counts. Every C-style row is the three-clause spelling
9705        // because tree-sitter-perl does not parse an empty initializer.
9706        let cases = [
9707            ("for (my $i = 0; $ok; $i++) { }", 1),
9708            ("for (my $i = 0; !$ok; $i++) { }", 1),
9709            ("for (my $i = 0; ($ok); $i++) { }", 1),
9710            ("for (my $i = 0; $i < $n; $i++) { }", 1),
9711            ("for (my $i = 0; $ok && $j; $i++) { }", 2),
9712            ("for (;;) { last; }", 0),
9713            ("for my $x (@l) { }", 0),
9714        ];
9715        let mut ran = 0;
9716        for (body, expected) in cases {
9717            let src = format!("sub f {{ {body} }}\n");
9718            assert_eq!(abc_conditions(LANG::Perl, &src), expected, "`{body}`");
9719            ran += 1;
9720        }
9721        assert_eq!(ran, cases.len());
9722        assert!(cases.iter().any(|&(_, n)| n == 0));
9723        assert!(cases.iter().any(|&(_, n)| n == 2));
9724
9725        // The slot agrees with the `if` slot for every shape, which is
9726        // the property the fix restores; the reference is non-degenerate
9727        // by the table above.
9728        for condition in ["$ok", "!$ok", "($ok)", "$ok && $j"] {
9729            let in_for = format!("sub f {{ for (my $i = 0; {condition}; $i++) {{ }} }}\n");
9730            let in_if = format!("sub f {{ if ({condition}) {{ }} }}\n");
9731            assert_eq!(
9732                abc_conditions(LANG::Perl, &in_for),
9733                abc_conditions(LANG::Perl, &in_if),
9734                "`{condition}` must score alike in a `for` header and an `if`",
9735            );
9736        }
9737    }
9738
9739    #[test]
9740    fn perl_short_circuit_counts_scalar_variable_operands() {
9741        // `$a && $b` reports 2 conditions — one walker count per
9742        // `ScalarVariable` operand. Renamed from the cross-language
9743        // `_with_boolean_literal_operand` convention because Perl has
9744        // no readily-grammar-exposed boolean literal in an `&&`
9745        // operand slot at the pinned grammar version (the `Boolean`
9746        // kind only fires on the `boolean` pragma's named constants,
9747        // not bareword `1` / `0`). Two scalar variables are the
9748        // grammar-stable terminal-set witness for Perl.
9749        check_metrics::<PerlParser>(
9750            "sub f { my ($a) = @_; return $a && $b; }\n",
9751            "foo.pl",
9752            |metric| {
9753                assert_eq!(metric.abc.conditions_sum(), 2);
9754                insta::assert_json_snapshot!(metric.abc);
9755            },
9756        );
9757    }
9758
9759    #[test]
9760    fn perl_array_in_binary_operand_descends_to_scalar_context_value() {
9761        // Regression test for the code-review findings on the
9762        // Phase-2B Perl walker:
9763        //   - Pre-fix-A: `perl_inspect_container` descended `Array`
9764        //     via `node.child(1)` — the FIRST element — wrongly
9765        //     attributing `$x` for `($x, $y)` (semantically `$y`
9766        //     is the scalar-context value).
9767        //   - Fix-A (the `array_is_paren` guard, 5db8078): dropped
9768        //     Array-as-paren entirely in `BinaryExpression` operand
9769        //     contexts to avoid the wrong attribution — but
9770        //     regressed `$a || ($x)` (single paren-grouped operand)
9771        //     to C=1 instead of 2.
9772        //   - Fix-B (this change): keeps Array-as-paren unconditional
9773        //     but descends via the LAST named child. `$a || ($x)`
9774        //     reaches `$x` (count both operands → 2);
9775        //     `$a || ($x, $y)` reaches `$y` (count `$a` + `$y` →
9776        //     still 2, matching Fitzpatrick Rule 7 "one per
9777        //     operand"); `if ($a)` still reaches `$a` (single-
9778        //     element grouping → 1).
9779        check_metrics::<PerlParser>(
9780            "sub f { my ($a, $x, $y) = @_;\n\
9781             \x20   my $r = $a || ($x, $y);    # +2c: $a + last-named $y\n\
9782             \x20   my $s = $a || ($x);        # +2c: $a + only-named $x\n\
9783             \x20   $r + $s;\n\
9784             }\n",
9785            "foo.pl",
9786            |metric| {
9787                // 2 + 2 = 4 unary conditions from the two `||`s.
9788                assert_eq!(metric.abc.conditions_sum(), 4);
9789                insta::assert_json_snapshot!(metric.abc);
9790            },
9791        );
9792    }
9793
9794    #[test]
9795    fn perl_if_scalar_variable_condition() {
9796        // Renamed from the cross-language
9797        // `_if_boolean_literal_condition` convention because
9798        // Perl has no readily-grammar-exposed boolean literal in
9799        // an `if (cond)` slot at the pinned grammar version:
9800        // tree-sitter-perl's `Boolean` kind only fires for the
9801        // `boolean` pragma's named constants (not bareword `1` /
9802        // `0`, which surface as `Integer` / not in the
9803        // terminal-bool set). A scalar-variable condition is the
9804        // grammar-stable witness — `if ($a)` reaches
9805        // `scalar_variable` via the `Array` paren unwrap.
9806        check_metrics::<PerlParser>(
9807            "sub f { my ($a) = @_; if ($a) { return 1; } }\n",
9808            "foo.pl",
9809            |metric| {
9810                assert_eq!(metric.abc.conditions_sum(), 1);
9811                insta::assert_json_snapshot!(metric.abc);
9812            },
9813        );
9814    }
9815
9816    #[test]
9817    fn perl_methods_arguments_with_conditions() {
9818        // `call(!$a, !$b)` — argument list walker counts each
9819        // unary-conditional argument once. Cannot use `m(...)` as
9820        // the function name — tree-sitter-perl parses `m(...)` as
9821        // the regex-match operator, not a function call.
9822        check_metrics::<PerlParser>(
9823            "sub f { my ($a, $b) = @_; call($a, $b); call(!$a, !$b); }\n",
9824            "foo.pl",
9825            |metric| {
9826                // Two calls × 1 branch each = 2 branches.
9827                // `call(!$a, !$b)` contributes 2 walker conditions
9828                // (one per `!`-wrapped scalar-variable argument);
9829                // `call($a, $b)` contributes 0 (bare-args don't
9830                // count via the Arguments walker — list_kind !=
9831                // BinaryExpression).
9832                assert_eq!(metric.abc.branches_sum(), 2);
9833                assert_eq!(metric.abc.conditions_sum(), 2);
9834                insta::assert_json_snapshot!(metric.abc);
9835            },
9836        );
9837    }
9838
9839    #[test]
9840    fn perl_return_with_conditions() {
9841        // `return !$a` reports 1 condition via the walker (unary
9842        // unwrap to scalar-variable terminal). `return $a` reports
9843        // 0 (no paren / unary wrap, has_boolean_content stays
9844        // false from ReturnExpression parent).
9845        check_metrics::<PerlParser>(
9846            "sub m1 { my ($z) = @_; return !($z); }\n\
9847             sub m2 { my ($x) = @_; return (((!$x))); }\n\
9848             sub m3 { my ($x, $y) = @_; return $x && $y; }\n",
9849            "foo.pl",
9850            |metric| {
9851                // m1: !($z) → walker on `!` unwraps paren to $z (1).
9852                // m2: (((!$x))) → walker unwraps three parens + one
9853                //     unary to $x (1).
9854                // m3: $x && $y → walker on `&&` counts both (2).
9855                // Sum: 4.
9856                assert_eq!(metric.abc.conditions_sum(), 4);
9857                insta::assert_json_snapshot!(metric.abc);
9858            },
9859        );
9860    }
9861
9862    // ---------- Lua ABC tests ----------
9863
9864    #[test]
9865    fn lua_empty_unit_zero() {
9866        check_metrics::<LuaParser>("", "empty.lua", |metric| {
9867            assert_eq!(metric.abc.assignments_sum(), 0);
9868            assert_eq!(metric.abc.branches_sum(), 0);
9869            assert_eq!(metric.abc.conditions_sum(), 0);
9870            insta::assert_json_snapshot!(metric.abc);
9871        });
9872    }
9873
9874    #[test]
9875    fn lua_assignments_count_locals_and_plain() {
9876        // `local x = 0` wraps an `assignment_statement` under a
9877        // `variable_declaration`; the inner wrapper still counts.
9878        // Multi-target assignment `a, b = 1, 2` is a single
9879        // `assignment_statement` and contributes 1, NOT 2 — the
9880        // wrapper is the unit of counting (matches the Python rule:
9881        // one `Assignment` node, one assignment).
9882        check_metrics::<LuaParser>(
9883            "function f()\n\
9884                 local x = 0\n\
9885                 x = 1\n\
9886                 local a, b = 1, 2\n\
9887                 a, b = b, a\n\
9888             end",
9889            "foo.lua",
9890            |metric| {
9891                assert_eq!(metric.abc.assignments_sum(), 4);
9892                assert_eq!(metric.abc.branches_sum(), 0);
9893                assert_eq!(metric.abc.conditions_sum(), 0);
9894                insta::assert_json_snapshot!(metric.abc);
9895            },
9896        );
9897    }
9898
9899    #[test]
9900    fn lua_calls_are_branches() {
9901        // `print(x)`, `obj.m(x)`, `obj:m(x)`, `f(g(1))` — every
9902        // call form is a `function_call` node. The nested
9903        // `f(g(1))` counts as 2 branches (one per dispatch).
9904        check_metrics::<LuaParser>(
9905            "function r(x)\n\
9906                 print(x)\n\
9907                 obj.m(x)\n\
9908                 obj:m(x)\n\
9909                 return f(g(1))\n\
9910             end",
9911            "foo.lua",
9912            |metric| {
9913                assert_eq!(metric.abc.assignments_sum(), 0);
9914                assert_eq!(metric.abc.branches_sum(), 5);
9915                assert_eq!(metric.abc.conditions_sum(), 0);
9916                insta::assert_json_snapshot!(metric.abc);
9917            },
9918        );
9919    }
9920
9921    #[test]
9922    fn lua_comparisons_count_logical_ops_do_not() {
9923        // Each comparison token contributes one condition; `and` /
9924        // `or` are NOT counted on their own (Fitzpatrick Rule 5;
9925        // #395) — instead each operand is counted as a unary
9926        // conditional by the walker (Rule 9; #403). The two
9927        // `a and b` / `a or b` lines add 2 walker conditions each.
9928        check_metrics::<LuaParser>(
9929            "function f(a, b)\n\
9930                 local r\n\
9931                 r = a == b\n\
9932                 r = a ~= b\n\
9933                 r = a <  b\n\
9934                 r = a >  b\n\
9935                 r = a <= b\n\
9936                 r = a >= b\n\
9937                 r = a and b\n\
9938                 r = a or  b\n\
9939             end",
9940            "foo.lua",
9941            |metric| {
9942                // 8 `r = …` reassignments, plus `local r` (no `=`).
9943                assert_eq!(metric.abc.assignments_sum(), 8);
9944                assert_eq!(metric.abc.branches_sum(), 0);
9945                // 6 comparisons (+6) + 2 logical lines × 2 walker
9946                // operands (+4) = 10.
9947                assert_eq!(metric.abc.conditions_sum(), 10);
9948                insta::assert_json_snapshot!(metric.abc);
9949            },
9950        );
9951    }
9952
9953    #[test]
9954    fn lua_elseif_and_else_count_conditions() {
9955        // Each elseif / else arm of the if contributes one
9956        // condition, mirroring the Python rule.
9957        check_metrics::<LuaParser>(
9958            "function f(x)\n\
9959                 if x > 0 then\n\
9960                     return 1\n\
9961                 elseif x < 0 then\n\
9962                     return -1\n\
9963                 else\n\
9964                     return 0\n\
9965                 end\n\
9966             end",
9967            "foo.lua",
9968            |metric| {
9969                // Comparisons: `>`, `<` → 2; elseif_statement → 1;
9970                // else_statement → 1. C = 4. No branches (no calls).
9971                assert_eq!(metric.abc.assignments_sum(), 0);
9972                assert_eq!(metric.abc.branches_sum(), 0);
9973                assert_eq!(metric.abc.conditions_sum(), 4);
9974                insta::assert_json_snapshot!(metric.abc);
9975            },
9976        );
9977    }
9978
9979    #[test]
9980    fn lua_complex_function_abc() {
9981        // Combines every category to pin the metric.
9982        check_metrics::<LuaParser>(
9983            "function run(n)\n\
9984                 local total = 0\n\
9985                 for i = 1, n do\n\
9986                     if i % 2 == 0 then\n\
9987                         do_work(i)\n\
9988                     else\n\
9989                         total = total + i\n\
9990                     end\n\
9991                 end\n\
9992                 print(\"done\")\n\
9993                 return total\n\
9994             end",
9995            "foo.lua",
9996            |metric| {
9997                // Assignments: `local total = 0` (1), `total = total + i` (1) → 2.
9998                // Branches: `do_work(i)` (1), `print(\"done\")` (1) → 2.
9999                // Conditions: `==` (1), `else_statement` (1) → 2.
10000                assert_eq!(metric.abc.assignments_sum(), 2);
10001                assert_eq!(metric.abc.branches_sum(), 2);
10002                assert_eq!(metric.abc.conditions_sum(), 2);
10003                insta::assert_json_snapshot!(metric.abc);
10004            },
10005        );
10006    }
10007
10008    #[test]
10009    fn lua_if_multiple_conditions() {
10010        // Fitzpatrick Rule 9 walker (issue #403). Lua's `and` / `or`
10011        // are keyword tokens inside a `binary_expression`.
10012        check_metrics::<LuaParser>(
10013            "function f(a, b, c, d)\n\
10014                 if a or b or c or d then return 1 end       -- +4c\n\
10015                 if a and b and c then return 2 end          -- +3c\n\
10016                 if not a and not b then return 3 end        -- +2c\n\
10017                 return 0\n\
10018             end",
10019            "foo.lua",
10020            |metric| {
10021                assert_eq!(metric.abc.conditions_sum(), 9);
10022                insta::assert_json_snapshot!(metric.abc);
10023            },
10024        );
10025    }
10026
10027    #[test]
10028    fn lua_while_conditions() {
10029        // Lua has no `do { ... } while(cond);` — `while cond do …
10030        // end` and `repeat … until cond` are the loop forms.
10031        check_metrics::<LuaParser>(
10032            "function f(a, b)\n\
10033                 while a or b do break end                   -- +2c\n\
10034                 repeat break until a and not b              -- +2c\n\
10035             end",
10036            "foo.lua",
10037            |metric| {
10038                assert_eq!(metric.abc.conditions_sum(), 4);
10039                insta::assert_json_snapshot!(metric.abc);
10040            },
10041        );
10042    }
10043
10044    #[test]
10045    fn lua_short_circuit_with_boolean_literal_operand() {
10046        // `a and true` reports 2 conditions: one Identifier, one
10047        // True keyword literal.
10048        check_metrics::<LuaParser>("function f(a) return a and true end", "foo.lua", |metric| {
10049            assert_eq!(metric.abc.conditions_sum(), 2);
10050            insta::assert_json_snapshot!(metric.abc);
10051        });
10052    }
10053
10054    #[test]
10055    fn lua_number_truthy_condition_counts() {
10056        // Regression for findings.md #2: Lua treats every non-nil,
10057        // non-false value as truthy, so `if 1 then ... end` and
10058        // `return a and 2` should each count their numeric literal
10059        // as a Fitzpatrick Rule 6 / 7 unary condition. Pre-fix,
10060        // `lua_bool_terminal_kinds!()` listed `True` / `False` /
10061        // `Nil` but omitted `Number`, so the walker dropped every
10062        // numeric-truthy operand. The walker comment at the top of
10063        // `lua_inspect_container` already promised numbers were
10064        // terminal-bool kinds; this commit closes the gap.
10065        check_metrics::<LuaParser>(
10066            "function f(a)\n\
10067             \x20   if 1 then return 1 end\n\
10068             \x20   return a and 2\n\
10069             end",
10070            "foo.lua",
10071            |metric| {
10072                // `if 1 then` → walker counts the Number literal (+1).
10073                // `a and 2` → `and` walker counts both operands:
10074                //   identifier `a` (+1), Number `2` (+1).
10075                // Total: 3.
10076                assert_eq!(metric.abc.conditions_sum(), 3);
10077                insta::assert_json_snapshot!(metric.abc);
10078            },
10079        );
10080    }
10081
10082    #[test]
10083    fn lua_if_boolean_literal_condition() {
10084        check_metrics::<LuaParser>(
10085            "function f()\n\
10086                 if true then end                  -- +1c\n\
10087                 if not false then end             -- +1c\n\
10088                 while true do break end           -- +1c\n\
10089                 repeat break until false          -- +1c\n\
10090             end",
10091            "foo.lua",
10092            |metric| {
10093                assert_eq!(metric.abc.conditions_sum(), 4);
10094                insta::assert_json_snapshot!(metric.abc);
10095            },
10096        );
10097    }
10098
10099    #[test]
10100    fn lua_methods_arguments_with_conditions() {
10101        // `m(not a, not b)` — argument list walker counts each
10102        // unary-conditional argument once. Bare-identifier args
10103        // (`m(a, b)`) do not count (list_kind != BinaryExpression).
10104        check_metrics::<LuaParser>(
10105            "function f(a, b) m(a, b); m(not a, not b) end",
10106            "foo.lua",
10107            |metric| {
10108                assert_eq!(metric.abc.branches_sum(), 2);
10109                assert_eq!(metric.abc.conditions_sum(), 2);
10110                insta::assert_json_snapshot!(metric.abc);
10111            },
10112        );
10113    }
10114
10115    #[test]
10116    fn lua_return_with_conditions() {
10117        // `return not (z >= 0)` → walker on `not` unwraps the paren
10118        // chain and reaches the inner BinaryExpression; the inner
10119        // `>=` comparison is the actual Fitzpatrick condition.
10120        check_metrics::<LuaParser>(
10121            "function m1(z) return not (z >= 0) end\n\
10122             function m2(x) return (((not x))) end\n\
10123             function m3(x, y) return x and y end",
10124            "foo.lua",
10125            |metric| {
10126                // m1: `>=` (1). `not` wraps a paren'd
10127                //     BinaryExpression — Lua's lua_inspect_container
10128                //     reaches the inner BinaryExpression and stops,
10129                //     no walker count. +1.
10130                // m2: ReturnStatement → iterate expression_list →
10131                //     inspect_container on the outermost paren →
10132                //     unwraps to `x` in has_boolean_content-true
10133                //     (seeded by the `not`). +1.
10134                // m3: x and y → `and` walker counts both → +2.
10135                // Sum: 4.
10136                assert_eq!(metric.abc.conditions_sum(), 4);
10137                insta::assert_json_snapshot!(metric.abc);
10138            },
10139        );
10140    }
10141
10142    // ---------- Tcl ABC tests ----------
10143
10144    #[test]
10145    fn tcl_empty_unit_zero() {
10146        check_metrics::<TclParser>("", "empty.tcl", |metric| {
10147            assert_eq!(metric.abc.assignments_sum(), 0);
10148            assert_eq!(metric.abc.branches_sum(), 0);
10149            assert_eq!(metric.abc.conditions_sum(), 0);
10150            insta::assert_json_snapshot!(metric.abc);
10151        });
10152    }
10153
10154    #[test]
10155    fn tcl_set_command_counts_assignment() {
10156        // `set` has its own grammar production; each invocation is
10157        // one assignment.
10158        check_metrics::<TclParser>(
10159            "proc f {} {\n\
10160                 set x 1\n\
10161                 set y 2\n\
10162                 set x [expr {$x + $y}]\n\
10163             }",
10164            "foo.tcl",
10165            |metric| {
10166                // 3 `set` invocations → A=3. The inner `expr` is a
10167                // sub-command (`command_substitution` + `expr_cmd`),
10168                // not a `command` node, so it doesn't add a branch.
10169                assert_eq!(metric.abc.assignments_sum(), 3);
10170                assert_eq!(metric.abc.branches_sum(), 0);
10171                assert_eq!(metric.abc.conditions_sum(), 0);
10172                insta::assert_json_snapshot!(metric.abc);
10173            },
10174        );
10175    }
10176
10177    #[test]
10178    fn tcl_incr_append_lappend_count_assignment() {
10179        // Variable-mutation commands (`incr`, `append`, `lappend`)
10180        // are recognised by name and count as assignments, not
10181        // branches.
10182        check_metrics::<TclParser>(
10183            "proc f {} {\n\
10184                 set x 0\n\
10185                 incr x\n\
10186                 append s \"hi\"\n\
10187                 lappend lst 1\n\
10188             }",
10189            "foo.tcl",
10190            |metric| {
10191                // `set` (1) + `incr` (1) + `append` (1) + `lappend`
10192                // (1) → A=4. No branches, no conditions.
10193                assert_eq!(metric.abc.assignments_sum(), 4);
10194                assert_eq!(metric.abc.branches_sum(), 0);
10195                assert_eq!(metric.abc.conditions_sum(), 0);
10196                insta::assert_json_snapshot!(metric.abc);
10197            },
10198        );
10199    }
10200
10201    #[test]
10202    fn tcl_computed_command_name_is_not_an_assignment() {
10203        // A command whose leading word is computed (`$cmd args`) names no
10204        // builtin the parser can resolve, so it stays a branch. Pins the
10205        // field-addressed, `simple_word`-gated read the classifier shares
10206        // with the Cognitive / Cyclomatic detectors (grammar-dispatch §3):
10207        // the earlier `child(0)` byte-slice addressed the slot by position
10208        // and compared whatever literal text sat there, which agrees with
10209        // the gated read on today's grammar only because no mutator name is
10210        // spelled with a leading `$`. A grammar that moved the name out of
10211        // `child(0)`, or a mutator list that grew a computed-looking entry,
10212        // would diverge — this fixture is where that surfaces.
10213        check_metrics::<TclParser>(
10214            "proc f {cmd x} {\n\
10215                 $cmd $x\n\
10216                 incr x\n\
10217             }",
10218            "foo.tcl",
10219            |metric| {
10220                // `incr x` is the only assignment; `$cmd $x` is a branch.
10221                assert_eq!(metric.abc.assignments_sum(), 1);
10222                assert_eq!(metric.abc.branches_sum(), 1);
10223                assert_eq!(metric.abc.conditions_sum(), 0);
10224            },
10225        );
10226    }
10227
10228    #[test]
10229    fn tcl_generic_commands_are_branches() {
10230        // Anything that isn't `set` or a known mutator command
10231        // counts as a branch — including builtins like `puts` and
10232        // `return`.
10233        check_metrics::<TclParser>(
10234            "proc f {} {\n\
10235                 puts \"hello\"\n\
10236                 do_work 1 2\n\
10237                 return 0\n\
10238             }",
10239            "foo.tcl",
10240            |metric| {
10241                // 3 commands, all branches.
10242                assert_eq!(metric.abc.assignments_sum(), 0);
10243                assert_eq!(metric.abc.branches_sum(), 3);
10244                assert_eq!(metric.abc.conditions_sum(), 0);
10245                insta::assert_json_snapshot!(metric.abc);
10246            },
10247        );
10248    }
10249
10250    #[test]
10251    fn tcl_comparisons_count_logical_ops_do_not() {
10252        // `expr` predicates expose comparison / logical tokens at
10253        // the leaf level. Each comparison token contributes one
10254        // condition; `&&` and `||` are NOT counted on their own
10255        // (Fitzpatrick Rule 5; #395) — instead each operand is
10256        // counted as a unary conditional by the walker (Rule 9;
10257        // #403). The two logical lines add 2 walker conditions
10258        // each (variable-substitution operands).
10259        check_metrics::<TclParser>(
10260            "proc f {a b} {\n\
10261                 set r [expr {$a == $b}]\n\
10262                 set r [expr {$a != $b}]\n\
10263                 set r [expr {$a <  $b}]\n\
10264                 set r [expr {$a >  $b}]\n\
10265                 set r [expr {$a <= $b}]\n\
10266                 set r [expr {$a >= $b}]\n\
10267                 set r [expr {$a eq $b}]\n\
10268                 set r [expr {$a ne $b}]\n\
10269                 set r [expr {$a && $b}]\n\
10270                 set r [expr {$a || $b}]\n\
10271             }",
10272            "foo.tcl",
10273            |metric| {
10274                // 10 `set` assignments.
10275                assert_eq!(metric.abc.assignments_sum(), 10);
10276                assert_eq!(metric.abc.branches_sum(), 0);
10277                // 8 comparisons (+8) + 2 logical lines × 2 walker
10278                // operands (+4) = 12.
10279                assert_eq!(metric.abc.conditions_sum(), 12);
10280                insta::assert_json_snapshot!(metric.abc);
10281            },
10282        );
10283    }
10284
10285    #[test]
10286    fn tcl_ternary_counts_condition() {
10287        // The `ternary_expr` node is one condition and its condition
10288        // slot `$a` — a bare truthy test — is another, matching C++'s
10289        // `int r = a ? b : c;` (also 2). The two branch operands are
10290        // unnegated and so contribute nothing (#1180).
10291        check_metrics::<TclParser>(
10292            "proc f {a b c} {\n\
10293                 set r [expr {$a ? $b : $c}]\n\
10294             }",
10295            "foo.tcl",
10296            |metric| {
10297                assert_eq!(metric.abc.assignments_sum(), 1);
10298                assert_eq!(metric.abc.branches_sum(), 0);
10299                assert_eq!(metric.abc.conditions_sum(), 2);
10300                insta::assert_json_snapshot!(metric.abc);
10301            },
10302        );
10303    }
10304
10305    /// The Tcl half of the ternary slot-location guard (#1180).
10306    ///
10307    /// `ternary_expr` exposes no grammar fields, so the slots are found
10308    /// relative to the `?` and `:` tokens. A *parenthesised* condition is
10309    /// the input that discriminates that from a fixed-index reading:
10310    /// `_expr` inlines `( … )` as anonymous children of `ternary_expr`,
10311    /// so `($a) ? !$b : !$c` shifts every operand right by one and
10312    /// `child(0)` / `child(2)` / `child(4)` land on `(`, `)` and `?`.
10313    /// Without this case the whole fixed-index revert passes.
10314    #[test]
10315    fn tcl_parenthesised_ternary_condition_matches_the_bare_form() {
10316        let conditions = |source: &str| {
10317            crate::test_support::metrics_verbatim(
10318                crate::LANG::Tcl,
10319                source.as_bytes(),
10320                crate::MetricsOptions::default(),
10321            )
10322            .abc
10323            .conditions_sum()
10324        };
10325        let bare = conditions("proc f {a b c} {\n set r [expr {$a ? !$b : !$c}]\n}");
10326        assert_eq!(bare, 4, "the bare form is the documented reference value");
10327        assert_eq!(
10328            conditions("proc f {a b c} {\n set r [expr {($a) ? !$b : !$c}]\n}"),
10329            bare,
10330            "parenthesising the condition must not change the count"
10331        );
10332    }
10333
10334    /// A parenthesised operand under `!` counts the same as a bare one.
10335    ///
10336    /// `_expr` inlines `( … )` as anonymous children, so `!($a)` puts
10337    /// `(` where a positional read expects the operand. The walker's
10338    /// negation branch kept a fixed `child(1)` through the first draft of
10339    /// #1180 and scored these 0 while the unparenthesised forms scored 1
10340    /// — an inconsistency the fix itself introduced, since before it
10341    /// neither form counted. Found in review, not by the tests: the
10342    /// parenthesised fixtures added with #1180 covered the ternary
10343    /// *condition* slot only.
10344    #[test]
10345    fn tcl_parenthesised_negated_operands_match_the_bare_form() {
10346        let conditions = |source: &str| {
10347            crate::test_support::metrics_verbatim(
10348                crate::LANG::Tcl,
10349                source.as_bytes(),
10350                crate::MetricsOptions::default(),
10351            )
10352            .abc
10353            .conditions_sum()
10354        };
10355        for (bare, parenthesised) in [
10356            (
10357                "proc f {a} {\n if {!$a} { puts x }\n}",
10358                "proc f {a} {\n if {!($a)} { puts x }\n}",
10359            ),
10360            (
10361                "proc f {a b} {\n if {$a && !$b} { puts x }\n}",
10362                "proc f {a b} {\n if {$a && !($b)} { puts x }\n}",
10363            ),
10364            (
10365                "proc f {a b c} {\n set r [expr {$a ? !$b : !$c}]\n}",
10366                "proc f {a b c} {\n set r [expr {$a ? !($b) : !$c}]\n}",
10367            ),
10368        ] {
10369            let want = conditions(bare);
10370            assert!(want > 0, "the bare form must count something: {bare}");
10371            assert_eq!(
10372                conditions(parenthesised),
10373                want,
10374                "parenthesising the negated operand changed the count\n  bare:  {bare}\n  paren: {parenthesised}"
10375            );
10376        }
10377    }
10378
10379    #[test]
10380    fn irules_abc_parenthesised_negated_operands_match_the_bare_form() {
10381        let conditions = |source: &str| {
10382            crate::test_support::metrics_verbatim(
10383                crate::LANG::Irules,
10384                source.as_bytes(),
10385                crate::MetricsOptions::default(),
10386            )
10387            .abc
10388            .conditions_sum()
10389        };
10390        let want = conditions("when X {\n    if { !$a } { log local0. hi }\n}\n");
10391        assert_eq!(want, 1);
10392        assert_eq!(
10393            conditions("when X {\n    if { !($a) } { log local0. hi }\n}\n"),
10394            want
10395        );
10396    }
10397
10398    #[test]
10399    fn tcl_bare_truthy_and_negated_predicates_count_one_condition() {
10400        // The headline #1180 fix, on the Tcl side: both were 0 before.
10401        let conditions = |source: &str| {
10402            crate::test_support::metrics_verbatim(
10403                crate::LANG::Tcl,
10404                source.as_bytes(),
10405                crate::MetricsOptions::default(),
10406            )
10407            .abc
10408            .conditions_sum()
10409        };
10410        assert_eq!(conditions("proc f {a} {\n if {$a} { puts x }\n}"), 1);
10411        assert_eq!(conditions("proc f {a} {\n if {!$a} { puts x }\n}"), 1);
10412        assert_eq!(conditions("proc f {a} {\n while {$a} { puts x }\n}"), 1);
10413        assert_eq!(conditions("proc f {a} {\n while {!$a} { puts x }\n}"), 1);
10414    }
10415
10416    #[test]
10417    fn tcl_bare_truthy_elseif_predicate_counts_one_condition() {
10418        // The `Tcl::Elseif` arm routes its predicate through
10419        // `tcl_condition_expr` exactly as `If` / `While` do (#1180), but
10420        // a comparison predicate is counted by its leaf operator, so it
10421        // cannot tell the routing from its absence. Only a bare truthy
10422        // predicate can: `$b` scores through the routed walker or not at
10423        // all.
10424        let conditions = |source: &str| {
10425            crate::test_support::metrics_verbatim(
10426                crate::LANG::Tcl,
10427                source.as_bytes(),
10428                crate::MetricsOptions::default(),
10429            )
10430            .abc
10431            .conditions_sum()
10432        };
10433        // `$a` truthy (1) + `elseif` clause (1) + `$b` truthy (1).
10434        assert_eq!(
10435            conditions("proc f {a b} {\n if {$a} { puts x } elseif {$b} { puts y }\n}"),
10436            3
10437        );
10438    }
10439
10440    #[test]
10441    fn tcl_elseif_and_else_count_conditions() {
10442        // `if` / `elseif` / `else` clause productions each
10443        // contribute one condition. The leaf comparison inside the
10444        // predicate is counted independently.
10445        check_metrics::<TclParser>(
10446            "proc f {x} {\n\
10447                 if {$x > 0} {\n\
10448                     return 1\n\
10449                 } elseif {$x < 0} {\n\
10450                     return -1\n\
10451                 } else {\n\
10452                     return 0\n\
10453                 }\n\
10454             }",
10455            "foo.tcl",
10456            |metric| {
10457                // Branches: three `return` commands → 3.
10458                // Conditions: `>` (1), `<` (1), `elseif` (1), `else`
10459                // (1) → 4.
10460                assert_eq!(metric.abc.assignments_sum(), 0);
10461                assert_eq!(metric.abc.branches_sum(), 3);
10462                assert_eq!(metric.abc.conditions_sum(), 4);
10463                insta::assert_json_snapshot!(metric.abc);
10464            },
10465        );
10466    }
10467
10468    #[test]
10469    fn tcl_if_multiple_conditions() {
10470        // Fitzpatrick Rule 9 walker (issue #403). Tcl's `expr` slot
10471        // exposes `&&` / `||` operands as variable substitutions
10472        // (`$a`, `$b`, …) inside a `binop_expr`.
10473        check_metrics::<TclParser>(
10474            "proc f {a b c d} {\n\
10475                 if {[expr {$a || $b || $c || $d}]} { return 1 }    \n\
10476                 if {[expr {$a && $b && $c}]} { return 2 }          \n\
10477                 return 0\n\
10478             }",
10479            "foo.tcl",
10480            |metric| {
10481                // The two chains feed the walker: 4 + 3 = 7. Each `if`
10482                // predicate is additionally a bare truthy test of a
10483                // command substitution — `{[expr {…}]}` is structurally
10484                // `if {[somecmd]}`, which counts 1 exactly as `if {$a}`
10485                // does — so 7 + 2 = 9 (#1180). Written without the
10486                // redundant `[expr …]` wrapper, `if {$a || $b}` scores
10487                // 2, matching C++'s `if (a || b)`.
10488                assert_eq!(metric.abc.conditions_sum(), 9);
10489                insta::assert_json_snapshot!(metric.abc);
10490            },
10491        );
10492    }
10493
10494    #[test]
10495    fn tcl_while_conditions() {
10496        // Tcl has no `do { ... } while(cond);` — `while {…} {…}` is
10497        // the standard loop. The walker fires on `&&` / `||` tokens
10498        // inside the `expr` predicate.
10499        check_metrics::<TclParser>(
10500            "proc f {a b} {\n\
10501                 while {[expr {$a || $b}]} { break }    \n\
10502                 while {[expr {$a && $b}]} { break }    \n\
10503             }",
10504            "foo.tcl",
10505            |metric| {
10506                // 2 + 2 from the chains, plus one bare truthy test per
10507                // `while` predicate — see `tcl_if_multiple_conditions`
10508                // (#1180).
10509                assert_eq!(metric.abc.conditions_sum(), 6);
10510                insta::assert_json_snapshot!(metric.abc);
10511            },
10512        );
10513    }
10514
10515    #[test]
10516    fn tcl_short_circuit_with_boolean_literal_operand() {
10517        // `$a && 1` reports 2 conditions: a VariableSubstitution
10518        // operand plus a Number-literal operand. Confirms `Number`
10519        // is in the walker terminal set. `true` / `false` Tcl
10520        // keywords are not literal tokens in tree-sitter-tcl —
10521        // they're emitted as the operator-context word, which is
10522        // captured separately by the `Tcl::Boolean` kind for
10523        // dedicated `expr {true}` predicates but not as a `&&`
10524        // operand at this iteration; using a numeric literal keeps
10525        // the assertion grammar-stable.
10526        check_metrics::<TclParser>(
10527            "proc f {a} { return [expr {$a && 1}] }\n",
10528            "foo.tcl",
10529            |metric| {
10530                assert_eq!(metric.abc.conditions_sum(), 2);
10531                insta::assert_json_snapshot!(metric.abc);
10532            },
10533        );
10534    }
10535
10536    #[test]
10537    fn tcl_complex_function_abc() {
10538        // Mixed program covering every category. Tcl's grammar
10539        // re-parses braced content that looks command-shaped as a
10540        // nested `command` node, which inflates the branch count
10541        // relative to a naive read of the source — see breakdown.
10542        check_metrics::<TclParser>(
10543            "proc run {n} {\n\
10544                 set total 0\n\
10545                 for {set i 0} {$i < $n} {incr i} {\n\
10546                     if {$i % 2 == 0} {\n\
10547                         do_work $i\n\
10548                     } else {\n\
10549                         incr total $i\n\
10550                     }\n\
10551                 }\n\
10552                 puts \"done\"\n\
10553                 return $total\n\
10554             }",
10555            "foo.tcl",
10556            |metric| {
10557                // Assignments: `set total 0` (1), `set i 0` (1),
10558                // `incr i` (1), `incr total $i` (1) → A = 4.
10559                // Branches: the outer `for …` is one `command` node;
10560                // the `{$i < $n}` predicate ALSO re-parses as a
10561                // `command` node (tree-sitter-tcl treats braced
10562                // predicates as nested commands at the pinned
10563                // grammar version); plus `do_work $i`, `puts
10564                // "done"`, and `return $total`. The for-loop body's
10565                // `incr` and `incr total $i` are assignment commands
10566                // and don't add branches. Total B = 5.
10567                // Conditions: `==` (1) and `else` (1) → C = 2. The
10568                // `<` inside `{$i < $n}` is NOT `Tcl::LT`: because
10569                // that predicate re-parses as a `command`, the `<`
10570                // is emitted as `simple_word`. Only `<` inside a
10571                // real `expr` production becomes `Tcl::LT`.
10572                assert_eq!(metric.abc.assignments_sum(), 4);
10573                assert_eq!(metric.abc.branches_sum(), 5);
10574                assert_eq!(metric.abc.conditions_sum(), 2);
10575                insta::assert_json_snapshot!(metric.abc);
10576            },
10577        );
10578    }
10579
10580    /// The dedicated `set name value` production counts as one assignment.
10581    #[test]
10582    fn irules_abc_set_assignment() {
10583        check_metrics::<IrulesParser>("when X {\n    set x 1\n}\n", "foo.irule", |metric| {
10584            assert_eq!(metric.abc.assignments_sum(), 1);
10585            assert_eq!(metric.abc.branches_sum(), 0);
10586            assert_eq!(metric.abc.conditions_sum(), 0);
10587        });
10588    }
10589
10590    /// Mutator commands (`incr` / `append` / `lappend`) count as
10591    /// assignments, not branches — iRules has no assignment operators, so
10592    /// mutation is always a command invocation.
10593    #[test]
10594    fn irules_abc_mutator_commands() {
10595        check_metrics::<IrulesParser>(
10596            "when X {\n    incr x\n    append s \"y\"\n    lappend l 1\n}\n",
10597            "foo.irule",
10598            |metric| {
10599                assert_eq!(metric.abc.assignments_sum(), 3);
10600                assert_eq!(metric.abc.branches_sum(), 0);
10601            },
10602        );
10603    }
10604
10605    /// Generic (non-mutator) commands count as branches.
10606    #[test]
10607    fn irules_abc_branch_commands() {
10608        check_metrics::<IrulesParser>(
10609            "when X {\n    log local0. hi\n    pool p1\n}\n",
10610            "foo.irule",
10611            |metric| {
10612                assert_eq!(metric.abc.assignments_sum(), 0);
10613                assert_eq!(metric.abc.branches_sum(), 2);
10614                assert_eq!(metric.abc.conditions_sum(), 0);
10615            },
10616        );
10617    }
10618
10619    /// A numeric comparison (`==`) is one condition; the `log` inside the
10620    /// `if` body is one branch.
10621    #[test]
10622    fn irules_abc_comparison_condition() {
10623        check_metrics::<IrulesParser>(
10624            "when X {\n    if { $a == 1 } { log local0. hi }\n}\n",
10625            "foo.irule",
10626            |metric| {
10627                assert_eq!(metric.abc.branches_sum(), 1);
10628                assert_eq!(metric.abc.conditions_sum(), 1);
10629            },
10630        );
10631    }
10632
10633    /// A word-form string comparator (`contains`) is a condition just like
10634    /// `==` — iRules-specific (Tcl has only `eq`/`ne`/`in`/`ni`). If
10635    /// `contains` were dropped from the condition set this would report 0.
10636    #[test]
10637    fn irules_abc_string_op_condition() {
10638        check_metrics::<IrulesParser>(
10639            "when X {\n    if { $a contains \"x\" } { log local0. hi }\n}\n",
10640            "foo.irule",
10641            |metric| {
10642                assert_eq!(metric.abc.branches_sum(), 1);
10643                assert_eq!(metric.abc.conditions_sum(), 1);
10644            },
10645        );
10646    }
10647
10648    /// Each `elseif` / `else` clause is one condition; the three `set`s are
10649    /// assignments. The leading `if` is not itself a condition.
10650    #[test]
10651    fn irules_abc_elseif_else_conditions() {
10652        check_metrics::<IrulesParser>(
10653            "when X {\n    if { $a } { set r 1 } elseif { $b } { set r 2 } else { set r 3 }\n}\n",
10654            "foo.irule",
10655            |metric| {
10656                assert_eq!(metric.abc.assignments_sum(), 3);
10657                // The `elseif` and `else` clauses are one condition each,
10658                // as before; #1180 adds the two bare truthy predicates
10659                // (`{ $a }`, `{ $b }`). C++'s
10660                // `if(a){} else if(b){} else {}` also scores 4.
10661                assert_eq!(metric.abc.conditions_sum(), 4);
10662            },
10663        );
10664    }
10665
10666    /// A ternary contributes its own condition plus the `>` comparison in
10667    /// its test: conditions 2; the `set` is one assignment.
10668    #[test]
10669    fn irules_abc_ternary_condition() {
10670        check_metrics::<IrulesParser>(
10671            "when X {\n    set y [expr { $a > 0 ? 1 : 0 }]\n}\n",
10672            "foo.irule",
10673            |metric| {
10674                assert_eq!(metric.abc.assignments_sum(), 1);
10675                assert_eq!(metric.abc.conditions_sum(), 2);
10676            },
10677        );
10678    }
10679
10680    /// Fitzpatrick Rule 9: the short-circuit `&&` is not itself a condition,
10681    /// but each negated bare operand (`!$a`, `!$b`) in the chain is. Guards
10682    /// the `irules_count_unary_conditions` / `irules_inspect_container`
10683    /// walker — conditions 2.
10684    #[test]
10685    fn irules_abc_negated_operands_in_chain() {
10686        check_metrics::<IrulesParser>(
10687            "when X {\n    if { !$a && !$b } { log local0. hi }\n}\n",
10688            "foo.irule",
10689            |metric| {
10690                assert_eq!(metric.abc.branches_sum(), 1);
10691                assert_eq!(metric.abc.conditions_sum(), 2);
10692            },
10693        );
10694    }
10695
10696    /// A bare-truthy `if {$a}` predicate is one condition (#1180).
10697    ///
10698    /// It carries no comparison, ternary or short-circuit operator, so
10699    /// before the Phase 2B slot routing landed nothing invoked the
10700    /// unary-conditional walker and the whole predicate scored 0 — this
10701    /// test previously pinned that absence, and the metrics book's ABC
10702    /// deviation table said so. Now the `if` node routes its `expr`
10703    /// predicate and the count matches C++'s `if (a)`, which is also 1.
10704    /// The `log` command remains the single branch.
10705    #[test]
10706    fn irules_abc_bare_truthy_counts_one_condition() {
10707        check_metrics::<IrulesParser>(
10708            "when X {\n    if { $a } { log local0. hi }\n}\n",
10709            "foo.irule",
10710            |metric| {
10711                assert_eq!(metric.abc.branches_sum(), 1);
10712                assert_eq!(metric.abc.conditions_sum(), 1);
10713            },
10714        );
10715    }
10716
10717    /// The negated form of the same predicate, which was *also* 0 before
10718    /// #1180: `!$a` reached the walker but no parent seeded boolean
10719    /// context, so the terminal operand was never counted. Distinct from
10720    /// `irules_abc_negated_operands_in_chain`, whose `&&` supplied the
10721    /// seed the bare form lacked.
10722    #[test]
10723    fn irules_abc_negated_bare_truthy_counts_one_condition() {
10724        check_metrics::<IrulesParser>(
10725            "when X {\n    if { !$a } { log local0. hi }\n}\n",
10726            "foo.irule",
10727            |metric| {
10728                assert_eq!(metric.abc.branches_sum(), 1);
10729                assert_eq!(metric.abc.conditions_sum(), 1);
10730            },
10731        );
10732    }
10733
10734    /// The ternary's three operand slots, located relative to the `?`
10735    /// and `:` tokens because the grammar exposes no fields (#1180).
10736    ///
10737    /// `$a ? !$b : !$c` is four: the `ternary_expr` node, the bare
10738    /// truthy condition, and one per negated branch — the same value
10739    /// Java, C#, Groovy, the C family, the JS family, PHP, Perl, Ruby
10740    /// and Python report for the identical expression.
10741    #[test]
10742    fn irules_abc_ternary_routes_its_operand_slots() {
10743        check_metrics::<IrulesParser>(
10744            "when X {\n    set y [expr { $a ? !$b : !$c }]\n}\n",
10745            "foo.irule",
10746            |metric| {
10747                assert_eq!(metric.abc.conditions_sum(), 4);
10748            },
10749        );
10750    }
10751
10752    /// The #1161 control: a ternary whose condition is a *comparison*
10753    /// must not move. The `>` already supplied its condition and the
10754    /// branches are unnegated, so routing the slots adds nothing.
10755    #[test]
10756    fn irules_abc_comparison_ternary_is_unchanged_by_slot_routing() {
10757        check_metrics::<IrulesParser>(
10758            "when X {\n    set y [expr { $a > 0 ? 1 : 0 }]\n}\n",
10759            "foo.irule",
10760            |metric| {
10761                assert_eq!(metric.abc.conditions_sum(), 2);
10762            },
10763        );
10764    }
10765
10766    /// A parenthesised ternary condition scores the same as a bare one.
10767    ///
10768    /// The grammar inlines `( … )` as anonymous children of
10769    /// `ternary_expr` rather than wrapping them in a node, so a
10770    /// fixed-index reading of the slots would shift right by one and
10771    /// mis-assign every operand. This is the input that discriminates
10772    /// the token-relative location the fix uses.
10773    #[test]
10774    fn irules_abc_parenthesised_ternary_condition_matches_the_bare_form() {
10775        // `check_metrics` takes a bare `fn`, so it cannot carry the
10776        // first measurement into the second comparison; `metrics_verbatim`
10777        // returns a value instead.
10778        let conditions = |source: &str| {
10779            crate::test_support::metrics_verbatim(
10780                crate::LANG::Irules,
10781                source.as_bytes(),
10782                crate::MetricsOptions::default(),
10783            )
10784            .abc
10785            .conditions_sum()
10786        };
10787        let bare = conditions("when X {\n    set y [expr { $a ? !$b : !$c }]\n}\n");
10788        assert_eq!(bare, 4, "the bare form is the documented reference value");
10789        assert_eq!(
10790            conditions("when X {\n    set y [expr { ($a) ? !$b : !$c }]\n}\n"),
10791            bare,
10792            "parenthesising the condition must not change the count"
10793        );
10794    }
10795}
10796
10797/// A comment inside a ternary must not change its ABC conditions
10798/// (#1181).
10799///
10800/// Two opposite defects, one cause: tree-sitter counts a comment among a
10801/// node's children, so it is the operand's previous sibling *and* it
10802/// shifts every positional index.
10803///
10804/// * Languages whose seed asked "is my previous sibling `?` or `:`"
10805///   (C family, PHP, Perl, JS family) read the comment as "not a
10806///   ternary token", flipped the boolean-context seed on for a *branch*
10807///   slot, and **over**-counted: `a ? /*n*/ (b) : c` scored 3 where
10808///   `a ? (b) : c` scores 2.
10809/// * Languages whose branch walk read `child(2)` / `child(4)` (Java,
10810///   C#, Groovy) landed on the comment instead of the operand, never
10811///   inspected it, and **under**-counted: `a ? /*n*/ !b : c` scored 2
10812///   where `a ? !b : c` scores 3.
10813///
10814/// Both slots are now addressed by grammar field. The parenthesised
10815/// operand is the only input that discriminates the first defect and
10816/// the negated operand the only one that discriminates the second —
10817/// existing ternary fixtures use neither.
10818#[cfg(test)]
10819mod ternary_comment_invariance {
10820    use crate::test_support::metrics_verbatim;
10821    use crate::{LANG, MetricsOptions};
10822
10823    fn conditions(lang: LANG, source: &str) -> u64 {
10824        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
10825            .abc
10826            .conditions_sum()
10827    }
10828
10829    /// `(base, with_comment)` for a parenthesised and a negated branch
10830    /// operand, per language.
10831    fn cases(lang: LANG) -> Option<[(String, String); 2]> {
10832        // `{}` marks the consequence slot; `/*n*/` the inserted comment.
10833        let (template, paren, negated, comment) = match lang {
10834            LANG::Cpp | LANG::C | LANG::Objc | LANG::Mozcpp => {
10835                ("int f(){ int x = a ? {} : c; }", "(b)", "!b", "/*n*/ ")
10836            }
10837            LANG::Java => (
10838                "class K{ void f(){ int x = a ? {} : c; } }",
10839                "(b)",
10840                "!b",
10841                "/*n*/ ",
10842            ),
10843            LANG::Csharp => (
10844                "class K{ void f(){ var x = a ? {} : c; } }",
10845                "(b)",
10846                "!b",
10847                "/*n*/ ",
10848            ),
10849            LANG::Groovy => ("def f(){ def x = a ? {} : c }", "(b)", "!b", "/*n*/ "),
10850            LANG::Javascript | LANG::Typescript | LANG::Tsx | LANG::Mozjs => {
10851                ("function f(){ var x = a ? {} : c; }", "(b)", "!b", "/*n*/ ")
10852            }
10853            LANG::Php => (
10854                "<?php function f(){ $x = $a ? {} : $c; }",
10855                "($b)",
10856                "!$b",
10857                "/*n*/ ",
10858            ),
10859            // Perl has no block comment: `#` runs to end of line, so the
10860            // comment must carry its own newline.
10861            LANG::Perl => ("sub f { my $x = $a ? {} : $c; }", "($b)", "!$b", "# n\n "),
10862            _ => return None,
10863        };
10864        let build = |operand: &str, with_comment: bool| {
10865            let slot = if with_comment {
10866                format!("{comment}{operand}")
10867            } else {
10868                operand.to_owned()
10869            };
10870            template.replace("{}", &slot)
10871        };
10872        Some([
10873            (build(paren, false), build(paren, true)),
10874            (build(negated, false), build(negated, true)),
10875        ])
10876    }
10877
10878    #[test]
10879    fn a_comment_before_a_branch_operand_changes_nothing() {
10880        let mut checked = 0;
10881        for lang in LANG::into_enum_iter() {
10882            if !lang.is_enabled() {
10883                continue;
10884            }
10885            let Some(pairs) = cases(lang) else { continue };
10886            checked += 1;
10887            for (base, commented) in pairs {
10888                assert_eq!(
10889                    conditions(lang, &commented),
10890                    conditions(lang, &base),
10891                    "{lang:?}: a comment changed the ABC conditions of a ternary\n  \
10892                     without: {base}\n  with:    {commented}"
10893                );
10894            }
10895        }
10896        assert!(
10897            checked > 0,
10898            "no ternary language enabled; this test asserted nothing"
10899        );
10900    }
10901
10902    /// The absolute values the invariance test compares against, so a
10903    /// regression that moved *both* sides equally still fails.
10904    ///
10905    /// expected: `a ? (b) : c` counts the `?` marker plus the condition
10906    /// `a` in boolean context = 2. Negating the consequence adds one
10907    /// more, since `!b` establishes boolean content for that slot = 3.
10908    #[test]
10909    fn the_baseline_values_are_two_and_three() {
10910        let mut checked = 0;
10911        for lang in LANG::into_enum_iter() {
10912            if !lang.is_enabled() {
10913                continue;
10914            }
10915            let Some([(paren, _), (negated, _)]) = cases(lang) else {
10916                continue;
10917            };
10918            assert_eq!(
10919                conditions(lang, &paren),
10920                2,
10921                "{lang:?}: parenthesised branch"
10922            );
10923            assert_eq!(conditions(lang, &negated), 3, "{lang:?}: negated branch");
10924            checked += 1;
10925        }
10926        assert!(
10927            checked > 0,
10928            "no ternary language enabled; this test asserted nothing"
10929        );
10930    }
10931}
10932
10933/// A keyword negation must score like its symbolic twin (#1182).
10934///
10935/// `not b` and `!b` are the same negation — they differ in precedence,
10936/// not in meaning, and ABC counts the negation rather than the parse.
10937/// Ruby and Perl tested only the `!` token, so `if not b` scored 0
10938/// against `if !b`'s 1, and a `not` ternary scored 2 against the `!`
10939/// form's 4.
10940///
10941/// Lua and Elixir were checked in the same sweep and were already
10942/// correct: Lua's only negation keyword *is* `not` and it was the token
10943/// being tested, and Elixir reaches the same count by another path.
10944/// They are exercised here so a future edit cannot regress them
10945/// silently. Python counts `not` through its own dispatcher arm and has
10946/// no `!` spelling to compare against.
10947#[cfg(test)]
10948mod keyword_negation_parity {
10949    use crate::test_support::metrics_verbatim;
10950    use crate::{LANG, MetricsOptions};
10951
10952    fn conditions(lang: LANG, source: &str) -> u64 {
10953        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
10954            .abc
10955            .conditions_sum()
10956    }
10957
10958    /// `(bang_form, keyword_form)` pairs that must score identically.
10959    fn pairs(lang: LANG) -> Option<Vec<(String, String)>> {
10960        let build =
10961            |t: &str| -> (String, String) { (t.replace("{NOT}", "!"), t.replace("{NOT}", "not ")) };
10962        let templates: &[&str] = match lang {
10963            LANG::Ruby => &[
10964                "def f(b)\n  if {NOT}b\n    1\n  end\nend\n",
10965                "def f(a, b, c)\n  x = a ? ({NOT}b) : ({NOT}c)\nend\n",
10966                "def f(a, b, c)\n  x = a ? b : ({NOT}c)\nend\n",
10967            ],
10968            LANG::Perl => &[
10969                "sub f { if ({NOT}$b) { 1; } }",
10970                "sub f { my $x = $a ? ({NOT}$b) : ({NOT}$c); }",
10971                "sub f { my $x = $a ? $b : ({NOT}$c); }",
10972            ],
10973            // Already correct before #1182; pinned so they stay that way.
10974            LANG::Elixir => &["def f(b) do\n  if {NOT}b do\n    1\n  end\nend\n"],
10975            _ => return None,
10976        };
10977        Some(templates.iter().map(|t| build(t)).collect())
10978    }
10979
10980    #[test]
10981    fn the_not_keyword_scores_like_bang() {
10982        let mut checked = 0;
10983        for lang in LANG::into_enum_iter() {
10984            if !lang.is_enabled() {
10985                continue;
10986            }
10987            let Some(pairs) = pairs(lang) else { continue };
10988            checked += 1;
10989            for (bang, keyword) in pairs {
10990                assert_eq!(
10991                    conditions(lang, &keyword),
10992                    conditions(lang, &bang),
10993                    "{lang:?}: `not` and `!` scored differently\n  bang:    {bang}\n  keyword: {keyword}"
10994                );
10995            }
10996        }
10997        assert!(
10998            checked > 0,
10999            "no language enabled; this test asserted nothing"
11000        );
11001    }
11002
11003    /// The absolute values, so a regression that moved both spellings
11004    /// equally still fails.
11005    ///
11006    /// expected: `if !b` is one condition — the negated bare operand.
11007    /// `a ? (!b) : (!c)` is four: the `?` marker, the condition `a` in
11008    /// boolean context, and one per negated branch operand.
11009    #[test]
11010    fn the_baseline_values_are_one_and_four() {
11011        let mut checked = 0;
11012        for (lang, guard, ternary) in [
11013            (
11014                LANG::Ruby,
11015                "def f(b)\n  if not b\n    1\n  end\nend\n",
11016                "def f(a, b, c)\n  x = a ? (not b) : (not c)\nend\n",
11017            ),
11018            (
11019                LANG::Perl,
11020                "sub f { if (not $b) { 1; } }",
11021                "sub f { my $x = $a ? (not $b) : (not $c); }",
11022            ),
11023        ] {
11024            if !lang.is_enabled() {
11025                continue;
11026            }
11027            assert_eq!(conditions(lang, guard), 1, "{lang:?}: `if not b`");
11028            assert_eq!(conditions(lang, ternary), 4, "{lang:?}: `not` ternary");
11029            checked += 1;
11030        }
11031        assert!(
11032            checked > 0,
11033            "no language enabled; this test asserted nothing"
11034        );
11035    }
11036
11037    /// Lua's only negation keyword is `not`, so it has no `!` twin to
11038    /// compare against — its guard is that the keyword counts at all.
11039    #[test]
11040    fn lua_counts_its_only_negation_keyword() {
11041        if !LANG::Lua.is_enabled() {
11042            return;
11043        }
11044        assert_eq!(
11045            conditions(
11046                LANG::Lua,
11047                "function f(b)\n  if not b then return 1 end\nend\n"
11048            ),
11049            1
11050        );
11051    }
11052}