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;
29use crate::metrics::ConstraintReach;
30
31use crate::*;
32
33mod bash;
34mod c;
35mod cpp;
36mod csharp;
37mod elixir;
38mod go;
39mod groovy;
40mod irules;
41mod java;
42mod js_family;
43mod kotlin;
44mod lua;
45mod mozcpp;
46mod objc;
47mod perl;
48mod php;
49mod python;
50mod ruby;
51mod rust;
52#[cfg(test)]
53mod slot_pays_tests;
54mod tcl;
55
56/// The `ABC` metric.
57///
58/// The `ABC` metric measures the size of a source code by counting
59/// the number of Assignments (`A`), Branches (`B`) and Conditions (`C`).
60/// The metric defines an ABC score as a vector of three elements (`<A,B,C>`).
61/// The ABC score can be represented by its individual components (`A`, `B` and `C`)
62/// or by the magnitude of the vector (`|<A,B,C>| = sqrt(A^2 + B^2 + C^2)`).
63///
64/// Official paper and definition:
65///
66/// Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report.
67///
68/// <https://www.softwarerenovation.com/Articles.aspx>
69///
70/// # Cross-language `&&` / `||` policy
71///
72/// Per Fitzpatrick's conditional-operator rule (Rule 5 in Figure 2
73/// for C and Figure 4 for Java; Rule 7 in Figure 3 for C++), only
74/// comparison operators (`==`, `!=`, `<=`, `>=`, `<`, `>`) and a
75/// paper-defined keyword set (`else`, `case`, `default`, `?`, plus
76/// `try` / `catch` for C++ and Java) contribute to the condition
77/// count. Per-language `impl Abc` blocks narrow this set where
78/// appropriate — e.g., C++/Rust/Go/Python exclude `default` since
79/// it falls through unconditionally (matching the Rust `_ =>` and
80/// Java `default:` precedent). The short-
81/// circuit logical operators `&&` and `||` (and per-language
82/// equivalents — Python's `and` / `or`, Lua's `and` / `or`, Tcl's
83/// `&&` / `||`, Perl's `&&` / `||` / `//` / `and` / `or`)
84/// are deliberately **not** counted on their own. The paper's
85/// worked Listing 2 annotates `(am >= 0 && am <= 0xF) ? '/' : 'C'`
86/// as `accc` — three conditions for `>=`, `<=`, `?`, zero for
87/// `&&`.
88///
89/// Fitzpatrick's Rule 7 (Figure 3, C++) / Rule 9 (Figure 4, Java) —
90/// "Add one to the condition count for each unary conditional
91/// expression" — instead counts each non-comparison operand of a
92/// `&&` / `||` chain once. The paper's worked example for this
93/// rule is `if (x || y) printf("test failure\n");`, annotated:
94/// "there are two unary conditions since both `x` and `y` are
95/// tested as conditional expressions" (so `||` contributes zero,
96/// `x` contributes one, `y` contributes one, and `printf(...)`
97/// contributes one branch). The walker machinery for this —
98/// modelled on the former `java_count_unary_conditions` /
99/// `java_inspect_container` — is present today for Java, Groovy,
100/// C#, Rust, Go, JavaScript, TypeScript, TSX, Mozjs, PHP, C++,
101/// Python, Perl, Lua, Tcl, iRules, Kotlin, Ruby, and Elixir. So
102/// `if (a && b)` reports 2 conditions across this set, matching
103/// the paper. Bash is the lone exception: its `&&` / `||` are
104/// command-list separators rather than boolean-expression operands
105/// with named leaf operands, so Fitzpatrick's Rule 9 does not map
106/// onto its grammar and the walker is deliberately not wired.
107///
108/// This policy is paper-faithful and deviates from RuboCop's
109/// `Metrics/AbcSize` (which counts `and` / `or` as conditions
110/// directly) while matching `StepicOrg/abcmeter` and
111/// `eoinnoble/python-abc`. The book's *ABC counting rules*
112/// section reproduces the rule tables, a per-language deviation
113/// table, and worked examples — see the chapter at
114/// <https://dekobon.github.io/big-code-analysis/metrics.html#abc>.
115///
116/// # Cross-language empty-`for`-condition policy
117///
118/// `for (;;)` — and every other spelling that omits the test slot
119/// (`for (init; ; update)`, Go's bare `for {}`) — counts **zero**
120/// conditions. Nothing in Fitzpatrick's condition rules attributes a
121/// count to a `for` keyword: they count conditional operators and
122/// unary conditions that are *present*, and an omitted test is not a
123/// decision. Most languages get this for free: `*_walk_for_statement`
124/// asks `for_statement` for its `condition` field and an empty header
125/// has none. Two do not, and neither needs a special case either —
126/// the JS family fills the slot with an `empty_statement`, which is
127/// not a boolean terminal and not a paren / `!` wrapper, so it falls
128/// through; and Go, whose `for_statement` exposes no `condition` field
129/// at all, locates the header slot structurally and finds only the
130/// body. Before #1276 Java and Groovy alone disagreed, counting the
131/// `;` or `)` that landed in a positional child slot as a
132/// vacuously-true condition.
133///
134/// See issue #395 for the Phase-1 cross-language policy
135/// alignment, #403 for the Phase-2 unary-conditional walker
136/// fan-out, #404 for the Phase-3 book documentation, #557
137/// for the Kotlin / Ruby / Elixir walker wiring, and #1276 for the
138/// `for`-header condition slot.
139#[derive(Debug, Clone, PartialEq)]
140#[non_exhaustive]
141pub struct Stats {
142    pub(super) assignments: f64,
143    assignments_sum: f64,
144    assignments_min: f64,
145    assignments_max: f64,
146    pub(super) branches: f64,
147    branches_sum: f64,
148    branches_min: f64,
149    branches_max: f64,
150    pub(super) conditions: f64,
151    conditions_sum: f64,
152    conditions_min: f64,
153    conditions_max: f64,
154    space_count: usize,
155    constraint: ConstraintReach,
156}
157
158impl Default for Stats {
159    fn default() -> Self {
160        Self {
161            assignments: 0.,
162            assignments_sum: 0.,
163            assignments_min: f64::MAX,
164            assignments_max: 0.,
165            branches: 0.,
166            branches_sum: 0.,
167            branches_min: f64::MAX,
168            branches_max: 0.,
169            conditions: 0.,
170            conditions_sum: 0.,
171            conditions_min: f64::MAX,
172            conditions_max: 0.,
173            space_count: 1,
174            constraint: ConstraintReach::default(),
175        }
176    }
177}
178
179impl fmt::Display for Stats {
180    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
181        write!(
182            f,
183            "assignments: {}, branches: {}, conditions: {}, magnitude: {}, \
184            assignments_average: {}, branches_average: {}, conditions_average: {}, \
185            assignments_min: {}, assignments_max: {}, \
186            branches_min: {}, branches_max: {}, \
187            conditions_min: {}, conditions_max: {}",
188            self.assignments_sum(),
189            self.branches_sum(),
190            self.conditions_sum(),
191            self.magnitude_sum(),
192            self.assignments_average(),
193            self.branches_average(),
194            self.conditions_average(),
195            self.assignments_min(),
196            self.assignments_max(),
197            self.branches_min(),
198            self.branches_max(),
199            self.conditions_min(),
200            self.conditions_max()
201        )
202    }
203}
204
205impl Stats {
206    /// Merges a second `Abc` metric into the first one.
207    pub fn merge(&mut self, other: &Stats) {
208        // Calculates minimum and maximum values
209        self.assignments_min = self.assignments_min.min(other.assignments_min);
210        self.assignments_max = self.assignments_max.max(other.assignments_max);
211        self.branches_min = self.branches_min.min(other.branches_min);
212        self.branches_max = self.branches_max.max(other.branches_max);
213        self.conditions_min = self.conditions_min.min(other.conditions_min);
214        self.conditions_max = self.conditions_max.max(other.conditions_max);
215
216        self.assignments_sum += other.assignments_sum;
217        self.branches_sum += other.branches_sum;
218        self.conditions_sum += other.conditions_sum;
219
220        self.space_count += other.space_count;
221    }
222
223    /// Returns the `Abc` assignments metric value.
224    #[must_use]
225    pub fn assignments(&self) -> u64 {
226        self.assignments as u64
227    }
228
229    /// Returns the `Abc` assignments sum metric value.
230    #[must_use]
231    pub fn assignments_sum(&self) -> u64 {
232        self.assignments_sum as u64
233    }
234
235    /// Returns the `Abc` assignments average value.
236    ///
237    /// This value is computed dividing the `Abc`
238    /// assignments value for the number of spaces.
239    #[must_use]
240    pub fn assignments_average(&self) -> f64 {
241        crate::metrics::average(self.assignments_sum() as f64, self.space_count)
242    }
243
244    /// Returns the `Abc` assignments minimum value.
245    ///
246    /// Collapses the `f64::MAX` sentinel that `Stats::default()` plants
247    /// into `assignments_min` to `0`, so a never-observed space
248    /// serializes to a meaningful number rather than `1.7976931e308`.
249    #[allow(clippy::float_cmp)]
250    #[must_use]
251    pub fn assignments_min(&self) -> u64 {
252        if self.assignments_min == f64::MAX {
253            0
254        } else {
255            self.assignments_min as u64
256        }
257    }
258
259    /// Returns the `Abc` assignments maximum value.
260    #[must_use]
261    pub fn assignments_max(&self) -> u64 {
262        self.assignments_max as u64
263    }
264
265    /// Returns the `Abc` branches metric value.
266    #[must_use]
267    pub fn branches(&self) -> u64 {
268        self.branches as u64
269    }
270
271    /// Returns the `Abc` branches sum metric value.
272    #[must_use]
273    pub fn branches_sum(&self) -> u64 {
274        self.branches_sum as u64
275    }
276
277    /// Returns the `Abc` branches average value.
278    ///
279    /// This value is computed dividing the `Abc`
280    /// branches value for the number of spaces.
281    #[must_use]
282    pub fn branches_average(&self) -> f64 {
283        crate::metrics::average(self.branches_sum() as f64, self.space_count)
284    }
285
286    /// Returns the `Abc` branches minimum value.
287    ///
288    /// Same `f64::MAX` sentinel collapse as `assignments_min`.
289    #[allow(clippy::float_cmp)]
290    #[must_use]
291    pub fn branches_min(&self) -> u64 {
292        if self.branches_min == f64::MAX {
293            0
294        } else {
295            self.branches_min as u64
296        }
297    }
298
299    /// Returns the `Abc` branches maximum value.
300    #[must_use]
301    pub fn branches_max(&self) -> u64 {
302        self.branches_max as u64
303    }
304
305    /// Returns the `Abc` conditions metric value.
306    #[must_use]
307    pub fn conditions(&self) -> u64 {
308        self.conditions as u64
309    }
310
311    /// Returns the `Abc` conditions sum metric value.
312    #[must_use]
313    pub fn conditions_sum(&self) -> u64 {
314        self.conditions_sum as u64
315    }
316
317    /// Returns the `Abc` conditions average value.
318    ///
319    /// This value is computed dividing the `Abc`
320    /// conditions value for the number of spaces.
321    #[must_use]
322    pub fn conditions_average(&self) -> f64 {
323        crate::metrics::average(self.conditions_sum() as f64, self.space_count)
324    }
325
326    /// Returns the `Abc` conditions minimum value.
327    ///
328    /// Same `f64::MAX` sentinel collapse as `assignments_min`.
329    #[allow(clippy::float_cmp)]
330    #[must_use]
331    pub fn conditions_min(&self) -> u64 {
332        if self.conditions_min == f64::MAX {
333            0
334        } else {
335            self.conditions_min as u64
336        }
337    }
338
339    /// Returns the `Abc` conditions maximum value.
340    #[must_use]
341    pub fn conditions_max(&self) -> u64 {
342        self.conditions_max as u64
343    }
344
345    /// Returns the `Abc` magnitude metric value.
346    #[must_use]
347    pub fn magnitude(&self) -> f64 {
348        (self.assignments.powi(2) + self.branches.powi(2) + self.conditions.powi(2)).sqrt()
349    }
350
351    /// Returns the `Abc` magnitude sum metric value.
352    #[must_use]
353    pub fn magnitude_sum(&self) -> f64 {
354        (self.assignments_sum.powi(2) + self.branches_sum.powi(2) + self.conditions_sum.powi(2))
355            .sqrt()
356    }
357
358    #[inline]
359    pub(crate) fn compute_sum(&mut self) {
360        self.assignments_sum += self.assignments;
361        self.branches_sum += self.branches;
362        self.conditions_sum += self.conditions;
363    }
364
365    #[inline]
366    pub(crate) fn compute_minmax(&mut self) {
367        self.assignments_min = self.assignments_min.min(self.assignments);
368        self.assignments_max = self.assignments_max.max(self.assignments);
369        self.branches_min = self.branches_min.min(self.branches);
370        self.branches_max = self.branches_max.max(self.branches);
371        self.conditions_min = self.conditions_min.min(self.conditions);
372        self.conditions_max = self.conditions_max.max(self.conditions);
373        self.compute_sum();
374    }
375}
376
377#[doc(hidden)]
378/// Per-language computation of the ABC metric.
379pub(crate) trait Abc
380where
381    Self: Checker,
382{
383    /// Walk `node` and update `stats` with this metric for the language
384    /// implementing the trait.
385    ///
386    /// `code` is the source bytes underlying the parsed tree. Most
387    /// languages ignore it: assignments, branches, and conditions all
388    /// surface as distinct grammar productions and a `kind_id()` match
389    /// is enough. Elixir is the exception — `case` / `cond` / `if` /
390    /// `with` / guard `when` arms surface as `Call` nodes whose keyword
391    /// target lives only in the source text. Matching the `Cyclomatic`
392    /// / `Halstead` / `Exit` / `Cognitive` pattern keeps the signature
393    /// uniform.
394    ///
395    /// `ancestors` is the chain the walker descended through. Nearly
396    /// every language classifies some token by what encloses it: `<`
397    /// and `>` are comparisons only under a binary expression (a type
398    /// -argument list otherwise), and `&&` / `||` reach their operands
399    /// through the enclosing chain node. Reaching that parent with
400    /// [`Node::parent`] costs `O(depth)` per node (#1096). The
401    /// condition-slot walkers take *their* parent as an argument
402    /// instead, because the caller descended from it.
403    fn compute<'a>(
404        node: &Node<'a>,
405        code: &'a [u8],
406        ancestors: Ancestors<'a, '_>,
407        stats: &mut Stats,
408    );
409}
410
411// Shared Phase-2B helper (issue #403): walk every named child of an
412// expression-list-style wrapper (Go's `expression_list`, Lua's
413// `expression_list`) and route each through a language-specific
414// classifier. Used for `return value1, value2, ...` arms where the
415// values live one level below the return statement under a list
416// wrapper. The classifier receives only named children so that
417// `,` / `;` / `(` / `)` tokens never reach it, plus `list` itself as
418// the child's parent — the container classifiers seed their
419// boolean-context flag from the parent kind, and reaching it with
420// `Node::parent` would cost `O(depth)` per node (#1096).
421pub(super) fn for_each_named_child(
422    list: &Node,
423    conditions: &mut f64,
424    f: fn(&Node, &Node, &mut f64),
425) {
426    let mut cursor = list.cursor();
427    if cursor.goto_first_child() {
428        loop {
429            let child = cursor.node();
430            if child.is_named() {
431                f(&child, list, conditions);
432            }
433            if !cursor.goto_next_sibling() {
434                break;
435            }
436        }
437    }
438}
439
440// The operand a single-operand wrapper holds — `(…)`, a negation, a
441// `return` — read as its first named child that is not a tree-sitter
442// `extra`. A positional `child(1)` read assumes nothing sits between the
443// opening token and the operand, and a comment can sit anywhere: `(/*c*/
444// b)` handed the comment to the peel, which declined it, and the
445// condition scored zero (#1455). `is_named` alone is not enough either,
446// because extras are named.
447pub(super) fn wrapped_operand<'a>(node: &Node<'a>) -> Option<Node<'a>> {
448    node.children().find(is_operand)
449}
450
451// The last operand of `node`, by the same rule as `wrapped_operand`: for
452// a wrapper that evaluates to its final element (a Perl list in scalar
453// context, a Ruby statement sequence) and for a slot that follows an
454// unnamed one (a C# declarator's initialiser after its name).
455pub(super) fn last_operand<'a>(node: &Node<'a>) -> Option<Node<'a>> {
456    node.children().filter(is_operand).last()
457}
458
459// Whether `child` can occupy an operand slot: named, and not an `extra`.
460// The `extra` flag is read off the tree-sitter node because the `Node`
461// wrapper exposes no accessor for it.
462pub(super) fn is_operand(child: &Node) -> bool {
463    child.is_named() && !child.as_tree_sitter().is_extra()
464}
465
466// One step of a language's value peel: the operand a wrapper evaluates
467// to — `(…)`, a negation, an assignment's value, a type cast — and
468// whether that wrapper itself proves the operand boolean (only a
469// negation does). `None` for anything that is not a wrapper.
470pub(super) type PeelStep<'a> = fn(&Node<'a>) -> Option<(Node<'a>, bool)>;
471
472// Every wrapper `step` descends, peeled: the operand they evaluate to,
473// and whether any layer proved it boolean.
474pub(super) fn peel<'a>(node: &Node<'a>, step: PeelStep<'a>) -> (Node<'a>, bool) {
475    let mut node = *node;
476    let mut proves_boolean = false;
477    while let Some((operand, proves)) = step(&node) {
478        proves_boolean |= proves;
479        node = operand;
480    }
481    (node, proves_boolean)
482}
483
484// Scores one boolean slot — an `if` / `while` predicate, a ternary
485// condition, a guard, an operand of an `&&` / `||` chain — as
486// Fitzpatrick's "unary conditional expression" (Rule 6 / 7 / 9): one
487// condition, unless another arm already charged the same decision.
488// `scores_itself` says whether the peeled predicate is a comparison,
489// chain, ternary or other construct some arm of the language's
490// `compute` already counts, and must agree with those arms exactly: a
491// kind listed here and counted nowhere leaves its slot at zero, and one
492// counted there but missing here pays twice.
493//
494// The slots once paid only for an occupant that peeled down to a fixed
495// list of terminal kinds, so every kind missing from the list — `-x`,
496// `x + 1`, `this`, `*p`, a lambda — scored 0 against a cyclomatic
497// decision of 1 (#1520, #1526). Asking whether the decision is already
498// paid for covers every kind the grammar can put in the slot, including
499// ones a future grammar adds. Each language supplies only its peel step
500// and `scores_itself`, so the rule has this one home (lesson 59).
501pub(super) fn count_boolean_slot<'a>(
502    slot: &Node<'a>,
503    step: PeelStep<'a>,
504    scores_itself: impl Fn(&Node<'a>) -> bool,
505    conditions: &mut f64,
506) {
507    if !scores_itself(&peel(slot, step).0) {
508        *conditions += 1.;
509    }
510}
511
512// The same rule for an operand outside a boolean slot — a call
513// argument, a `return` value, a ternary branch. Those are type-free, so
514// the operand is a slot only when the peel proves it boolean through a
515// negation layer: `f(!x)` scores one, `f(x)` and `f(-x)` none. A
516// negation is a slot whatever it negates (`!this`, `!-x`), exactly as
517// Ruby's negated ternary branch is (#1529).
518pub(super) fn count_negated_operand<'a>(
519    operand: &Node<'a>,
520    step: PeelStep<'a>,
521    scores_itself: fn(&Node<'a>) -> bool,
522    conditions: &mut f64,
523) {
524    let (node, proves_boolean) = peel(operand, step);
525    if proves_boolean && !scores_itself(&node) {
526        *conditions += 1.;
527    }
528}
529
530// Fitzpatrick Rule 9 for a chain whose grammar names its two operands
531// `left` and `right`: each is scored by `score`, the language's
532// `count_boolean_slot` or `count_negated_operand` wrapper — a closure
533// where it needs the source bytes (Elixir's, #1574). Read by field rather
534// than as every operand child, so a comment beside the operator — a
535// named child too — does not pay. The caller owns the gate deciding
536// that `chain` is a chain at all.
537pub(super) fn count_field_operands(
538    chain: &Node,
539    score: impl Fn(&Node, &mut f64),
540    conditions: &mut f64,
541) {
542    for field in ["left", "right"] {
543        if let Some(operand) = chain.child_by_field_name(field) {
544            score(&operand, conditions);
545        }
546    }
547}
548
549// Each operand child of `list` — an argument list, a `return` value
550// list, a chain whose grammar names no operand field — scored by
551// `score`. `is_operand` keeps punctuation and comments out.
552pub(super) fn count_each_operand(
553    list: &Node,
554    score: impl Fn(&Node, &mut f64),
555    conditions: &mut f64,
556) {
557    for operand in list.children().filter(is_operand) {
558        score(&operand, conditions);
559    }
560}
561
562// Default no-op `Abc` impls. Audited in #188; the matrix below
563// records the rationale for every entry so the no-op default is a
564// deliberate choice, not scaffolding leftover.
565//
566// Real defaults (the language has no construct ABC measures, so the
567// metric is genuinely 0):
568//   - PreprocCode, CcommentCode: no executable code (comments /
569//     preprocessor lines only).
570implement_metric_trait!(Abc, PreprocCode, CcommentCode);
571
572#[cfg(test)]
573#[allow(
574    clippy::float_cmp,
575    clippy::cast_precision_loss,
576    clippy::cast_possible_truncation,
577    clippy::cast_sign_loss,
578    clippy::similar_names,
579    clippy::doc_markdown,
580    clippy::needless_raw_string_hashes,
581    clippy::too_many_lines
582)]
583mod tests {
584    #[cfg(feature = "csharp")]
585    use crate::test_support::assert_csharp_fixture_spells;
586    use crate::test_support::{
587        assert_fixture_spells, ast_has_kind_id, check_func_space_only_shim,
588        check_metrics_only_shim, child_space, metrics_verbatim,
589    };
590    // Hand-written, not derived. A trait is used through its methods, so
591    // nothing in a test body names `ParserTrait` and
592    // `check-test-lang-gates.py` derives no gate for an import at all,
593    // so an ungated one stays compiled into builds that never call
594    // `<Lang>Parser::new`. These seven are the languages that do; a new
595    // `XParser::new` test in this module needs its feature added here,
596    // and the leg that lacks it says so as an unused import.
597    #[cfg(any(
598        feature = "csharp",
599        feature = "groovy",
600        feature = "kotlin",
601        feature = "php",
602        feature = "ruby",
603        feature = "rust",
604        feature = "typescript",
605    ))]
606    use crate::traits::ParserTrait;
607
608    use super::*;
609
610    check_metrics_only_shim!(check_metrics, Abc);
611    // Every `check_func_space` caller in this module is an ABC-versus-
612    // cyclomatic parity test (`abc.conditions() == cyclomatic() - 1` on
613    // the same space), so the func-space shim carries Cyclomatic too.
614    check_func_space_only_shim!(check_func_space, Abc, Cyclomatic);
615
616    /// `abc.conditions_sum()` of `src` under a walk restricted to ABC
617    /// and the metrics it resolves, for the cross-language parity tests
618    /// that compare one construct across sibling grammars.
619    /// `check_metrics` expands to a plain `fn` and so cannot close over
620    /// a reference value; this can. Restricted rather than
621    /// `MetricsOptions::default()` per `metrics_verbatim`'s own doc
622    /// (#1127).
623    #[cfg(any(
624        feature = "c",
625        feature = "cpp",
626        feature = "csharp",
627        feature = "go",
628        feature = "java",
629        feature = "javascript",
630        feature = "mozcpp",
631        feature = "mozjs",
632        feature = "objc",
633        feature = "perl",
634        feature = "typescript",
635    ))]
636    fn abc_conditions(lang: LANG, src: &str) -> u64 {
637        metrics_verbatim(
638            lang,
639            src.as_bytes(),
640            MetricsOptions::default().with_only(&[crate::Metric::Abc]),
641        )
642        .abc
643        .conditions_sum()
644    }
645
646    // Recurse to the innermost function space and assert the invariant
647    // named above there. `conditions()` is that one space's own count,
648    // so it pairs with `cyclomatic()` and never `cyclomatic_sum()`,
649    // which folds in a base of 1 per nested space.
650    #[cfg(any(
651        feature = "cpp",
652        feature = "csharp",
653        feature = "groovy",
654        feature = "java",
655        feature = "perl",
656        feature = "php",
657        feature = "typescript",
658    ))]
659    fn assert_deepest_conditions_match_cyclomatic(space: &crate::FuncSpace, expected: u64) {
660        let mut deepest = space;
661        while let Some(child) = deepest.spaces.last() {
662            deepest = child;
663        }
664        let decisions = deepest.metrics.cyclomatic.cyclomatic() - 1;
665        assert_eq!(decisions, expected);
666        assert_eq!(deepest.metrics.abc.conditions(), decisions);
667    }
668
669    // The sibling form of the helper above, for a fixture whose point is
670    // that several *different* spellings of one construct agree. The
671    // deepest-space walk cannot serve those: it follows `spaces.last()`
672    // and so inspects exactly one member, which is the shape #1383's
673    // over-count hid behind — a merged root total cannot tell a
674    // relational method scoring 2 from the constant control scoring 2.
675    #[cfg(any(feature = "csharp", feature = "elixir"))]
676    fn assert_every_member_scores(
677        container: &crate::FuncSpace,
678        members: usize,
679        expected: u64,
680        why: &str,
681    ) {
682        // The count is a parameter rather than a `!is_empty()` check so
683        // a caller cannot omit it: without it a fixture that lost every
684        // member but one still satisfies "every member scores N".
685        assert_eq!(
686            container.spaces.len(),
687            members,
688            "member count changed — the fixture moved, not the metric"
689        );
690        for member in &container.spaces {
691            let name = member.name.as_deref().unwrap_or("?");
692            assert_eq!(member.metrics.abc.conditions(), expected, "{name}: {why}");
693            assert_eq!(
694                member.metrics.abc.conditions(),
695                member.metrics.cyclomatic.cyclomatic() - 1,
696                "{name}: §8 parity with the cyclomatic decision count"
697            );
698        }
699    }
700
701    // The second sibling, for a fixture whose point is that a construct
702    // moves a member *off* the value its unguarded control scores.
703    // `assert_every_member_scores` cannot serve those: it takes one
704    // expected value for the whole container, and every #1422 guard
705    // fixture deliberately pairs guarded members with an unguarded
706    // control one lower — which is the comparison the test exists to
707    // make. Both tiers per member, because a guard is a decision in each
708    // and a fix that moved only one of them would satisfy neither claim
709    // on its own, and because a member may legitimately sit *above* its
710    // decision count — an `else` arm, or a comparison nested inside
711    // another comparison, is an ABC condition with no cyclomatic
712    // decision behind it (#1421).
713    #[cfg(any(
714        feature = "csharp",
715        feature = "elixir",
716        feature = "go",
717        feature = "groovy",
718        feature = "java",
719        feature = "kotlin",
720        feature = "perl",
721        feature = "python",
722        feature = "ruby",
723        feature = "rust",
724    ))]
725    #[track_caller]
726    fn assert_members_score(container: &crate::FuncSpace, expected: &[(&str, u64, u64)]) {
727        assert_eq!(
728            container.spaces.len(),
729            expected.len(),
730            "member count changed — the fixture moved, not the metric"
731        );
732        for (name, conditions, cyclomatic) in expected {
733            let member = child_space(container, name);
734            assert_eq!(
735                member.metrics.abc.conditions(),
736                *conditions,
737                "{name}: abc.conditions"
738            );
739            assert_eq!(
740                member.metrics.cyclomatic.cyclomatic(),
741                *cyclomatic,
742                "{name}: cyclomatic"
743            );
744        }
745    }
746
747    /// Regression for #227: a `Stats::default()` that never sees an
748    /// observation must not leak the `f64::MAX` sentinel for
749    /// `assignments_min`, `branches_min`, or `conditions_min`. All
750    /// three getters collapse the sentinel to `0.0` so JSON never
751    /// emits `1.7976931e308`.
752    #[test]
753    fn abc_empty_file_min_is_zero() {
754        let stats = Stats::default();
755        assert_eq!(stats.assignments_min(), 0);
756        assert_eq!(stats.branches_min(), 0);
757        assert_eq!(stats.conditions_min(), 0);
758    }
759
760    // The `EQ` arm of `java_count_token_assignment`: a plain `=` counts
761    // unless `java_eq_initializes_final_binding` finds it initialising a
762    // `final` binding, whether the declaration is a local or a field.
763    #[cfg(feature = "java")]
764    #[test]
765    fn java_eq_arm_counts_outside_final_declarations() {
766        check_metrics::<JavaParser>(
767            "class A { void m() { int x = 0; x = 1; x = 2; x = 3; } }",
768            "foo.java",
769            |metric| {
770                // `int x = 0;` is not `final`, so it counts like each
771                // `x = N;` that follows.
772                assert_eq!(metric.abc.assignments_sum(), 4);
773            },
774        );
775    }
776
777    #[cfg(feature = "java")]
778    #[test]
779    fn java_eq_arm_skips_final_initializers() {
780        check_metrics::<JavaParser>(
781            "class A {
782                final int X = 1;
783                @Deprecated private final int Y = 2;
784                void m() { final int Z = 3; }
785            }",
786            "foo.java",
787            |metric| {
788                // All three `=` tokens are `final` initializers — the
789                // second behind an annotation and an access modifier in
790                // the same `modifiers` node — so assignments are 0
791                // across all spaces.
792                assert_eq!(metric.abc.assignments_sum(), 0);
793            },
794        );
795    }
796
797    #[cfg(feature = "java")]
798    #[test]
799    fn java_final_initializer_does_not_suppress_the_assignments_inside_it() {
800        // The sentinel stack the structural predicate replaced stayed
801        // live from `final` to the next `;`, so every `=` nested in the
802        // initializer — a lambda body's, an array initializer's — was
803        // suppressed with the declarator's own. Java lambdas open no
804        // space, so nothing else separated them. Each row names the
805        // pre-fix value.
806        let cases = [
807            ("void m() { final Runnable r = () -> { x = 1; }; }", 1, 0),
808            (
809                "void m() { final Runnable r = () -> { x = 1; y = 2; }; }",
810                2,
811                1,
812            ),
813            ("void m() { final Runnable r = () -> x = 1; }", 1, 0),
814            ("void m() { final int[] a = { x = 1 }; }", 1, 0),
815            ("private final Runnable f = () -> { x = 1; };", 1, 0),
816            // The `;` that closed the leak is the reference: the same
817            // body after a `final` local counts as it always did.
818            ("void m() { final int q = 1; x = 1; }", 1, 1),
819        ];
820        let mut ran = 0;
821        for (body, expected, before) in cases {
822            let src = format!("class K {{ int x, y; {body} }}\n");
823            let assignments = metrics_verbatim(
824                LANG::Java,
825                src.as_bytes(),
826                MetricsOptions::default().with_only(&[crate::Metric::Abc]),
827            )
828            .abc
829            .assignments_sum();
830            assert_eq!(assignments, expected, "`{body}` (pre-fix {before})");
831            ran += 1;
832        }
833        assert_eq!(ran, cases.len());
834        assert!(cases.iter().any(|&(_, now, before)| now != before));
835    }
836
837    // Constant declarations are not counted as assignments
838    #[cfg(feature = "java")]
839    #[test]
840    fn java_constant_declarations() {
841        check_metrics::<JavaParser>(
842            "class A {
843                private final int X1 = 0, Y1 = 0;
844                public final float PI = 3.14f;
845                final static String HELLO = \"Hello,\";
846                protected String world = \" world!\";   // +1a
847                public float e = 2.718f;                // +1a
848                private int x2 = 1, y2 = 2;             // +2a
849
850                void m() {
851                    final int Z1 = 0, Z2 = 0, Z3 = 0;
852                    final float T = 0.0f;
853                    int z1 = 1, z2 = 2, z3 = 3;         // +3a
854                    float t = 60.0f;                    // +1a
855                }
856            }",
857            "foo.java",
858            |metric| {
859                // magnitude: sqrt(64 + 0 + 0) = sqrt(64)
860                // space count: 3 (1 unit, 1 class and 1 method)
861                insta::assert_json_snapshot!(
862                    metric.abc,
863                    @r#"
864                {
865                  "assignments": 8,
866                  "branches": 0,
867                  "conditions": 0,
868                  "magnitude": 8.0,
869                  "value": 0.0,
870                  "assignments_average": 2.6666666666666665,
871                  "branches_average": 0.0,
872                  "conditions_average": 0.0,
873                  "assignments_min": 0,
874                  "assignments_max": 4,
875                  "branches_min": 0,
876                  "branches_max": 0,
877                  "conditions_min": 0,
878                  "conditions_max": 0
879                }
880                "#
881                );
882            },
883        );
884    }
885
886    // "In computer science, conditionals (that is, conditional statements, conditional expressions
887    // and conditional constructs,) are programming language commands for handling decisions."
888    // Source: https://en.wikipedia.org/wiki/Conditional_(computer_programming)
889    // According to this definition, boolean expressions that are evaluated to make a decision are considered as conditions
890    // Variables, method invocations and true or false values used inside
891    // variable declarations and assignment expressions are not counted as conditions
892    #[cfg(feature = "java")]
893    #[test]
894    fn java_declarations_with_conditions() {
895        check_metrics::<JavaParser>(
896            "
897            boolean a = (1 > 2);            // +1a +1c
898            boolean b = 3 > 4;              // +1a +1c
899            boolean c = (1 > 2) && 3 > 4;   // +1a +2c
900            boolean d = b && (x > 5) || c;  // +1a +3c
901            boolean e = !d;                 // +1a +1c
902            boolean f = ((!false));         // +1a +1c
903            boolean g = !(!(true));         // +1a +1c
904            boolean h = true;               // +1a
905            boolean i = (false);            // +1a
906            boolean j = (((((true)))));     // +1a
907            boolean k = (((((m())))));      // +1a +1b
908            boolean l = (((((!m())))));     // +1a +1b +1c
909            boolean m = (!(!((m()))));      // +1a +1b +1c
910            List<String> n = null;          // +1a (< and > used for generic types are not counted as conditions)
911            ",
912            "foo.java",
913          |metric| {
914                // magnitude: sqrt(196 + 9 + 144) = sqrt(349)
915                // space count: 1 (1 unit)
916                insta::assert_json_snapshot!(
917                    metric.abc,
918                    @r#"
919                {
920                  "assignments": 14,
921                  "branches": 3,
922                  "conditions": 12,
923                  "magnitude": 18.681541692269406,
924                  "value": 18.681541692269406,
925                  "assignments_average": 14.0,
926                  "branches_average": 3.0,
927                  "conditions_average": 12.0,
928                  "assignments_min": 14,
929                  "assignments_max": 14,
930                  "branches_min": 3,
931                  "branches_max": 3,
932                  "conditions_min": 12,
933                  "conditions_max": 12
934                }
935                "#
936                );
937            },
938        );
939    }
940
941    // Conditions can be found in assignment expressions
942    #[cfg(feature = "java")]
943    #[test]
944    fn java_assignments_with_conditions() {
945        check_metrics::<JavaParser>(
946            "
947            a = 2 < 1;                  // +1a +1c
948            b = (4 >= 3) && 2 <= 1;     // +1a +2c
949            c = a || (x != 10) && b;    // +1a +3c
950            d = !false;                 // +1a +1c
951            e = (!false);               // +1a +1c
952            f = !(false);               // +1a +1c
953            g = (!(((true))));          // +1a +1c
954            h = ((true));               // +1a
955            i = !m();                   // +1a +1b +1c
956            j = !((m()));               // +1a +1b +1c
957            k = (!(m()));               // +1a +1b +1c
958            l = ((!(m())));             // +1a +1b +1c
959            m = !B.<Integer>m(2);       // +1a +1b +1c
960            n = !((B.<Integer>m(4)));   // +1a +1b +1c
961            ",
962            "foo.java",
963            |metric| {
964                // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
965                // space count: 1 (1 unit)
966                insta::assert_json_snapshot!(
967                    metric.abc,
968                    @r#"
969                {
970                  "assignments": 14,
971                  "branches": 6,
972                  "conditions": 16,
973                  "magnitude": 22.090722034374522,
974                  "value": 22.090722034374522,
975                  "assignments_average": 14.0,
976                  "branches_average": 6.0,
977                  "conditions_average": 16.0,
978                  "assignments_min": 14,
979                  "assignments_max": 14,
980                  "branches_min": 6,
981                  "branches_max": 6,
982                  "conditions_min": 16,
983                  "conditions_max": 16
984                }
985                "#
986                );
987            },
988        );
989    }
990
991    // Conditions can be found in method arguments
992    #[cfg(feature = "java")]
993    #[test]
994    fn java_methods_arguments_with_conditions() {
995        check_metrics::<JavaParser>(
996            "
997            m1(a);                                  // +1b
998            m2(a, b);                               // +1b
999            m3(true, (false), (((true))));          // +1b
1000            m3(m1(false), m1(true), m1(false));     // +4b
1001            m1(!a);                                 // +1b +1c
1002            m2((((a))), (!b));                      // +1b +1c
1003            m3(!(a), b, !!!c);                      // +1b +2c
1004            m3(a, !b, m2(!a, !m2(!b, !m1(!c))));    // +4b +6c
1005            ",
1006            "foo.java",
1007            |metric| {
1008                // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
1009                // space count: 1 (1 unit)
1010                insta::assert_json_snapshot!(
1011                    metric.abc,
1012                    @r#"
1013                {
1014                  "assignments": 0,
1015                  "branches": 14,
1016                  "conditions": 10,
1017                  "magnitude": 17.204650534085253,
1018                  "value": 17.204650534085253,
1019                  "assignments_average": 0.0,
1020                  "branches_average": 14.0,
1021                  "conditions_average": 10.0,
1022                  "assignments_min": 0,
1023                  "assignments_max": 0,
1024                  "branches_min": 14,
1025                  "branches_max": 14,
1026                  "conditions_min": 10,
1027                  "conditions_max": 10
1028                }
1029                "#
1030                );
1031            },
1032        );
1033    }
1034
1035    // "A unary conditional expression is an implicit condition that uses no relational operators."
1036    // Source: Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report.
1037    // https://www.softwarerenovation.com/Articles.aspx (page 5)
1038    #[cfg(feature = "java")]
1039    #[test]
1040    fn java_if_single_conditions() {
1041        check_metrics::<JavaParser>(
1042            "
1043            if ( a < 0 ) {}             // +1c
1044            if ( ((a != 0)) ) {}        // +1c
1045            if ( !(a > 0) ) {}          // +1c
1046            if ( !(((a == 0))) ) {}     // +1c
1047            if ( b.m1() ) {}            // +1b +1c
1048            if ( !b.m1() ) {}           // +1b +1c
1049            if ( !!b.m2() ) {}          // +1b +1c
1050            if ( (!(b.m1())) ) {}       // +1b +1c
1051            if ( (!(!b.m1())) ) {}      // +1b +1c
1052            if ( ((b.m2())) ) {}        // +1b +1c
1053            if ( ((b.m().m1())) ) {}    // +2b +1c
1054            if ( c ) {}                 // +1c
1055            if ( !c ) {}                // +1c
1056            if ( !!!!!!!!!!c ) {}       // +1c
1057            if ( (((c))) ) {}           // +1c
1058            if ( (((!c))) ) {}          // +1c
1059            if ( ((!(c))) ) {}          // +1c
1060            if ( true ) {}              // +1c
1061            if ( !true ) {}             // +1c
1062            if ( ((false)) ) {}         // +1c
1063            if ( !(!(false)) ) {}       // +1c
1064            if ( !!!false ) {}          // +1c
1065            ",
1066            "foo.java",
1067            |metric| {
1068                // magnitude: sqrt(0 + 64 + 484) = sqrt(548)
1069                // space count: 1 (1 unit)
1070                insta::assert_json_snapshot!(
1071                    metric.abc,
1072                    @r#"
1073                {
1074                  "assignments": 0,
1075                  "branches": 8,
1076                  "conditions": 22,
1077                  "magnitude": 23.40939982143925,
1078                  "value": 23.40939982143925,
1079                  "assignments_average": 0.0,
1080                  "branches_average": 8.0,
1081                  "conditions_average": 22.0,
1082                  "assignments_min": 0,
1083                  "assignments_max": 0,
1084                  "branches_min": 8,
1085                  "branches_max": 8,
1086                  "conditions_min": 22,
1087                  "conditions_max": 22
1088                }
1089                "#
1090                );
1091            },
1092        );
1093    }
1094
1095    #[cfg(feature = "java")]
1096    #[test]
1097    fn java_if_multiple_conditions() {
1098        check_metrics::<JavaParser>(
1099            "
1100            if ( a || b || c || d ) {}              // +4c
1101            if ( a || b && c && d ) {}              // +4c
1102            if ( x < y && a == b ) {}               // +2c
1103            if ( ((z < (x + y))) ) {}               // +1c
1104            if ( a || ((((b))) && c) ) {}           // +3c
1105            if ( a && ((((a == b))) && c) ) {}      // +3c
1106            if ( a || ((((a == b))) || ((c))) ) {}  // +3c
1107            if ( x < y && B.m() ) {}                // +1b +2c
1108            if ( x < y && !(((B.m()))) ) {}         // +1b +2c
1109            if ( !(x < y) && !B.m() ) {}            // +1b +2c
1110            if ( !!!(!!!(a)) && B.m() ||            // +1b +2c
1111                 !B.m() && (((x > 4))) ) {}         // +1b +2c
1112            ",
1113            "foo.java",
1114            |metric| {
1115                // magnitude: sqrt(0 + 25 + 900) = sqrt(925)
1116                // space count: 1 (1 unit)
1117                insta::assert_json_snapshot!(
1118                    metric.abc,
1119                    @r#"
1120                {
1121                  "assignments": 0,
1122                  "branches": 5,
1123                  "conditions": 30,
1124                  "magnitude": 30.4138126514911,
1125                  "value": 30.4138126514911,
1126                  "assignments_average": 0.0,
1127                  "branches_average": 5.0,
1128                  "conditions_average": 30.0,
1129                  "assignments_min": 0,
1130                  "assignments_max": 0,
1131                  "branches_min": 5,
1132                  "branches_max": 5,
1133                  "conditions_min": 30,
1134                  "conditions_max": 30
1135                }
1136                "#
1137                );
1138            },
1139        );
1140    }
1141
1142    #[cfg(feature = "java")]
1143    #[test]
1144    fn java_while_and_do_while_conditions() {
1145        check_metrics::<JavaParser>(
1146            "
1147            while ( (!(!(!(a)))) ) {}                   // +1c
1148            while ( b || 1 > 2 ) {}                     // +2c
1149            while ( x.m() && (((c))) ) {}               // +1b +2c
1150            do {} while ( !!!(((!!!a))) );              // +1c
1151            do {} while ( a || (b && c) );              // +3c
1152            do {} while ( !x.m() && 1 > 2 || !true );   // +1b +3c
1153            ",
1154            "foo.java",
1155            |metric| {
1156                // magnitude: sqrt(0 + 4 + 144) = sqrt(148)
1157                // space count: 1 (1 unit)
1158                insta::assert_json_snapshot!(
1159                    metric.abc,
1160                    @r#"
1161                {
1162                  "assignments": 0,
1163                  "branches": 2,
1164                  "conditions": 12,
1165                  "magnitude": 12.165525060596439,
1166                  "value": 12.165525060596439,
1167                  "assignments_average": 0.0,
1168                  "branches_average": 2.0,
1169                  "conditions_average": 12.0,
1170                  "assignments_min": 0,
1171                  "assignments_max": 0,
1172                  "branches_min": 2,
1173                  "branches_max": 2,
1174                  "conditions_min": 12,
1175                  "conditions_max": 12
1176                }
1177                "#
1178                );
1179            },
1180        );
1181    }
1182
1183    // GMetrics, a Groovy source code analyzer, provides the following definition of unary conditional expression:
1184    // "These are cases where a single variable/field/value is treated as a boolean value.
1185    // Examples include `if (x)` and `return !ready`."
1186    // According to this definition, unary conditional expressions are counted also in function return values.
1187    // Source: https://dx42.github.io/gmetrics/metrics/AbcMetric.html
1188    // Examples: https://github.com/dx42/gmetrics/blob/master/src/test/groovy/org/gmetrics/metric/abc/AbcMetric_MethodTest.groovy
1189    #[cfg(feature = "java")]
1190    #[test]
1191    fn java_return_with_conditions() {
1192        check_metrics::<JavaParser>(
1193            "class A {
1194                boolean m1() {
1195                    return !(z >= 0);       // +1c
1196                }
1197                boolean m2() {
1198                    return (((!x)));        // +1c
1199                }
1200                boolean m3() {
1201                    return x && y;          // +2c
1202                }
1203                boolean m4() {
1204                    return y || (z < 0);    // +2c
1205                }
1206                boolean m5() {
1207                    return x || y ?         // +3c (two unary conditions and one ?)
1208                        true : false;
1209                }
1210            }",
1211            "foo.java",
1212            |metric| {
1213                // magnitude: sqrt(0 + 0 + 81) = sqrt(81)
1214                // space count: 7 (1 unit, 1 class and 5 methods)
1215                insta::assert_json_snapshot!(
1216                    metric.abc,
1217                    @r#"
1218                {
1219                  "assignments": 0,
1220                  "branches": 0,
1221                  "conditions": 9,
1222                  "magnitude": 9.0,
1223                  "value": 0.0,
1224                  "assignments_average": 0.0,
1225                  "branches_average": 0.0,
1226                  "conditions_average": 1.2857142857142858,
1227                  "assignments_min": 0,
1228                  "assignments_max": 0,
1229                  "branches_min": 0,
1230                  "branches_max": 0,
1231                  "conditions_min": 0,
1232                  "conditions_max": 3
1233                }
1234                "#
1235                );
1236            },
1237        );
1238    }
1239
1240    // Variables, method invocations, and true or false values
1241    // inside return statements are not counted as conditions
1242    #[cfg(feature = "java")]
1243    #[test]
1244    fn java_return_without_conditions() {
1245        check_metrics::<JavaParser>(
1246            "class A {
1247                boolean m1() {
1248                    return x;
1249                }
1250                boolean m2() {
1251                    return (x);
1252                }
1253                boolean m3() {
1254                    return y.m();   // +1b
1255                }
1256                boolean m4() {
1257                    return false;
1258                }
1259                void m5() {
1260                    return;
1261                }
1262            }",
1263            "foo.java",
1264            |metric| {
1265                // magnitude: sqrt(0 + 1 + 0) = sqrt(1)
1266                // space count: 7 (1 unit, 1 class and 5 methods)
1267                insta::assert_json_snapshot!(
1268                    metric.abc,
1269                    @r#"
1270                {
1271                  "assignments": 0,
1272                  "branches": 1,
1273                  "conditions": 0,
1274                  "magnitude": 1.0,
1275                  "value": 0.0,
1276                  "assignments_average": 0.0,
1277                  "branches_average": 0.14285714285714285,
1278                  "conditions_average": 0.0,
1279                  "assignments_min": 0,
1280                  "assignments_max": 0,
1281                  "branches_min": 0,
1282                  "branches_max": 1,
1283                  "conditions_min": 0,
1284                  "conditions_max": 0
1285                }
1286                "#
1287                );
1288            },
1289        );
1290    }
1291
1292    // Variables, method invocations, and true or false values
1293    // in lambda expression return values are not counted as conditions
1294    #[cfg(feature = "java")]
1295    #[test]
1296    fn java_lambda_expressions_return_with_conditions() {
1297        check_metrics::<JavaParser>(
1298            "
1299            Predicate<Boolean> p1 = a -> a;                         // +1a
1300            Predicate<Boolean> p2 = b -> true;                      // +1a
1301            Predicate<Boolean> p3 = c -> m();                       // +1a
1302            Predicate<Integer> p4 = d -> d > 10;                    // +1a +1c
1303            Predicate<Boolean> p5 = (e) -> !e;                      // +1a +1c
1304            Predicate<Boolean> p6 = (f) -> !((!f));                 // +1a +1c
1305            Predicate<Boolean> p7 = (g) -> !g && true;              // +1a +2c
1306            BiPredicate<Boolean, Boolean> bp1 = (h, i) -> !h && !i; // +1a +2c
1307            BiPredicate<Boolean, Boolean> bp2 = (j, k) -> {
1308                return j || k;                                      // +1a +2c
1309            };
1310            ",
1311            "foo.java",
1312            |metric| {
1313                // magnitude: sqrt(81 + 1 + 81) = sqrt(163)
1314                // space count: 1 (1 unit)
1315                insta::assert_json_snapshot!(
1316                    metric.abc,
1317                    @r#"
1318                {
1319                  "assignments": 9,
1320                  "branches": 1,
1321                  "conditions": 9,
1322                  "magnitude": 12.767145334803704,
1323                  "value": 12.767145334803704,
1324                  "assignments_average": 9.0,
1325                  "branches_average": 1.0,
1326                  "conditions_average": 9.0,
1327                  "assignments_min": 9,
1328                  "assignments_max": 9,
1329                  "branches_min": 1,
1330                  "branches_max": 1,
1331                  "conditions_min": 9,
1332                  "conditions_max": 9
1333                }
1334                "#
1335                );
1336            },
1337        );
1338    }
1339
1340    #[cfg(feature = "java")]
1341    #[test]
1342    fn java_for_with_variable_declaration() {
1343        check_metrics::<JavaParser>(
1344            "
1345            for ( int i1 = 0; !(!(!(!a))); i1++ ) {}                // +2a +1c
1346            for ( int i2 = 0; !B.m(); i2++ ) {}                     // +2a +1b +1c
1347            for ( int i3 = 0; a || false; i3++ ) {}                 // +2a +2c
1348            for ( int i4 = 0; a && B.m() ? true : false; i4++ ) {}  // +2a +1b +3c
1349            for ( int i5 = 0; true; i5++ ) {}                       // +2a +1c
1350            ",
1351            "foo.java",
1352            |metric| {
1353                // magnitude: sqrt(100 + 4 + 64) = sqrt(168)
1354                // space count: 1 (1 unit)
1355                insta::assert_json_snapshot!(
1356                    metric.abc,
1357                    @r#"
1358                {
1359                  "assignments": 10,
1360                  "branches": 2,
1361                  "conditions": 8,
1362                  "magnitude": 12.96148139681572,
1363                  "value": 12.96148139681572,
1364                  "assignments_average": 10.0,
1365                  "branches_average": 2.0,
1366                  "conditions_average": 8.0,
1367                  "assignments_min": 10,
1368                  "assignments_max": 10,
1369                  "branches_min": 2,
1370                  "branches_max": 2,
1371                  "conditions_min": 8,
1372                  "conditions_max": 8
1373                }
1374                "#
1375                );
1376            },
1377        );
1378    }
1379
1380    #[cfg(feature = "java")]
1381    #[test]
1382    fn java_for_without_variable_declaration() {
1383        check_metrics::<JavaParser>(
1384            "class A{
1385                void m1() {
1386                    for (i = 0; x < y; i++) {}          // +2a +1c
1387                    for (i = 0; ((x < y)); i++) {}      // +2a +1c
1388                    for (i = 0; !(!(x < y)); i++) {}    // +2a +1c
1389                    for (i = 0; true; i++) {}           // +2a +1c
1390                }
1391                void m2() {
1392                    for ( ; true; ) {}  // +1c
1393                }
1394                void m3() {
1395                    for ( ; ; ) {}      // +0c — no condition to count (#1276)
1396                }
1397            }",
1398            "foo.java",
1399            |metric| {
1400                // magnitude: sqrt(64 + 0 + 25) = sqrt(89)
1401                // space count: 5 (1 unit, 1 class and 3 methods)
1402                insta::assert_json_snapshot!(
1403                    metric.abc,
1404                    @r#"
1405                {
1406                  "assignments": 8,
1407                  "branches": 0,
1408                  "conditions": 5,
1409                  "magnitude": 9.433981132056603,
1410                  "value": 0.0,
1411                  "assignments_average": 1.6,
1412                  "branches_average": 0.0,
1413                  "conditions_average": 1.0,
1414                  "assignments_min": 0,
1415                  "assignments_max": 8,
1416                  "branches_min": 0,
1417                  "branches_max": 0,
1418                  "conditions_min": 0,
1419                  "conditions_max": 4
1420                }
1421                "#
1422                );
1423            },
1424        );
1425    }
1426
1427    // Issue #1276 changed Java's answer here. `java_walk_for_statement`
1428    // used to read child(3), fall through to child(4) when that was the
1429    // `;` an expression initializer leaves behind, and count a `;` or
1430    // `)` landing there as a vacuously-true condition — so `for (;;)`
1431    // scored one. Nothing in Fitzpatrick's C dimension attributes a
1432    // condition to a `for` keyword; it counts conditional operators and
1433    // unary conditions that are present. Java and Groovy were the only
1434    // two impls disagreeing, and the field-addressed walker now reports
1435    // zero the way the C family, the JS family, PHP, C# and Go all do.
1436    #[cfg(feature = "java")]
1437    #[test]
1438    fn java_empty_for_condition_counts_nothing() {
1439        check_metrics::<JavaParser>(
1440            "class A { void m() { for (;;) { break; } } }",
1441            "foo.java",
1442            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
1443        );
1444        // The other empty spelling, and the one the old cascade
1445        // actually mis-scored: with an **expression** initializer the
1446        // children are `for ( init ; ; update )`, so child(3) was the
1447        // separating `;` and child(4) the empty condition's `;`, which
1448        // the vacuous-true arm counted. The `for (int i = 0; ; i++)`
1449        // spelling scored 0 both before and after — Java's
1450        // `local_variable_declaration` swallows its own `;`, putting the
1451        // update expression at child(4), where it matched no arm — so it
1452        // would be a vacuous regression test and is deliberately not
1453        // used here.
1454        check_metrics::<JavaParser>(
1455            "class A { void m() { int i; for (i = 0; ; i++) { break; } } }",
1456            "foo.java",
1457            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
1458        );
1459    }
1460
1461    // The second defect the positional cascade carried: tree-sitter
1462    // counts comments among a node's children, so a comment anywhere in
1463    // the header shifted every index and the condition went unread —
1464    // the same failure #1181 removed from `java_walk_ternary`. Reading
1465    // the `condition` field cannot shift.
1466    #[cfg(feature = "java")]
1467    #[test]
1468    fn java_for_condition_survives_a_header_comment() {
1469        check_metrics::<JavaParser>(
1470            "class A { void m(boolean a) { for (; /* n */ a; ) { break; } } }",
1471            "foo.java",
1472            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
1473        );
1474        // Unchanged control: the same loop without the comment. Both
1475        // spellings must agree, which is the property the positional
1476        // form broke.
1477        check_metrics::<JavaParser>(
1478            "class A { void m(boolean a) { for (; a; ) { break; } } }",
1479            "foo.java",
1480            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
1481        );
1482    }
1483
1484    // Variables, method invocations, and true or false values
1485    // in ternary expression return values are not counted as conditions
1486    #[cfg(feature = "java")]
1487    #[test]
1488    fn java_ternary_conditions() {
1489        check_metrics::<JavaParser>(
1490            "
1491            a = true;                                   // +1a
1492            b = a ? true : false;                       // +1a +2c
1493            c = ((((a)))) ? !false : !b;                // +1a +4c
1494            d = !this.m() ? !!a : (false);              // +1a +1b +3c
1495            e = !(a) && b ? ((c)) : !d;                 // +1a +4c
1496            if ( this.m() ? a : !this.m() ) {}          // +2b +3c
1497            if ( x > 0 ? !(false) : this.m() ) {}       // +1b +3c
1498            if ( x > 0 && x != 3 ? !(a) : (!(b)) ) {}   // +5c
1499            ",
1500            "foo.java",
1501            |metric| {
1502                // magnitude: sqrt(25 + 16 + 576) = sqrt(617)
1503                //  space count: 1 (1 unit)
1504                insta::assert_json_snapshot!(
1505                    metric.abc,
1506                    @r#"
1507                {
1508                  "assignments": 5,
1509                  "branches": 4,
1510                  "conditions": 24,
1511                  "magnitude": 24.839484696748443,
1512                  "value": 24.839484696748443,
1513                  "assignments_average": 5.0,
1514                  "branches_average": 4.0,
1515                  "conditions_average": 24.0,
1516                  "assignments_min": 5,
1517                  "assignments_max": 5,
1518                  "branches_min": 4,
1519                  "branches_max": 4,
1520                  "conditions_min": 24,
1521                  "conditions_max": 24
1522                }
1523                "#
1524                );
1525            },
1526        );
1527    }
1528
1529    #[cfg(feature = "bash")]
1530    #[test]
1531    fn bash_assignments_only() {
1532        check_metrics::<BashParser>(
1533            "f() {
1534                 a=1
1535                 b=2
1536                 c+=3
1537             }",
1538            "foo.sh",
1539            |metric| {
1540                insta::assert_json_snapshot!(
1541                    metric.abc,
1542                    @r#"
1543                {
1544                  "assignments": 3,
1545                  "branches": 0,
1546                  "conditions": 0,
1547                  "magnitude": 3.0,
1548                  "value": 0.0,
1549                  "assignments_average": 1.5,
1550                  "branches_average": 0.0,
1551                  "conditions_average": 0.0,
1552                  "assignments_min": 0,
1553                  "assignments_max": 3,
1554                  "branches_min": 0,
1555                  "branches_max": 0,
1556                  "conditions_min": 0,
1557                  "conditions_max": 0
1558                }
1559                "#
1560                );
1561            },
1562        );
1563    }
1564
1565    #[cfg(feature = "bash")]
1566    #[test]
1567    fn bash_commands_only() {
1568        check_metrics::<BashParser>(
1569            "f() {
1570                 echo a
1571                 ls
1572             }",
1573            "foo.sh",
1574            |metric| {
1575                insta::assert_json_snapshot!(
1576                    metric.abc,
1577                    @r#"
1578                {
1579                  "assignments": 0,
1580                  "branches": 2,
1581                  "conditions": 0,
1582                  "magnitude": 2.0,
1583                  "value": 0.0,
1584                  "assignments_average": 0.0,
1585                  "branches_average": 1.0,
1586                  "conditions_average": 0.0,
1587                  "assignments_min": 0,
1588                  "assignments_max": 0,
1589                  "branches_min": 0,
1590                  "branches_max": 2,
1591                  "conditions_min": 0,
1592                  "conditions_max": 0
1593                }
1594                "#
1595                );
1596            },
1597        );
1598    }
1599
1600    #[cfg(feature = "bash")]
1601    #[test]
1602    fn bash_control_flow_counts_conditions() {
1603        // Regression for #696: Bash control-flow branches are ABC
1604        // conditions (a Bash predicate is a command, so the branch keyword
1605        // is the only condition signal). Each mirrors a cyclomatic decision.
1606        //
1607        // expected: 4 conditions — `if` (1) + `elif` (1) + `while` (1) +
1608        // the non-wildcard case arm `a)` (1). The bare-`*)` wildcard arm is
1609        // the Bash analogue of `default:` and is excluded, exactly as the
1610        // cyclomatic standard count excludes it. No comparison / test
1611        // operators appear, so every condition here is control-flow.
1612        check_metrics::<BashParser>(
1613            "f() {
1614                 if cmd; then
1615                     echo a
1616                 elif other; then
1617                     echo b
1618                 fi
1619                 while running; do
1620                     echo c
1621                 done
1622                 case \"$x\" in
1623                     a) echo d ;;
1624                     *) echo e ;;
1625                 esac
1626             }",
1627            "foo.sh",
1628            |metric| {
1629                assert_eq!(metric.abc.conditions_sum(), 4);
1630            },
1631        );
1632    }
1633
1634    #[cfg(feature = "bash")]
1635    #[test]
1636    fn bash_conditions_mix() {
1637        // Exercises every condition path: `==` and `!=` inside `[[ ]]`,
1638        // arithmetic `<` inside `(( ))`, and the prefix `-z` test operator
1639        // inside `[ ]`. Each `if` body's `echo` contributes a branch.
1640        //
1641        // expected: 8 conditions — each of the four `if`s contributes one
1642        // for the control-flow branch (#696) plus one for its comparison /
1643        // test operator (`==`, `!=`, `<`, `-z`). 4 branches (one `echo`
1644        // each). magnitude = sqrt(4² + 8²) = sqrt(80).
1645        check_metrics::<BashParser>(
1646            "f() {
1647                 if [[ \"$a\" == \"$b\" ]]; then
1648                     echo eq
1649                 fi
1650                 if [[ \"$x\" != \"$y\" ]]; then
1651                     echo ne
1652                 fi
1653                 if (( $a < $b )); then
1654                     echo lt
1655                 fi
1656                 if [ -z \"$x\" ]; then
1657                     echo empty
1658                 fi
1659             }",
1660            "foo.sh",
1661            |metric| {
1662                assert_eq!(metric.abc.conditions_sum(), 8);
1663                assert_eq!(metric.abc.branches_sum(), 4);
1664                insta::assert_json_snapshot!(
1665                    metric.abc,
1666                    @r#"
1667                {
1668                  "assignments": 0,
1669                  "branches": 4,
1670                  "conditions": 8,
1671                  "magnitude": 8.94427190999916,
1672                  "value": 0.0,
1673                  "assignments_average": 0.0,
1674                  "branches_average": 2.0,
1675                  "conditions_average": 4.0,
1676                  "assignments_min": 0,
1677                  "assignments_max": 0,
1678                  "branches_min": 0,
1679                  "branches_max": 4,
1680                  "conditions_min": 0,
1681                  "conditions_max": 8
1682                }
1683                "#
1684                );
1685            },
1686        );
1687    }
1688
1689    #[cfg(feature = "bash")]
1690    #[test]
1691    fn bash_redirection_is_not_a_condition() {
1692        // `>` and `<` spell an I/O redirection as well as a comparison, and
1693        // the grammar parents the redirection under `file_redirect` rather
1694        // than `binary_expression`. Ungated, every redirect in a script
1695        // scored a condition: this fixture measured 2 before the parent
1696        // gate — the Bash instance of #1280's positive-parent polarity.
1697        // expected: 0 conditions — no test, no `if`, no comparison.
1698        check_metrics::<BashParser>(
1699            "f() {\n  echo hi > out.txt\n  read x < in.txt\n}\n",
1700            "foo.sh",
1701            |metric| {
1702                assert_eq!(metric.abc.conditions_sum(), 0);
1703                // The two `echo` / `read` commands still count as branches,
1704                // so the zero above is the gate firing rather than the walk
1705                // skipping the function body.
1706                assert_eq!(metric.abc.branches_sum(), 2);
1707            },
1708        );
1709    }
1710
1711    #[cfg(feature = "bash")]
1712    #[test]
1713    fn bash_comparison_inside_an_arithmetic_or_test_context_is_a_condition() {
1714        // The positive control for the gate above: the same tokens under a
1715        // `binary_expression` are real comparisons in both the `[[ … ]]`
1716        // test form and the `(( … ))` arithmetic form, and still count.
1717        // expected: 4 — one `if` control-flow condition and one `>` per
1718        // function.
1719        check_metrics::<BashParser>(
1720            "f() {\n  if [[ $a > $b ]]; then :; fi\n}\ng() {\n  if (( a > b )); then :; fi\n}\n",
1721            "foo.sh",
1722            |metric| {
1723                assert_eq!(metric.abc.conditions_sum(), 4);
1724            },
1725        );
1726    }
1727
1728    #[cfg(feature = "bash")]
1729    #[test]
1730    fn bash_arithmetic_ternary_is_a_condition() {
1731        // The ABC half of #1268. Cyclomatic and cognitive both count Bash's
1732        // only ternary form; ABC did not, so the identical construct scored
1733        // 1 here against the C family's 2.
1734        // expected: 2 — the `>` comparison and the ternary itself, matching
1735        // `int m = a > b ? a : b;` in C.
1736        check_metrics::<BashParser>(
1737            "f() {\n  local m=$(( a > b ? a : b ))\n}\n",
1738            "foo.sh",
1739            |metric| {
1740                assert_eq!(metric.abc.conditions_sum(), 2);
1741            },
1742        );
1743    }
1744
1745    #[cfg(feature = "bash")]
1746    #[test]
1747    fn bash_magnitude() {
1748        // Combined assignments + branches + conditions. The single `if`
1749        // contributes two conditions (the control-flow branch, #696, plus
1750        // the `==` operator), so magnitude = sqrt(2² + 1² + 2²) = sqrt(9).
1751        check_metrics::<BashParser>(
1752            "f() {
1753                 a=1
1754                 b=2
1755                 if [[ \"$a\" == \"$b\" ]]; then
1756                     echo eq
1757                 fi
1758             }",
1759            "foo.sh",
1760            |metric| {
1761                assert_eq!(metric.abc.conditions_sum(), 2);
1762                insta::assert_json_snapshot!(
1763                    metric.abc,
1764                    @r#"
1765                {
1766                  "assignments": 2,
1767                  "branches": 1,
1768                  "conditions": 2,
1769                  "magnitude": 3.0,
1770                  "value": 0.0,
1771                  "assignments_average": 1.0,
1772                  "branches_average": 0.5,
1773                  "conditions_average": 1.0,
1774                  "assignments_min": 0,
1775                  "assignments_max": 2,
1776                  "branches_min": 0,
1777                  "branches_max": 1,
1778                  "conditions_min": 0,
1779                  "conditions_max": 2
1780                }
1781                "#
1782                );
1783            },
1784        );
1785    }
1786
1787    #[cfg(feature = "java")]
1788    #[test]
1789    fn java_malformed_parenthesized_no_panic() {
1790        check_metrics::<JavaParser>("class A { void m() { if (( }) }", "foo.java", |metric| {
1791            // tree-sitter emits ERROR nodes for this malformed source, so no
1792            // IfStatement, branch, or condition is recognised — all counts are 0.
1793            // Primary goal: the unwrap-free path does not panic.
1794            assert_eq!(metric.abc.assignments(), 0);
1795            assert_eq!(metric.abc.branches(), 0);
1796            assert_eq!(metric.abc.conditions(), 0);
1797            assert_eq!(metric.abc.magnitude(), 0.0);
1798        });
1799    }
1800
1801    #[cfg(feature = "java")]
1802    #[test]
1803    fn java_bool_returning_terminal_kinds_count() {
1804        // Companion to `csharp_bool_returning_terminal_kinds_count`
1805        // (issue #372 / lesson #19). Java's grammar wraps every
1806        // if/while/do condition in `parenthesized_expression`, so
1807        // the gap lived in the former `java_inspect_container`'s
1808        // terminal-arm recognizer: `FieldAccess` (`cfg.flag`), `CastExpression`
1809        // (`(boolean)v`), `ArrayAccess` (`flags[0]`), and
1810        // `InstanceofExpression` (`x instanceof Foo`) were never
1811        // counted. Java has no `await` or `is_pattern` analogues,
1812        // so the C# fix's five-kind set collapses to four here.
1813        //
1814        // `instanceof` has since moved out of the terminal set to an
1815        // unconditional arm (#1461), so it no longer depends on the
1816        // slot to be counted. The total is unchanged, which is the
1817        // point of that change rather than an accident of it.
1818        //
1819        // expected: 4 conditions (one per `if`), 0 assignments,
1820        // 0 branches (no invocations).
1821        check_metrics::<JavaParser>(
1822            "class Cfg { boolean flag; }
1823            class A {
1824                void m(Object v, boolean[] flags, Cfg cfg) {
1825                    if (cfg.flag) { }
1826                    if ((boolean) v) { }
1827                    if (v instanceof Cfg) { }
1828                    if (flags[0]) { }
1829                }
1830            }",
1831            "foo.java",
1832            |metric| {
1833                assert_eq!(metric.abc.conditions_sum(), 4);
1834                assert_eq!(metric.abc.assignments_sum(), 0);
1835                assert_eq!(metric.abc.branches_sum(), 0);
1836            },
1837        );
1838    }
1839
1840    // Issue #1274: a generic *declaration* is type syntax, not a
1841    // decision. `java_count_token_condition` denied only
1842    // `type_arguments`, so `class Gen<T>` and `<U> U ident(U x)` — both
1843    // `type_parameters` — each scored two conditions. The fix counts
1844    // `<` / `>` only under `binary_expression`, the polarity C / C++ /
1845    // Rust / Go already use; a `grammar.json` sweep proves the token is
1846    // emitted from exactly three productions, so the two shapes agree on
1847    // every well-formed input.
1848    //
1849    // One fixture per production the arm must ignore: the class-level
1850    // `type_parameters` (`Gen<T …>`), its `extends` bound's nested
1851    // `type_arguments` (`Comparable<T>`), the method-level
1852    // `type_parameters` (`<U>`), and a nested generic
1853    // (`Map<String, List<T>>`) whose two closing `>` lex as separate
1854    // tokens under separate `type_arguments`. Pre-fix this file scored
1855    // 4 conditions — two per `type_parameters` bracket pair; expected 0,
1856    // the file contains no conditional construct at all.
1857    #[cfg(feature = "java")]
1858    #[test]
1859    fn java_generic_declarations_are_not_conditions() {
1860        check_metrics::<JavaParser>(
1861            "class Gen<T extends Comparable<T>> {
1862                <U> U ident(U x) { return x; }
1863                List<T> pick(Map<String, List<T>> m) { return m.get(\"k\"); }
1864            }",
1865            "foo.java",
1866            |metric| {
1867                assert_eq!(metric.abc.conditions_sum(), 0);
1868                // Non-vacuity guard: 0 is also what an unparsed file
1869                // scores, so pin a value only a walked body can produce
1870                // — the `m.get("k")` invocation.
1871                assert_eq!(metric.abc.branches_sum(), 1);
1872            },
1873        );
1874    }
1875
1876    // Second half of #1274, same premise and same match: a wildcard type
1877    // argument's `?` is type syntax, not a ternary. tree-sitter-java emits
1878    // a bare `?` from exactly two productions — `ternary_expression` and
1879    // `wildcard` — so the `QMARK` arm carries the same allowlist gate the
1880    // `<` / `>` arm does.
1881    //
1882    // The body carries a real ternary over a real comparison so the
1883    // expected total is 2, not 0. Asserting 0 on a wildcard-only fixture
1884    // would have been vacuous — an unparsable file scores 0 too, so the
1885    // test could not tell "the wildcard was ignored" from "the fixture
1886    // stopped being recognised". Each failure mode now lands on its own
1887    // number: 4 if the wildcard `?` counts again (the pre-fix value), 3
1888    // if the gate is aimed at `Wildcard` instead, 1 if it swallows the
1889    // genuine ternary, 0 if parsing breaks. Two wildcards against one
1890    // ternary is deliberate — with one of each, aiming the gate at the
1891    // wrong one of the two productions still totals 2 and the test
1892    // cannot see the difference.
1893    #[cfg(feature = "java")]
1894    #[test]
1895    fn java_generic_wildcard_is_not_a_condition() {
1896        check_metrics::<JavaParser>(
1897            "class A {
1898                int m(List<? extends Number> xs, Map<String, ? extends Number> ys,
1899                      int a, int b) { return a < b ? 1 : 2; }
1900            }",
1901            "foo.java",
1902            |metric| {
1903                assert_eq!(metric.abc.conditions_sum(), 2);
1904            },
1905        );
1906    }
1907
1908    // The other half of #1274: narrowing the `<` / `>` arm must not
1909    // swallow real comparisons. `List<String>` in the signature keeps a
1910    // generic in scope so the fixture proves both directions at once,
1911    // and the assertion is the grammar-dispatch §8 pin — on the
1912    // method's own space the ABC condition count must equal the
1913    // cyclomatic decision count (`cyclomatic()` minus the per-space
1914    // base of 1). Both are 2, one per `if`.
1915    #[cfg(feature = "java")]
1916    #[test]
1917    fn java_comparison_operators_still_count_alongside_generics() {
1918        check_func_space::<JavaParser, _>(
1919            "class A {
1920                int m(List<String> xs, int a, int b) {
1921                    if (a < b) { return 1; }
1922                    if (a > b) { return 2; }
1923                    return 0;
1924                }
1925            }",
1926            "foo.java",
1927            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
1928        );
1929    }
1930
1931    #[cfg(feature = "java")]
1932    #[test]
1933    fn java_constructor_delegation_is_a_branch() {
1934        // Regression for #1279: `super(…)` / `this(…)` parse as
1935        // `explicit_constructor_invocation`, not `method_invocation`, so
1936        // each scored zero branches while Groovy scored one for identical
1937        // source. Both constructors delegate, and neither body contains any
1938        // other call.
1939        // expected: 2 branches — one delegation each.
1940        check_metrics::<JavaParser>(
1941            "class Sub extends Base {
1942                Sub() { super(1); }
1943                Sub(int a) { this(); }
1944            }",
1945            "foo.java",
1946            |metric| {
1947                assert_eq!(metric.abc.branches_sum(), 2);
1948            },
1949        );
1950    }
1951
1952    #[cfg(feature = "java")]
1953    #[test]
1954    fn java_constructor_delegation_does_not_double_count_arguments() {
1955        // The delegation node does not wrap a `method_invocation` for the
1956        // call itself, so `super(f())` is exactly two branches — the
1957        // delegation and the argument call — not three (#1279).
1958        // expected: 2 branches.
1959        check_metrics::<JavaParser>(
1960            "class Sub extends Base {
1961                Sub() { super(f()); }
1962            }",
1963            "foo.java",
1964            |metric| {
1965                assert_eq!(metric.abc.branches_sum(), 2);
1966            },
1967        );
1968    }
1969
1970    #[cfg(feature = "csharp")]
1971    #[test]
1972    fn csharp_constructor_initializer_is_a_branch() {
1973        // C# spells the same delegation as a `constructor_initializer`
1974        // (`: base(…)` / `: this(…)`), which likewise scored zero (#1279).
1975        // expected: 2 branches — one initializer each, no other calls.
1976        check_metrics::<CsharpParser>(
1977            "class Sub : Base {
1978                Sub() : base(1) { }
1979                Sub(int a) : this() { }
1980            }",
1981            "foo.cs",
1982            |metric| {
1983                assert_eq!(metric.abc.branches_sum(), 2);
1984            },
1985        );
1986    }
1987
1988    #[cfg(feature = "csharp")]
1989    #[test]
1990    fn csharp_primary_constructor_base_call_is_a_branch() {
1991        // The C# 12 primary-constructor superclass call invokes the base
1992        // constructor exactly as the `: base(…)` above it does, and scored
1993        // zero until #1406 — the C# sibling of Kotlin's #1384.
1994        //
1995        // tree-sitter-c-sharp 0.23.5 spells the `record` and `class`
1996        // families differently, so per `.claude/rules/grammar-dispatch.md`
1997        // §11 they are two independent paths and each needs its own
1998        // fixture: the record form nests the arguments under a
1999        // `primary_constructor_base_type`, the class form hangs them
2000        // straight off the `base_list`. A record-only fixture says nothing
2001        // about the class arm, and vice versa.
2002        //
2003        // `S` and `R3` are the negatives: a base type with no argument
2004        // list is not a call, and neither spelling emits an
2005        // `argument_list` for it.
2006        let src = "class Sub(int x) : Base(x) { }
2007                   record R1(int x) : Base(x);
2008                   record class R2(int x) : Base(x);
2009                   class G<T>(int x) : GBase<T>(x) where T : class { }
2010                   class Multi(int x) : Base(x), IBase { }
2011                   struct S(int x) : IBase { }
2012                   record R3(int x) : Base;";
2013        // Anchors every spelling against a fixture edit: with presence
2014        // checks only, deleting `(x)` from the class form leaves the
2015        // record form satisfying the arm and this test green. The last
2016        // two counts anchor the declarations that would otherwise decay
2017        // into duplicates of `Sub` — `Multi` owns the fixture's only
2018        // comma (`Base(x), IBase`) and `G` its only type argument list
2019        // (`GBase<T>`), so trimming either back fails here by name.
2020        assert_csharp_fixture_spells(
2021            src,
2022            &[
2023                (
2024                    Csharp::PrimaryConstructorBaseType as u16,
2025                    2,
2026                    "`record` base calls",
2027                ),
2028                (Csharp::ArgumentList as u16, 5, "base-call argument lists"),
2029                (Csharp::BaseList2 as u16, 7, "base lists"),
2030                // The §2 drift marker for the arm's other half. Both ids
2031                // render to `"base_list"`; at this pin every declaration
2032                // family above emits 252 and none emits 246, so
2033                // `BaseList` is defensive and this zero says so. A
2034                // grammar bump that starts emitting it fails here rather
2035                // than silently changing which arm does the work.
2036                (Csharp::BaseList as u16, 0, "unaliased `base_list` nodes"),
2037                (Csharp::COMMA as u16, 1, "`Multi`'s trailing interface"),
2038                (Csharp::TypeArgumentList as u16, 1, "`G`'s generic base"),
2039            ],
2040        );
2041        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
2042            for name in ["Sub", "R1", "R2", "G", "Multi"] {
2043                assert_eq!(
2044                    child_space(&space, name).metrics.abc.branches(),
2045                    1,
2046                    "{name}: the primary-constructor base call is one branch"
2047                );
2048            }
2049            for name in ["S", "R3"] {
2050                assert_eq!(
2051                    child_space(&space, name).metrics.abc.branches(),
2052                    0,
2053                    "{name}: a base type with no argument list is not a call"
2054                );
2055            }
2056        });
2057    }
2058
2059    #[cfg(feature = "csharp")]
2060    #[test]
2061    fn csharp_base_list_gate_excludes_other_argument_lists() {
2062        // The `base_list` parent gate on the `ArgumentList` arm is
2063        // load-bearing, not decoration: ungated it bills a branch for the
2064        // argument list of every call and allocation in the file, on top
2065        // of the `InvocationExpression` / `ObjectCreationExpression` the
2066        // arm already counts — doubling both (#1406).
2067        //
2068        // The fixture pairs one real base call with the four other
2069        // argument-carrying productions C# spells nearby: an attribute
2070        // (`attribute_argument_list`), an invocation, an object creation,
2071        // and an indexer (`bracketed_argument_list`) alongside a generic
2072        // (`type_argument_list`). Those last three are distinct kind ids
2073        // rather than `argument_list`, so they pin the §1 neighbour set
2074        // as much as the gate.
2075        let src = "[System.Obsolete(\"why\")]
2076                   class A(int x) : Base(x) {
2077                       void M(int y) { Helper(y); }
2078                       object N() { return new Foo(1); }
2079                       int P(System.Collections.Generic.List<int> l, int i) { return l[i]; }
2080                   }";
2081        assert_csharp_fixture_spells(
2082            src,
2083            &[
2084                (
2085                    Csharp::AttributeArgumentList as u16,
2086                    1,
2087                    "attribute argument lists",
2088                ),
2089                (
2090                    Csharp::BracketedArgumentList as u16,
2091                    1,
2092                    "bracketed argument lists",
2093                ),
2094                (Csharp::TypeArgumentList as u16, 1, "type argument lists"),
2095                (Csharp::ArgumentList as u16, 3, "argument lists"),
2096            ],
2097        );
2098        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
2099            let class = child_space(&space, "A");
2100            assert_eq!(
2101                class.metrics.abc.branches(),
2102                1,
2103                "`: Base(x)` alone — the attribute's arguments are not a call"
2104            );
2105            // The controls. 2 apiece would mean the gate let the
2106            // argument list through beside the call that owns it.
2107            assert_eq!(
2108                child_space(class, "M").metrics.abc.branches(),
2109                1,
2110                "`Helper(y)` is one branch, not one per argument list"
2111            );
2112            assert_eq!(
2113                child_space(class, "N").metrics.abc.branches(),
2114                1,
2115                "`new Foo(1)` is one branch, not one per argument list"
2116            );
2117            // `P` carries the indexer and the generic deliberately, but
2118            // gets no assertion: `bracketed_argument_list` and
2119            // `type_argument_list` are distinct kind ids, so `P` reads 0
2120            // with the gate, without it, and without this change
2121            // altogether. The census above is what actually pins them —
2122            // an assertion here would read as coverage it cannot provide.
2123        });
2124    }
2125
2126    #[cfg(feature = "csharp")]
2127    #[test]
2128    fn csharp_base_call_and_constructor_initializer_do_not_double_count() {
2129        // A class can spell *both* delegations at once: the primary
2130        // constructor's `: Base(x)` and a secondary constructor's
2131        // `: this(a)`. They are separate nodes in separate spaces, so the
2132        // file scores 2 — one each. 3 would mean the #1406 arm also fired
2133        // on something `ConstructorInitializer` already owned; 1 would
2134        // mean one of the two stopped counting.
2135        //
2136        // Java has the equivalent guard
2137        // (`java_constructor_delegation_does_not_double_count_arguments`);
2138        // C# now has two delegation spellings and this is the pairing
2139        // test for them.
2140        let src = "class Both(int x) : Base(x) {
2141                       public Both(int a, int b) : this(a) { }
2142                   }";
2143        assert_csharp_fixture_spells(
2144            src,
2145            &[
2146                (Csharp::ConstructorInitializer as u16, 1, "`: this(a)`"),
2147                (Csharp::BaseList2 as u16, 1, "the primary-constructor base"),
2148            ],
2149        );
2150        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
2151            let class = child_space(&space, "Both");
2152            assert_eq!(
2153                class.metrics.abc.branches(),
2154                1,
2155                "the class space owns `: Base(x)` and nothing else"
2156            );
2157            assert_eq!(
2158                child_space(class, "Both").metrics.abc.branches(),
2159                1,
2160                "the secondary constructor owns `: this(a)` and nothing else"
2161            );
2162        });
2163    }
2164
2165    #[cfg(feature = "kotlin")]
2166    #[test]
2167    fn kotlin_constructor_delegation_is_a_branch() {
2168        // Kotlin's secondary-constructor delegation is a
2169        // `constructor_delegation_call`, a distinct production from
2170        // `CallExpression`, so it scored zero for the same reason (#1279).
2171        // expected: 1 branch — the `: super(x)` delegation.
2172        check_metrics::<KotlinParser>(
2173            "class Sub : Base {
2174                constructor(x: Int) : super(x) { }
2175            }",
2176            "foo.kt",
2177            |metric| {
2178                assert_eq!(metric.abc.branches_sum(), 1);
2179            },
2180        );
2181    }
2182
2183    #[cfg(feature = "kotlin")]
2184    #[test]
2185    fn kotlin_primary_constructor_superclass_call_is_a_branch() {
2186        // Kotlin's *primary*-constructor superclass call is a
2187        // `constructor_invocation` under a `delegation_specifier` — a third
2188        // production, distinct from both `CallExpression` and the
2189        // `ConstructorDelegationCall` #1279 added, so it scored zero while
2190        // the secondary form beside it scored one (#1384). An object
2191        // expression's superclass call uses the same production.
2192        //
2193        // `class Plain : Marker` is the negative case: a delegation
2194        // specifier with no argument list is a plain `user_type`, so it
2195        // must stay at zero. Broadening the arm to bare
2196        // `DelegationSpecifier` fails here at `branches_sum` (5 against
2197        // 3), though not on `Plain`'s account alone: `class Classic :
2198        // Base` below is argument-less too, so deleting `Plain` still
2199        // leaves the broadening failing, at 4. What `Plain` buys is the
2200        // case stated outright rather than riding on the contrast fixture,
2201        // and since the per-space assertions below name it, trimming it
2202        // from the fixture fails in `child_space` instead of passing
2203        // silently as it did while this test read only the sum.
2204        //
2205        // `class Classic` carries the secondary spelling of the same call
2206        // alongside, which is what makes the per-space assertions below a
2207        // contrast rather than a census: a class header's superclass call
2208        // sits in `delegation_specifiers`, outside every member, so no
2209        // function space encloses it and the branch is the **class**
2210        // space's, while `constructor(x: Int) : super(x)` opens a function
2211        // space and takes the branch with it. `branches_sum` cannot see the
2212        // difference — the file totals 3 either way — so a codebase moving
2213        // to primary constructors shifts per-function `branches_max` /
2214        // `branches_average` and per-space `abc` thresholds without any
2215        // behaviour change. That is deliberate (there is no function to
2216        // attribute the call to), and C# has the identical split, pinned by
2217        // `csharp_base_call_and_constructor_initializer_do_not_double_count`
2218        // (#1456).
2219        //
2220        // expected: 3 branches — `Base(1, 2)`, `Base(3)` and `super(x)`.
2221        // Nothing else in the fixture is a call, so deleting any one
2222        // construction from the fixture moves both the total and the space
2223        // it belongs to.
2224        check_func_space::<KotlinParser, _>(
2225            "class Sub : Base(1, 2) { }
2226             class Plain : Marker { }
2227             fun make(): Any = object : Base(3) { }
2228             class Classic : Base {
2229                 constructor(x: Int) : super(x) { }
2230             }",
2231            "foo.kt",
2232            |space| {
2233                assert_eq!(space.metrics.abc.branches_sum(), 3);
2234                assert_eq!(
2235                    child_space(&space, "Sub").metrics.abc.branches(),
2236                    1,
2237                    "the primary-constructor superclass call is the class space's"
2238                );
2239                assert_eq!(
2240                    child_space(&space, "Plain").metrics.abc.branches(),
2241                    0,
2242                    "a delegation specifier with no argument list is not a call"
2243                );
2244                // The object expression is its own Class space (#463), so
2245                // `make` holds nothing of its own — the same attribution
2246                // one level down.
2247                let make = child_space(&space, "make");
2248                assert_eq!(
2249                    make.metrics.abc.branches(),
2250                    0,
2251                    "`make` owns no branch; `object : Base(3)` is its own space"
2252                );
2253                assert_eq!(
2254                    child_space(make, "<anonymous>").metrics.abc.branches(),
2255                    1,
2256                    "the object expression's superclass call is that space's"
2257                );
2258                // The contrast. `Classic` spells the same call through a
2259                // secondary constructor, which *does* open a function
2260                // space, so the class owns nothing.
2261                let classic = child_space(&space, "Classic");
2262                assert_eq!(
2263                    classic.metrics.abc.branches(),
2264                    0,
2265                    "a secondary constructor's `: super(x)` is not the class's"
2266                );
2267                assert_eq!(
2268                    child_space(classic, "<anonymous>").metrics.abc.branches(),
2269                    1,
2270                    "it belongs to the constructor's own function space"
2271                );
2272            },
2273        );
2274    }
2275
2276    #[cfg(feature = "kotlin")]
2277    #[test]
2278    fn kotlin_annotation_arguments_are_not_a_branch() {
2279        // The parent gate on that arm is load-bearing, not decoration:
2280        // tree-sitter-kotlin-ng spells an annotation's argument list with
2281        // the *same* `constructor_invocation` production — `@Mark("a")`
2282        // parses as `annotation > constructor_invocation` and
2283        // `@file:Suppress(…)` as `file_annotation > constructor_invocation`.
2284        // Ungated, every argument-carrying annotation in a Kotlin file
2285        // would bill a branch, and annotations are everywhere (#1384).
2286        //
2287        // The fixture pairs three annotation spellings — file-level,
2288        // class-level and use-site-targeted — with one real superclass
2289        // call, so the total discriminates in both directions: 1 with the
2290        // gate, 4 without it, 0 without the arm at all.
2291        //
2292        // A correctly-excluded annotation contributes to no ABC axis, so
2293        // `branches_sum()` alone cannot notice the annotations being
2294        // trimmed out of the fixture — measured: dropping any of the three
2295        // leaves this test green, and with all three gone, removing the
2296        // production gate fails nothing. The node census below is the
2297        // anchor: it pins that the fixture really does hand the arm four
2298        // `constructor_invocation` nodes for it to score 1 out of, and
2299        // that the `file_annotation` parent — the spelling a denylist of
2300        // `Annotation` alone would have missed — is among them.
2301        //
2302        // expected: 1 branch — `Base(1)` only.
2303        let src = r#"@file:Suppress("unused")
2304             @Mark("a")
2305             class Ann : Base(1) {
2306                 @get:Mark("b")
2307                 val v: Int = 0
2308             }"#;
2309        let parser = KotlinParser::new(
2310            src.as_bytes().to_vec(),
2311            &std::path::PathBuf::from("foo.kt"),
2312            None,
2313        );
2314        assert_eq!(
2315            parser
2316                .root()
2317                .preorder()
2318                .filter(|n| n.kind_id() == Kotlin::ConstructorInvocation as u16)
2319                .count(),
2320            4,
2321            "fixture must keep three annotations plus the superclass call"
2322        );
2323        assert!(ast_has_kind_id(&parser, Kotlin::FileAnnotation as u16));
2324
2325        check_metrics::<KotlinParser>(src, "foo.kt", |metric| {
2326            assert_eq!(metric.abc.branches_sum(), 1);
2327        });
2328    }
2329
2330    #[cfg(feature = "groovy")]
2331    #[test]
2332    fn groovy_constructor_delegation_is_a_branch() {
2333        // Groovy already counted this shape before #1279; the assertion
2334        // pins the JVM-family parity the Java and Kotlin fixes restore.
2335        // expected: 1 branch — the `super(1)` delegation.
2336        check_metrics::<GroovyParser>(
2337            "class Sub extends Base {
2338                Sub() { super(1) }
2339            }",
2340            "foo.groovy",
2341            |metric| {
2342                assert_eq!(metric.abc.branches_sum(), 1);
2343            },
2344        );
2345    }
2346
2347    // The three tests below are one fix seen from three grammars (#1407):
2348    // an enum constant carrying constructor arguments is an object
2349    // construction and scored zero in every JVM-family language, which
2350    // #1279 and #1384 left behind because both looked at superclass
2351    // delegation only.
2352    //
2353    // Each fixture pairs an argument-carrying constant with a bare one in
2354    // the *same* enum, because that is the only shape that shows the gate
2355    // working: a fixture of only `A(1)` scores the same whether the arm is
2356    // gated on the argument list or matches every constant
2357    // (`.claude/rules/grammar-dispatch.md` §6). Each also carries a
2358    // constant whose argument is itself a call, which must score 2 rather
2359    // than 1 (§5).
2360    //
2361    // `branches_sum()` alone cannot notice the bare constant being trimmed
2362    // out — it contributes to no axis — so every fixture is anchored on a
2363    // node census that fails by name when a spelling is edited away
2364    // (`.claude/rules/testing.md`, "Perturb the fixture as well as the
2365    // production line"). The two are coupled in one helper for the same
2366    // reason `assert_kotlin_class_members` couples its pair: a branch
2367    // total added without the census reads as coverage and is not.
2368
2369    #[cfg(any(feature = "groovy", feature = "java", feature = "kotlin"))]
2370    fn assert_enum_branches<P: MetricSuite>(
2371        src: &str,
2372        path: &str,
2373        kinds: &[(u16, usize, &str)],
2374        branches: u64,
2375    ) {
2376        assert_fixture_spells::<P>(src, path, kinds);
2377        check_func_space::<P, _>(src, path, |space| {
2378            assert_eq!(space.metrics.abc.branches_sum(), branches);
2379        });
2380    }
2381
2382    #[cfg(feature = "java")]
2383    #[test]
2384    fn java_enum_constant_with_arguments_is_a_branch() {
2385        // `@SuppressWarnings("x") B` is the discriminating case for the
2386        // gate's *kind*, not just its presence: the annotation carries an
2387        // argument list of its own, so an arm gated on "has some argument
2388        // list anywhere below" would score it. It cannot satisfy this one
2389        // twice over — `annotation_argument_list` is a distinct kind id
2390        // from `argument_list`, and it hangs off the constant's `modifiers`
2391        // child rather than the constant itself. Both verified with `bca
2392        // dump`.
2393        //
2394        // expected: 4 branches — `A(1)`, `K(2)`, `D(…)`, and the `f()`
2395        // inside `D`'s argument list. `B` scores 0.
2396        let src = "class Outer {
2397                static int f() { return 1; }
2398                enum E {
2399                    A(1),
2400                    @SuppressWarnings(\"x\") B,
2401                    K(2),
2402                    D(f());
2403                    E(int v) {}
2404                }
2405            }";
2406        assert_enum_branches::<JavaParser>(
2407            src,
2408            "foo.java",
2409            &[
2410                (Java::EnumConstant as u16, 4, "enum constants"),
2411                (Java::ArgumentList as u16, 4, "argument lists"),
2412                (
2413                    Java::AnnotationArgumentList as u16,
2414                    1,
2415                    "`@SuppressWarnings`'s argument list",
2416                ),
2417                (Java::MethodInvocation as u16, 1, "`D`'s nested `f()` call"),
2418            ],
2419            4,
2420        );
2421    }
2422
2423    #[cfg(feature = "kotlin")]
2424    #[test]
2425    fn kotlin_enum_entry_with_arguments_is_a_branch() {
2426        // Kotlin needs a defaulted primary-constructor parameter to spell
2427        // both cases in one enum: `enum class E(val v: Int)` obliges every
2428        // entry to pass an argument, so `= 0` is what lets the bare `B`
2429        // sit beside `A(1)`. `enum class P` covers the other shape the gate
2430        // has to leave alone — an enum with no constructor at all, whose
2431        // entries can never carry an argument list.
2432        //
2433        // expected: 3 branches — `A(1)`, `C(…)`, and the `g()` inside `C`'s
2434        // argument list. `B`, `X` and `Y` score 0.
2435        let src = "fun g(): Int = 1
2436             enum class E(val v: Int = 0) { A(1), B, C(g()) }
2437             enum class P { X, Y }";
2438        assert_enum_branches::<KotlinParser>(
2439            src,
2440            "foo.kt",
2441            &[
2442                (Kotlin::EnumEntry as u16, 5, "enum entries"),
2443                (Kotlin::ValueArguments as u16, 3, "argument lists"),
2444                (Kotlin::CallExpression as u16, 1, "`C`'s nested `g()` call"),
2445            ],
2446            3,
2447        );
2448    }
2449
2450    #[cfg(feature = "groovy")]
2451    #[test]
2452    fn groovy_enum_constant_with_arguments_is_a_branch() {
2453        // Groovy's enum has no annotated-constant case to cover: the
2454        // dekobon grammar cannot parse `@Deprecated A(1)` inside an enum
2455        // body and recovers into `ERROR` nodes, emitting no `enum_constant`
2456        // at all. The remaining two cases are the ones that matter.
2457        //
2458        // expected: 3 branches — `A(1)`, `D(…)`, and the `Outer.f()` inside
2459        // `D`'s argument list. `B` scores 0.
2460        let src = "class Outer { static int f() { 1 } }
2461             enum E { A(1), B, D(Outer.f()) }";
2462        assert_enum_branches::<GroovyParser>(
2463            src,
2464            "foo.groovy",
2465            &[
2466                (Groovy::EnumConstant as u16, 3, "enum constants"),
2467                (Groovy::ArgumentList as u16, 3, "argument lists"),
2468                (
2469                    Groovy::MethodInvocation as u16,
2470                    1,
2471                    "`D`'s nested `Outer.f()` call",
2472                ),
2473            ],
2474            3,
2475        );
2476    }
2477
2478    #[cfg(feature = "groovy")]
2479    #[test]
2480    fn groovy_no_abc() {
2481        // Comment-only file has no executable code → all-zero ABC.
2482        check_metrics::<GroovyParser>(
2483            "// just a comment, no executable code",
2484            "foo.groovy",
2485            |metric| {
2486                assert_eq!(metric.abc.assignments_sum(), 0);
2487                assert_eq!(metric.abc.branches_sum(), 0);
2488                assert_eq!(metric.abc.conditions_sum(), 0);
2489            },
2490        );
2491    }
2492
2493    #[cfg(feature = "groovy")]
2494    #[test]
2495    fn groovy_single_assignment() {
2496        // `int x = 1` is a local-variable declaration whose `=` counts
2497        // as one assignment (matches Java's semantics).
2498        check_metrics::<GroovyParser>("int x = 1", "foo.groovy", |metric| {
2499            assert_eq!(metric.abc.assignments_sum(), 1);
2500            assert_eq!(metric.abc.branches_sum(), 0);
2501            assert_eq!(metric.abc.conditions_sum(), 0);
2502        });
2503    }
2504
2505    #[cfg(feature = "groovy")]
2506    #[test]
2507    fn groovy_assignments() {
2508        check_metrics::<GroovyParser>(
2509            "void f() {
2510                int a = 1
2511                int b = 2
2512                a = 3
2513                b = 4
2514                a += 1
2515                b -= 1
2516            }",
2517            "foo.groovy",
2518            |metric| {
2519                // Six `=` tokens total. The two `Final`-less local
2520                // var-decls (`int a = 1`, `int b = 2`) and the two
2521                // bare assignments (`a = 3`, `b = 4`) each contribute
2522                // one assignment via the `EQ` arm; the `+=` / `-=`
2523                // each contribute one via the compound-assign arm.
2524                assert_eq!(metric.abc.assignments_sum(), 6);
2525            },
2526        );
2527    }
2528
2529    #[cfg(feature = "groovy")]
2530    #[test]
2531    fn groovy_branches() {
2532        check_metrics::<GroovyParser>(
2533            "void f() {
2534                doStuff()
2535                helper.invoke()
2536                new Worker()
2537            }",
2538            "foo.groovy",
2539            |metric| {
2540                // 2 method invocations + 1 object creation = 3 branches
2541                assert_eq!(metric.abc.branches_sum(), 3);
2542            },
2543        );
2544    }
2545
2546    #[cfg(feature = "groovy")]
2547    #[test]
2548    fn groovy_conditions_in_if() {
2549        check_metrics::<GroovyParser>(
2550            "void f(int a) {
2551                if (a == 0) { println(a) }
2552                if (a >= 1) { println(a) }
2553                if (a != 2) { println(a) }
2554            }",
2555            "foo.groovy",
2556            |metric| {
2557                // Three relational ops = 3 conditions
2558                assert_eq!(metric.abc.conditions_sum(), 3);
2559            },
2560        );
2561    }
2562
2563    #[cfg(feature = "groovy")]
2564    #[test]
2565    fn groovy_branches_with_juxt_call() {
2566        // Groovy's parens-less call form `println foo` must be counted
2567        // as a branch (`JuxtFunctionCall`).
2568        check_metrics::<GroovyParser>(
2569            "void f() {
2570                println 'hi'
2571                println 'bye'
2572            }",
2573            "foo.groovy",
2574            |metric| {
2575                // 2 juxt calls = 2 branches.
2576                assert_eq!(metric.abc.branches_sum(), 2);
2577            },
2578        );
2579    }
2580
2581    #[cfg(feature = "groovy")]
2582    #[test]
2583    fn groovy_try_catch_conditions() {
2584        // Each `try` and `catch` keyword token contributes +1 to
2585        // conditions (mirrors Java).
2586        check_metrics::<GroovyParser>(
2587            "void f() {
2588                try {
2589                    risky()
2590                } catch (Exception e) {
2591                    handle(e)
2592                }
2593            }",
2594            "foo.groovy",
2595            |metric| {
2596                // try + catch = 2 conditions
2597                assert_eq!(metric.abc.conditions_sum(), 2);
2598            },
2599        );
2600    }
2601
2602    #[cfg(feature = "groovy")]
2603    #[test]
2604    fn groovy_ternary_conditions() {
2605        check_metrics::<GroovyParser>(
2606            "void f(int x) {
2607                def y = x > 0 ? 1 : 2
2608            }",
2609            "foo.groovy",
2610            |metric| {
2611                // QMARK alone is +1 condition, plus the `>` condition = 2.
2612                assert_eq!(metric.abc.conditions_sum(), 2);
2613            },
2614        );
2615    }
2616
2617    #[cfg(feature = "groovy")]
2618    #[test]
2619    fn groovy_constant_excluded_from_assignments() {
2620        // `final` declarations are not counted as assignments
2621        // (mirrors Java's `Final` handling).
2622        check_metrics::<GroovyParser>(
2623            "class A {
2624                final int CONST = 42
2625                int field = 0
2626            }",
2627            "foo.groovy",
2628            |metric| {
2629                // The `=` on `final int CONST = 42` is a constant
2630                // initialiser (skipped). Only `field = 0` counts.
2631                assert_eq!(metric.abc.assignments_sum(), 1);
2632            },
2633        );
2634    }
2635
2636    #[cfg(feature = "groovy")]
2637    #[test]
2638    fn groovy_malformed_parenthesized_no_panic() {
2639        // Regression: malformed Groovy input must not panic the ABC
2640        // walker; the `spaces.rs` Unit fallback (lesson 9) covers
2641        // structural recovery. amaanq's grammar treats `def x = (((`
2642        // as a `local_variable_declaration` whose initialiser is the
2643        // first opening paren — the `=` still fires the assignment
2644        // arm.
2645        check_metrics::<GroovyParser>("def x = (((", "foo.groovy", |metric| {
2646            assert_eq!(metric.abc.assignments_sum(), 1);
2647        });
2648    }
2649
2650    #[cfg(feature = "groovy")]
2651    #[test]
2652    fn groovy_bool_returning_terminal_kinds_count() {
2653        // Companion to `csharp_bool_returning_terminal_kinds_count`
2654        // (issue #372 / lesson #19). The dekobon Groovy grammar
2655        // shares Java's wrapping conventions for `FieldAccess` and
2656        // `InstanceofExpression`, but it splits casts into two
2657        // distinct kinds — `cast_expression` for the Groovy-idiomatic
2658        // `v as Boolean` and `parenthesized_type_cast` for the
2659        // Java-style `(boolean) v`. The grammar has no `await` or
2660        // `array_access` analogues, so the C# fix's five-kind set
2661        // collapses to four here (with the cast slot doubled).
2662        //
2663        // `instanceof` has since moved out of the terminal set to an
2664        // unconditional arm (#1461), so it no longer depends on the
2665        // slot to be counted. The total is unchanged, which is the
2666        // point of that change rather than an accident of it.
2667        //
2668        // expected: 4 conditions (one per `if`), 0 assignments,
2669        // 0 branches (no invocations).
2670        check_metrics::<GroovyParser>(
2671            "class Cfg { boolean flag }
2672            class A {
2673                void m(Object v, Cfg cfg) {
2674                    if (cfg.flag) { }
2675                    if ((boolean) v) { }
2676                    if (v as Boolean) { }
2677                    if (v instanceof Cfg) { }
2678                }
2679            }",
2680            "foo.groovy",
2681            |metric| {
2682                assert_eq!(metric.abc.conditions_sum(), 4);
2683                assert_eq!(metric.abc.assignments_sum(), 0);
2684                assert_eq!(metric.abc.branches_sum(), 0);
2685            },
2686        );
2687    }
2688
2689    // Groovy half of #1274 — see `java_generic_declarations_are_not_conditions`.
2690    // The class-level `type_parameters` and its bound's nested
2691    // `type_arguments` mirror the Java fixture; pre-fix this file scored
2692    // 2 conditions, expected 0.
2693    #[cfg(feature = "groovy")]
2694    #[test]
2695    fn groovy_generic_declarations_are_not_conditions() {
2696        check_metrics::<GroovyParser>(
2697            "class Gen<T extends Comparable<T>> {
2698                List<T> pick(Map<String, List<T>> m) { m.get(\"k\") }
2699            }",
2700            "foo.groovy",
2701            |metric| {
2702                assert_eq!(metric.abc.conditions_sum(), 0);
2703                // Non-vacuity guard — see the Java counterpart.
2704                assert_eq!(metric.abc.branches_sum(), 1);
2705            },
2706        );
2707    }
2708
2709    // Groovy counterpart of `java_generic_wildcard_is_not_a_condition`,
2710    // including its choice of a non-zero expected total and its
2711    // two-wildcards-one-ternary shape; the dekobon grammar emits the
2712    // same `wildcard` node. Pre-fix: 4.
2713    #[cfg(feature = "groovy")]
2714    #[test]
2715    fn groovy_generic_wildcard_is_not_a_condition() {
2716        check_metrics::<GroovyParser>(
2717            "class A {
2718                int m(List<? extends Number> xs, Map<String, ? extends Number> ys,
2719                      int a, int b) { a < b ? 1 : 2 }
2720            }",
2721            "foo.groovy",
2722            |metric| {
2723                assert_eq!(metric.abc.conditions_sum(), 2);
2724            },
2725        );
2726    }
2727
2728    // A generic *method* gets its own fixture because the dekobon
2729    // grammar gives it its own production: `def <U> U ident(U x)` emits
2730    // `method_type_parameters`, not the `type_parameters` the class form
2731    // above uses. It is the fourth and last production from which that
2732    // grammar emits a bare `<` / `>` (the others being
2733    // `binary_expression`, `type_arguments` and `type_parameters`), and
2734    // the one a denylist extended only with `TypeParameters` — the fix
2735    // this issue's own plan proposed — would still miss. Revert-verified
2736    // in both directions: under that denylist this test still fails
2737    // while `groovy_generic_declarations_are_not_conditions` passes.
2738    //
2739    // The body carries a ternary over a comparison for the same
2740    // non-vacuity reason as the wildcard tests: expected 2, pre-fix 4
2741    // (the `<U>` bracket pair), 0 if the fixture stops parsing.
2742    #[cfg(feature = "groovy")]
2743    #[test]
2744    fn groovy_method_type_parameters_are_not_conditions() {
2745        check_metrics::<GroovyParser>(
2746            "class A {
2747                def <U> U ident(U x, int a, int b) { a < b ? x : null }
2748            }",
2749            "foo.groovy",
2750            |metric| {
2751                assert_eq!(metric.abc.conditions_sum(), 2);
2752            },
2753        );
2754    }
2755
2756    // Groovy counterpart of
2757    // `java_comparison_operators_still_count_alongside_generics`: the
2758    // narrowed arm keeps counting real comparisons, pinned against the
2759    // cyclomatic decision count on the method's own space (§8).
2760    #[cfg(feature = "groovy")]
2761    #[test]
2762    fn groovy_elvis_counts_one_condition_per_token() {
2763        // `a ?: c` is a short-circuit decision Groovy cyclomatic already
2764        // counts and the Kotlin ABC arm counts for its identical token;
2765        // Groovy's arm listed neither `?:` nor its `elvis_expression`, so
2766        // a method whose only branching is elvis chains reported
2767        // cyclomatic > 1 with zero conditions. One per token, so the
2768        // grammar-dispatch §8 invariant holds on the chain:
2769        // `conditions() == cyclomatic() - 1`.
2770        check_func_space::<GroovyParser, _>(
2771            "class K { def f(a, c) { return a ?: c } }",
2772            "foo.groovy",
2773            |space| assert_deepest_conditions_match_cyclomatic(&space, 1),
2774        );
2775        check_func_space::<GroovyParser, _>(
2776            "class K { def g(a, b, c) { return a ?: b ?: c } }",
2777            "foo.groovy",
2778            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
2779        );
2780    }
2781
2782    #[cfg(feature = "groovy")]
2783    #[test]
2784    fn groovy_comparison_operators_still_count_alongside_generics() {
2785        check_func_space::<GroovyParser, _>(
2786            "class A {
2787                int m(List<String> xs, int a, int b) {
2788                    if (a < b) { return 1 }
2789                    if (a > b) { return 2 }
2790                    return 0
2791                }
2792            }",
2793            "foo.groovy",
2794            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
2795        );
2796    }
2797
2798    #[cfg(feature = "groovy")]
2799    #[test]
2800    fn groovy_if_multiple_conditions() {
2801        // Mirrors `java_if_multiple_conditions`: `&&` / `||` chains
2802        // and parenthesised unary forms each contribute one
2803        // condition per primitive comparison; the inspect-container
2804        // pass picks up the unary `!a` / `!b` arguments inside the
2805        // `BinaryExpression` and counts them too.
2806        check_metrics::<GroovyParser>(
2807            "void f(boolean a, boolean b, boolean c) {
2808                if (a || b || c) { println(a) }
2809                if (a && b && c) { println(a) }
2810                if (!a && !b) { println(a) }
2811            }",
2812            "foo.groovy",
2813            |metric| {
2814                // Conditions counted via the AMPAMP/PIPEPIPE arms
2815                // (one count per identifier in the chain — three
2816                // for `||`, three for `&&`, two for the unary chain)
2817                // = 8.
2818                assert_eq!(metric.abc.conditions_sum(), 8);
2819                // Three `println a` juxt calls — each is a branch.
2820                assert_eq!(metric.abc.branches_sum(), 3);
2821            },
2822        );
2823    }
2824
2825    #[cfg(feature = "groovy")]
2826    #[test]
2827    fn groovy_while_and_do_while_conditions() {
2828        // Covers the WhileStatement and DoStatement arms in
2829        // `impl Abc for GroovyCode`. Each `while` / `do-while` has
2830        // its condition scored through `groovy_count_condition`.
2831        check_metrics::<GroovyParser>(
2832            "void f(boolean a, boolean b) {
2833                while (a) {
2834                    a = false
2835                }
2836                do {
2837                    b = !b
2838                } while (b)
2839            }",
2840            "foo.groovy",
2841            |metric| {
2842                // `while(a)` + `while(b)` each contribute one condition;
2843                // the unary `!b` on the do body's right-hand side adds
2844                // one more via the assignment-arm inspection = 3.
2845                assert_eq!(metric.abc.conditions_sum(), 3);
2846                // Two assignments to existing variables (`a = false`,
2847                // `b = !b`).
2848                assert_eq!(metric.abc.assignments_sum(), 2);
2849            },
2850        );
2851    }
2852
2853    #[cfg(feature = "groovy")]
2854    #[test]
2855    fn groovy_if_while_boolean_literal_condition() {
2856        // Regression for the Groovy half of #371-class bugs: the
2857        // dekobon tree-sitter-groovy grammar wraps a bare
2858        // `true` / `false` literal used as the condition of
2859        // `if` / `while` / `do` / `?:` in a `boolean_literal` node
2860        // (`Groovy::BooleanLiteral`, kind_id 270), not the leaf
2861        // `True` / `False` keyword tokens. `groovy_count_condition`
2862        // must therefore match `BooleanLiteral` (the wrapper).
2863        // Without that, every literal-condition statement silently
2864        // scored 0 conditions. Mirror of
2865        // `csharp_if_while_boolean_literal_condition`.
2866        check_metrics::<GroovyParser>(
2867            "void m() {
2868                if (true) { println 'a' }
2869                if (false) { println 'b' }
2870                while (true) { break }
2871                int t = true ? 1 : 0
2872            }",
2873            "foo.groovy",
2874            |metric| {
2875                // Four literal-condition statements contribute 4
2876                // `BooleanLiteral` conditions (if / if / while /
2877                // ternary), plus the ternary's `?` token adds one
2878                // more via `groovy_count_token_condition` → 5
2879                // total. The `println` calls contribute 2 branches
2880                // (the `while` body's `break` is not a branch).
2881                // The `int t = …` initializer contributes 1
2882                // assignment.
2883                assert_eq!(metric.abc.conditions_sum(), 5);
2884                assert_eq!(metric.abc.branches_sum(), 2);
2885                assert_eq!(metric.abc.assignments_sum(), 1);
2886            },
2887        );
2888    }
2889
2890    #[cfg(feature = "groovy")]
2891    #[test]
2892    fn groovy_return_unary_boolean_literal() {
2893        // Companion to `groovy_if_while_boolean_literal_condition`:
2894        // a `!true` / `!false` operand inside a `return` statement
2895        // routes through `groovy_count_negated`. The `!` proves
2896        // boolean context, and a `BooleanLiteral` does not score
2897        // itself (`groovy_condition_scores_itself`), so the
2898        // negated operand pays one condition.
2899        check_metrics::<GroovyParser>(
2900            "boolean f() {
2901                return !true
2902            }
2903            boolean g() {
2904                return !false
2905            }",
2906            "foo.groovy",
2907            |metric| {
2908                // Each `return !X` walks into `groovy_count_negated`
2909                // with a UnaryExpression wrapping a `BANG` +
2910                // BooleanLiteral. The peel through `!` proves boolean
2911                // context (a `return` is not a slot), then the
2912                // BooleanLiteral operand contributes one condition.
2913                // Two `return !X` → 2 conditions, no branches, no
2914                // assignments.
2915                assert_eq!(metric.abc.conditions_sum(), 2);
2916                assert_eq!(metric.abc.branches_sum(), 0);
2917                assert_eq!(metric.abc.assignments_sum(), 0);
2918            },
2919        );
2920    }
2921
2922    #[cfg(feature = "groovy")]
2923    #[test]
2924    fn groovy_short_circuit_with_boolean_literal_operand() {
2925        // Companion to `groovy_if_while_boolean_literal_condition`:
2926        // a bare `true` / `false` operand of `&&` / `||` is a
2927        // chain-operand slot scored through `groovy_count_condition`.
2928        // A `BooleanLiteral` does not score itself, so the slot pays
2929        // for it; otherwise the operand would silently score zero.
2930        check_metrics::<GroovyParser>(
2931            "void m(boolean x) {
2932                if (x && true) { println 'a' }
2933                if (false || x) { println 'b' }
2934            }",
2935            "foo.groovy",
2936            |metric| {
2937                // `&&` and `||` themselves are NOT in
2938                // `groovy_count_token_condition`'s match list —
2939                // they route through
2940                // `groovy_walk_for_conditions::AMPAMP|PIPEPIPE`,
2941                // which scores each operand of the parent
2942                // BinaryExpression through `groovy_count_condition`.
2943                // An operand pays one condition unless it scores
2944                // itself. For `x && true`: Identifier x
2945                // (+1) + BooleanLiteral true (+1) = 2. For
2946                // `false || x`: BooleanLiteral false (+1) +
2947                // Identifier x (+1) = 2. Total 4.
2948                assert_eq!(metric.abc.conditions_sum(), 4);
2949                assert_eq!(metric.abc.branches_sum(), 2);
2950                assert_eq!(metric.abc.assignments_sum(), 0);
2951            },
2952        );
2953    }
2954
2955    #[cfg(feature = "groovy")]
2956    #[test]
2957    fn groovy_methods_arguments_with_conditions() {
2958        // Mirror of `java_methods_arguments_with_conditions`: a
2959        // unary `!x` inside an argument list must count both the
2960        // method invocation as a branch AND the unary as a
2961        // condition. The `ArgumentList | ArgumentList2` arm in
2962        // `impl Abc for GroovyCode` is what exercises this.
2963        check_metrics::<GroovyParser>(
2964            "void f(boolean a, boolean b, boolean c) {
2965                m1(a)
2966                m1(!a)
2967                m2(!a, !b)
2968            }",
2969            "foo.groovy",
2970            |metric| {
2971                // 3 method invocations (m1, m1, m2) — each fires the
2972                // branches arm.
2973                assert_eq!(metric.abc.branches_sum(), 3);
2974                // Three `!` unaries — `m1(!a)` and the two args of
2975                // `m2(!a, !b)` — each contribute one condition via
2976                // the ArgumentList inspection.
2977                assert_eq!(metric.abc.conditions_sum(), 3);
2978            },
2979        );
2980    }
2981
2982    #[cfg(feature = "groovy")]
2983    #[test]
2984    fn groovy_return_with_conditions() {
2985        // Mirror of `java_return_with_conditions`: a parenthesised
2986        // or unary expression inside `return` flows through the
2987        // `ReturnStatement` arm to `groovy_count_negated`.
2988        check_metrics::<GroovyParser>(
2989            "boolean f(boolean a) {
2990                return (a)
2991            }
2992            boolean g(boolean a) {
2993                return !a
2994            }",
2995            "foo.groovy",
2996            |metric| {
2997                // Only one of the two return forms surfaces a
2998                // condition: `return !a` hits the UnaryExpression
2999                // path and adds one; `return (a)` reaches
3000                // `groovy_count_negated` but no `!` proves the inner
3001                // identifier `a` boolean, so no condition is added.
3002                assert_eq!(metric.abc.conditions_sum(), 1);
3003            },
3004        );
3005    }
3006
3007    #[cfg(feature = "groovy")]
3008    #[test]
3009    fn groovy_for_with_variable_declaration() {
3010        // Classical `for (int i = 0; cond; i++)` form. The init
3011        // slot's `int i = 0` is suppressed from assignments by the
3012        // `LocalVariableDeclaration` push/pop dance; the `i++` in
3013        // the update slot contributes one assignment via the
3014        // `PLUSPLUS` arm. The condition `i < 10` flows through the
3015        // `ForStatement` arm.
3016        check_metrics::<GroovyParser>(
3017            "void f() {
3018                for (int i = 0; i < 10; i++) {
3019                    println(i)
3020                }
3021            }",
3022            "foo.groovy",
3023            |metric| {
3024                // `int i = 0` fires the EQ arm + `i++` fires the
3025                // PLUSPLUS arm = 2 assignments.
3026                assert_eq!(metric.abc.assignments_sum(), 2);
3027                // `i < 10` is one condition (the LT arm).
3028                assert_eq!(metric.abc.conditions_sum(), 1);
3029            },
3030        );
3031    }
3032
3033    /// The existing `for` test uses `i < 10`, which the `LT` token arm
3034    /// counts on its own — `groovy_walk_for_statement` never
3035    /// contributes there, so it stayed uncovered. A bare-identifier
3036    /// condition has no comparison token, so the count can only come
3037    /// from the walker.
3038    #[cfg(feature = "groovy")]
3039    #[test]
3040    fn groovy_for_with_bare_identifier_condition() {
3041        check_metrics::<GroovyParser>(
3042            "void f(boolean go) {
3043                for (int i = 0; go; i++) {
3044                    println(i)
3045                }
3046            }",
3047            "foo.groovy",
3048            |metric| {
3049                // `go` is the whole condition and counts once.
3050                assert_eq!(metric.abc.conditions_sum(), 1);
3051                // `int i = 0` (EQ) + `i++` (PLUSPLUS) = 2, as in
3052                // `groovy_for_with_variable_declaration`.
3053                assert_eq!(metric.abc.assignments_sum(), 2);
3054            },
3055        );
3056    }
3057
3058    /// The same slot with the initialiser hoisted out of the header.
3059    /// Under the pre-#1276 positional cascade this was a distinct code
3060    /// path — the condition moved from child(4) to child(3) — and the
3061    /// pair is kept as a shape guard now that the walker reads the
3062    /// `condition` field and cannot see the difference.
3063    #[cfg(feature = "groovy")]
3064    #[test]
3065    fn groovy_for_with_empty_initializer_counts_the_condition() {
3066        check_metrics::<GroovyParser>(
3067            "void f(boolean go) {
3068                int i = 0
3069                for (; go; i++) {
3070                    println(i)
3071                }
3072            }",
3073            "foo.groovy",
3074            |metric| {
3075                assert_eq!(metric.abc.conditions_sum(), 1);
3076                // `int i = 0` (EQ) + `i++` (PLUSPLUS), as above — the
3077                // initialiser just moved out of the loop header.
3078                assert_eq!(metric.abc.assignments_sum(), 2);
3079            },
3080        );
3081    }
3082
3083    /// Issue #1276 changed Groovy's answer here, exactly as it changed
3084    /// Java's: the positional cascade counted a `;` / `)` landing at
3085    /// child(4) as a vacuously-true condition, so `for (;;)` scored
3086    /// one. An omitted test is not a decision, and every other impl
3087    /// scores it zero. See `java_empty_for_condition_counts_nothing`.
3088    #[cfg(feature = "groovy")]
3089    #[test]
3090    fn groovy_empty_for_condition_counts_nothing() {
3091        check_metrics::<GroovyParser>("void f() { for (;;) { break } }", "foo.groovy", |metric| {
3092            assert_eq!(metric.abc.conditions_sum(), 0);
3093        });
3094        // The second spelling that moved. Unlike Java's, Groovy's
3095        // `local_variable_declaration` does not swallow its `;`, so the
3096        // old cascade found `;` at child(3) and `;` at child(4) here
3097        // too, and counted one.
3098        check_metrics::<GroovyParser>(
3099            "void f() { for (int i = 0; ; i++) { break } }",
3100            "foo.groovy",
3101            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
3102        );
3103    }
3104
3105    /// The cascade's other defect, shared with Java: a comment in the
3106    /// header shifted every child index, so the condition went unread.
3107    /// Reading the `condition` field cannot shift.
3108    #[cfg(feature = "groovy")]
3109    #[test]
3110    fn groovy_for_condition_survives_a_header_comment() {
3111        check_metrics::<GroovyParser>(
3112            "void f(boolean go) { for (; /* n */ go; ) { break } }",
3113            "foo.groovy",
3114            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
3115        );
3116        // Unchanged control: the same loop without the comment.
3117        check_metrics::<GroovyParser>(
3118            "void f(boolean go) { for (; go; ) { break } }",
3119            "foo.groovy",
3120            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
3121        );
3122    }
3123
3124    /// C#'s `csharp_walk_for_statement` reads the loop condition off
3125    /// the named `condition` field and scores a parenthesised or
3126    /// `!`-prefixed one through `csharp_count_condition`. Every other
3127    /// C# `for` test uses a comparison (`i < n`), which the `LT` token
3128    /// arm counts without entering the walker.
3129    #[cfg(feature = "csharp")]
3130    #[test]
3131    fn csharp_for_with_negated_condition() {
3132        check_metrics::<CsharpParser>(
3133            "class A {
3134                void M(bool done) {
3135                    for (int i = 0; !done; i++) { System.Console.WriteLine(i); }
3136                }
3137            }",
3138            "foo.cs",
3139            |metric| {
3140                // `!done` unwraps to the `done` terminal: one condition,
3141                // and no comparison token to double-count it.
3142                assert_eq!(metric.abc.conditions_sum(), 1);
3143                // `int i = 0` + `i++`.
3144                assert_eq!(metric.abc.assignments_sum(), 2);
3145                assert_eq!(metric.abc.branches_sum(), 1);
3146            },
3147        );
3148    }
3149
3150    #[cfg(feature = "groovy")]
3151    #[test]
3152    fn groovy_eq_arm_counts_outside_final_declarations() {
3153        // Bare reassignment of an already-declared variable: the `=`
3154        // belongs to no `final` declaration, so it counts. Mirrors
3155        // `java_eq_arm_counts_outside_final_declarations`.
3156        check_metrics::<GroovyParser>(
3157            "void f(int x) {
3158                x = 42
3159            }",
3160            "foo.groovy",
3161            |metric| {
3162                assert_eq!(metric.abc.assignments_sum(), 1);
3163                assert_eq!(metric.abc.branches_sum(), 0);
3164                assert_eq!(metric.abc.conditions_sum(), 0);
3165            },
3166        );
3167    }
3168
3169    #[cfg(feature = "groovy")]
3170    #[test]
3171    fn groovy_final_field_initializer_does_not_suppress_the_closure_body() {
3172        // The Groovy spelling of
3173        // `java_final_initializer_does_not_suppress_the_assignments_inside_it`:
3174        // a `final` field's closure body opens no space, and the sentinel
3175        // stack suppressed its `x = 1` with the declarator's own `=`.
3176        check_metrics::<GroovyParser>(
3177            "class K {
3178                int x
3179                final Closure c = { x = 1 }
3180                Closure d = { x = 2 }
3181                final int q = 3
3182            }",
3183            "foo.groovy",
3184            |metric| {
3185                // `x = 1`, `d = {…}`, `x = 2`; the two `final`
3186                // initializers are suppressed. Pre-fix: 2.
3187                assert_eq!(metric.abc.assignments_sum(), 3);
3188            },
3189        );
3190    }
3191
3192    #[cfg(feature = "groovy")]
3193    #[test]
3194    fn groovy_final_local_is_an_error_at_the_pinned_grammar() {
3195        // `groovy_eq_initializes_final_binding` lists
3196        // `local_variable_declaration` for symmetry with Java, but at
3197        // dekobon-tree-sitter-groovy 0.3.0 a `final` local never
3198        // reaches it: the parser emits an `ERROR` node for the
3199        // declaration, with or without a terminator, so both of its `=`
3200        // tokens count. This pins the grammar limitation so a bump that
3201        // starts parsing the local shows up here rather than as a silent
3202        // change in what the predicate suppresses.
3203        let source = b"class K { int x; def m() { final int a = 0; x = 1 } }";
3204        let parser = GroovyParser::new(
3205            source.to_vec(),
3206            &std::path::PathBuf::from("foo.groovy"),
3207            None,
3208        );
3209        assert!(
3210            ast_has_kind_id(&parser, u16::MAX),
3211            "a `final` local should still parse to an ERROR node; if the grammar \
3212             now accepts it, re-derive the local half of the predicate",
3213        );
3214        check_metrics::<GroovyParser>(
3215            "class K { int x; def m() { final int a = 0; x = 1 } }",
3216            "foo.groovy",
3217            |metric| {
3218                assert_eq!(metric.abc.assignments_sum(), 2);
3219            },
3220        );
3221    }
3222
3223    #[cfg(feature = "csharp")]
3224    #[test]
3225    fn csharp_const_initializer_shapes() {
3226        // `csharp_eq_initializes_const_binding` reads the `const`
3227        // through the `modifier` node one hop above the
3228        // `variable_declaration`, for a local and a field alike, behind
3229        // other modifiers; `readonly` is not `const` and its initializer
3230        // counts. A lambda's body opens its own space in C#, so unlike
3231        // Java there was never a nested `=` to leak — the row pins that
3232        // the structural rule keeps the count the sentinel gave.
3233        check_metrics::<CsharpParser>(
3234            "class K {
3235                int x;
3236                private const int Q = 1;
3237                public static readonly int R = 2;
3238                void M() { const int q = 3; System.Action a = () => { x = 4; }; }
3239            }",
3240            "foo.cs",
3241            |metric| {
3242                // `R = 2`, `a = () => …`, `x = 4`.
3243                assert_eq!(metric.abc.assignments_sum(), 3);
3244            },
3245        );
3246    }
3247
3248    #[cfg(feature = "csharp")]
3249    #[test]
3250    fn csharp_constant_declarations() {
3251        check_metrics::<CsharpParser>(
3252            "class A {
3253                private const int X1 = 0, Y1 = 0;
3254                public const float PI = 3.14f;
3255                const string HELLO = \"Hello,\";
3256                protected string world = \" world!\";
3257                public float e = 2.718f;
3258                private int x2 = 1, y2 = 2;
3259                void M() {
3260                    const int Z1 = 0, Z2 = 0, Z3 = 0;
3261                    const float T = 0.0f;
3262                    int z1 = 1, z2 = 2, z3 = 3;
3263                }
3264            }",
3265            "foo.cs",
3266            |metric| insta::assert_json_snapshot!(metric.abc),
3267        );
3268    }
3269
3270    #[cfg(feature = "csharp")]
3271    #[test]
3272    fn csharp_declarations_with_conditions() {
3273        check_metrics::<CsharpParser>(
3274            "class A {
3275                bool a = (1 == 2);
3276                bool b = (1 < 2);
3277                bool c = !true;
3278                bool d = !false;
3279            }",
3280            "foo.cs",
3281            |metric| insta::assert_json_snapshot!(metric.abc),
3282        );
3283    }
3284
3285    #[cfg(feature = "csharp")]
3286    #[test]
3287    fn csharp_assignments_with_conditions() {
3288        check_metrics::<CsharpParser>(
3289            "class A {
3290                void M() {
3291                    int a = 0;
3292                    a += 1;
3293                    a -= 2;
3294                    a *= 3;
3295                    a /= 4;
3296                    a %= 5;
3297                    a++;
3298                    a--;
3299                }
3300            }",
3301            "foo.cs",
3302            |metric| insta::assert_json_snapshot!(metric.abc),
3303        );
3304    }
3305
3306    #[cfg(feature = "csharp")]
3307    #[test]
3308    fn csharp_methods_arguments_with_conditions() {
3309        check_metrics::<CsharpParser>(
3310            "class A {
3311                void M(int x, int y) {
3312                    F(x == y, x < y, !x.Equals(y));
3313                }
3314                void F(bool a, bool b, bool c) {}
3315            }",
3316            "foo.cs",
3317            |metric| {
3318                // expected: `==` +1, `<` +1, and the negated argument
3319                // `!x.Equals(y)` +1 — it scored 0 until #1537, when every
3320                // C# argument reached the peel still in its `argument`
3321                // wrapper.
3322                assert_eq!(metric.abc.conditions_sum(), 3);
3323                insta::assert_json_snapshot!(metric.abc);
3324            },
3325        );
3326    }
3327
3328    // #1537: every C# argument is wrapped in an `argument` node that the
3329    // negation peel declined, so a negated argument scored 0 where every
3330    // other C-family language scores 1. Each row is a C# call next to its
3331    // Java twin, which takes the expected value from outside the fix.
3332    #[cfg(all(feature = "csharp", feature = "java"))]
3333    #[test]
3334    fn csharp_negated_call_argument_matches_java() {
3335        let rows = [
3336            ("G(!a);", "g(!a);", 1),
3337            ("G((!a));", "g((!a));", 1),
3338            ("G(!a, !b);", "g(!a, !b);", 2),
3339            ("G(a, !b);", "g(a, !b);", 1),
3340            ("new B(!a);", "new B(!a);", 1),
3341            ("G(-a);", "g(-a);", 0),
3342            // A negated comparison pays once, for its `>`; a chain pays
3343            // per operand. Neither argument may pay again as a negation.
3344            ("G(!(a > b));", "g(!(a > b));", 1),
3345            ("G(!a && b);", "g(!a && b);", 2),
3346        ];
3347        for (csharp, java, expected) in rows {
3348            let cs = abc_conditions(
3349                LANG::Csharp,
3350                &format!("class A {{ void M() {{ {csharp} }} }}"),
3351            );
3352            let jv = abc_conditions(LANG::Java, &format!("class A {{ void m() {{ {java} }} }}"));
3353            assert_eq!(jv, expected, "Java twin `{java}`");
3354            assert_eq!(cs, expected, "C# `{csharp}` against Java `{java}`");
3355        }
3356    }
3357
3358    // The C#-only spellings around an argument. The value is the
3359    // `argument` wrapper's last operand, so a named-argument label, a
3360    // passing-mode keyword or a comment before it must not hide it.
3361    #[cfg(feature = "csharp")]
3362    #[test]
3363    fn csharp_negated_call_argument_spellings() {
3364        let rows = [
3365            // A named argument: the value follows `name` and `:`.
3366            ("class A { void M() { G(name: !a); } }", 1),
3367            // A comment between the label and the value sits inside the
3368            // `argument` wrapper, so a positional read of the value lands
3369            // on it. A comment before or after a whole argument is a
3370            // sibling of the wrapper instead and never reaches this read.
3371            ("class A { void M() { G(name: /*c*/ !a); } }", 1),
3372            // `ref` / `out` / `in` precede a variable, never a negation. A
3373            // control only: 0 is also what the pre-#1537 walk scored, so
3374            // this row cannot fail for any defect the fix addresses.
3375            ("class A { void M() { G(ref x, out var y, in z); } }", 0),
3376            // The lambda arm pays for its `!a` body; the argument holding
3377            // the lambda is not a negation and must not pay again.
3378            ("class A { void M() { G(x => !a); } }", 1),
3379            // `: base(!a)` — a `constructor_initializer`'s list reaches the
3380            // walk like any call's.
3381            ("class A : B { A(bool a) : base(!a) {} }", 1),
3382            // The two primary-constructor base calls: the record nests its
3383            // list in a `primary_constructor_base_type`, the class's list
3384            // is the branch node itself and is scored on the branch path.
3385            ("record R(bool a) : Base(!a);", 1),
3386            ("class S(bool a) : Base(!a) {}", 1),
3387            // An indexer's `bracketed_argument_list` is not a call, as
3388            // Java's array index is not.
3389            ("class A { void M() { var q = o[!a]; } }", 0),
3390        ];
3391        for (src, expected) in rows {
3392            assert_eq!(abc_conditions(LANG::Csharp, src), expected, "{src}");
3393        }
3394    }
3395
3396    #[cfg(feature = "csharp")]
3397    #[test]
3398    fn csharp_if_single_conditions() {
3399        check_metrics::<CsharpParser>(
3400            "class A {
3401                void M(int x) {
3402                    if (x > 0) { System.Console.WriteLine(\"a\"); }
3403                    if (x < 0) { System.Console.WriteLine(\"b\"); }
3404                    if (x == 0) { System.Console.WriteLine(\"c\"); }
3405                }
3406            }",
3407            "foo.cs",
3408            |metric| insta::assert_json_snapshot!(metric.abc),
3409        );
3410    }
3411
3412    #[cfg(feature = "csharp")]
3413    #[test]
3414    fn csharp_if_multiple_conditions() {
3415        check_metrics::<CsharpParser>(
3416            "class A {
3417                void M(int x, int y) {
3418                    if (x > 0 && y > 0) { System.Console.WriteLine(\"a\"); }
3419                    if (x < 0 || y < 0) { System.Console.WriteLine(\"b\"); }
3420                }
3421            }",
3422            "foo.cs",
3423            |metric| insta::assert_json_snapshot!(metric.abc),
3424        );
3425    }
3426
3427    #[cfg(feature = "csharp")]
3428    #[test]
3429    fn csharp_while_and_do_while_conditions() {
3430        check_metrics::<CsharpParser>(
3431            "class A {
3432                void M(int x) {
3433                    while (x > 0) { x--; }
3434                    do { x++; } while (x < 10);
3435                }
3436            }",
3437            "foo.cs",
3438            |metric| insta::assert_json_snapshot!(metric.abc),
3439        );
3440    }
3441
3442    #[cfg(feature = "csharp")]
3443    #[test]
3444    fn csharp_return_with_conditions() {
3445        check_metrics::<CsharpParser>(
3446            "class A {
3447                bool M(int x) {
3448                    return (x > 0);
3449                }
3450                bool N(int x) {
3451                    return !(x < 0);
3452                }
3453            }",
3454            "foo.cs",
3455            |metric| insta::assert_json_snapshot!(metric.abc),
3456        );
3457    }
3458
3459    // C# `switch` *expression* arms scored zero ABC conditions before
3460    // #456 — they carry no `case` / `default` token, so the token-driven
3461    // `csharp_count_token_condition` never saw them, even though C#
3462    // cyclomatic counts each non-discard arm. Revert-verified: adding the
3463    // gated `SwitchExpressionArm` arm is what lifts this from 0 to 2. The
3464    // bare `_ =>` discard arm is excluded (the `default:` analogue),
3465    // mirroring the cyclomatic gate (lesson 11).
3466    #[cfg(feature = "csharp")]
3467    #[test]
3468    fn csharp_switch_expression_arm_counts_condition() {
3469        check_metrics::<CsharpParser>(
3470            "class A {
3471                int M(int x) {
3472                    return x switch { 1 => 10, 2 => 20, _ => 0 };
3473                }
3474            }",
3475            "foo.cs",
3476            |metric| {
3477                // arm `1 =>` (+1) + arm `2 =>` (+1) + `_ =>` discard (+0).
3478                assert_eq!(metric.abc.conditions_sum(), 2);
3479            },
3480        );
3481    }
3482
3483    // Cross-language parity (lesson 11): a C# `switch` expression and the
3484    // equivalent Java arrow-`switch` must report the same ABC condition
3485    // count on equivalent code. Both have two concrete case arms and no
3486    // fallback arm, so both must count exactly 2. `check_metrics` takes a
3487    // non-capturing `fn` pointer, so the shared expected value (2) is
3488    // asserted in each callback rather than compared across closures; the
3489    // matching constant is what enforces parity. This guards against the
3490    // C# fix drifting away from the Java arrow-case treatment.
3491    #[cfg(all(feature = "csharp", feature = "java"))]
3492    #[test]
3493    fn csharp_java_switch_arm_abc_parity() {
3494        // C# switch expression: two arms, no fallback → 2 conditions.
3495        check_metrics::<CsharpParser>(
3496            "class A {
3497                int M(int x) {
3498                    return x switch { 1 => 10, 2 => 20 };
3499                }
3500            }",
3501            "foo.cs",
3502            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3503        );
3504
3505        // Equivalent Java arrow-`switch`: two case arms, no default → 2.
3506        check_metrics::<JavaParser>(
3507            "class A {
3508                int m(int x) {
3509                    return switch (x) { case 1 -> 10; case 2 -> 20; };
3510                }
3511            }",
3512            "foo.java",
3513            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3514        );
3515    }
3516
3517    // Issue #469: the `default` arm of a C-family `switch` is the
3518    // unconditional fallthrough and must NOT count as an ABC condition,
3519    // mirroring cyclomatic — which counts only the `Case` arms, never
3520    // the `Default` token.
3521    //
3522    // expected: each fixture is a single function whose switch has two
3523    // concrete `case` arms plus one `default`. ABC must count exactly
3524    // the two case arms (conditions = 2), matching cyclomatic's two
3525    // case-arm decisions. Pre-fix, every language below scored 3 (the
3526    // `Default` token leaked into the condition tally) — revert-verified
3527    // against the pre-#469 condition arms. We anchor on the integer
3528    // `conditions_sum()` headline (the value the public JSON serializes;
3529    // float magnitude is bit-brittle and excluded by the snapshot
3530    // policy). The cyclomatic side is pinned separately in
3531    // `java_csharp_cpp_switch_default_cyclomatic_parity` below, where
3532    // the per-space `cyclomatic()` decision count is isolated.
3533    #[cfg(feature = "java")]
3534    #[test]
3535    fn java_switch_default_not_a_condition() {
3536        // Classic statement `default:`.
3537        check_metrics::<JavaParser>(
3538            "class A {
3539                int m(int x) {
3540                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3541                }
3542            }",
3543            "foo.java",
3544            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3545        );
3546        // Arrow `default ->` — shares the same `Default` token.
3547        check_metrics::<JavaParser>(
3548            "class A {
3549                int m(int x) {
3550                    return switch (x) { case 1 -> 1; case 2 -> 2; default -> 0; };
3551                }
3552            }",
3553            "foo.java",
3554            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3555        );
3556    }
3557
3558    // Over-exclusion guard (issue #469): a statement `switch` with two
3559    // `case` arms and NO `default` must still count both cases. This
3560    // pins that the fix excludes only the `Default` token, never a
3561    // `Case` arm — the count is identical before and after #469 (two
3562    // cases → 2), so it would catch a fix that over-eagerly dropped a
3563    // real case (e.g. treating the trailing case as a fallthrough).
3564    // expected: case 1 (+1) + case 2 (+1) = 2.
3565    #[cfg(feature = "java")]
3566    #[test]
3567    fn java_switch_without_default_counts_all_cases() {
3568        check_metrics::<JavaParser>(
3569            "class A {
3570                int m(int x) {
3571                    switch (x) { case 1: return 1; case 2: return 2; }
3572                    return -1;
3573                }
3574            }",
3575            "foo.java",
3576            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3577        );
3578    }
3579
3580    #[cfg(feature = "csharp")]
3581    #[test]
3582    fn csharp_switch_default_not_a_condition() {
3583        check_metrics::<CsharpParser>(
3584            "class A {
3585                int M(int x) {
3586                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3587                }
3588            }",
3589            "foo.cs",
3590            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3591        );
3592    }
3593
3594    /// Regression #1450 / #1451: `goto case 2;` spells the same `case`
3595    /// keyword token (id 114) as a real arm, from a second production
3596    /// (`goto_statement`), and scored an ABC condition it does not earn —
3597    /// an unconditional jump to an arm is not a decision.
3598    ///
3599    /// `jmp` differs from `ctl` by exactly one statement, so the
3600    /// `goto case` is the only thing that can separate their scores
3601    /// (`.claude/rules/grammar-dispatch.md` §11 — an arm and a
3602    /// `goto case` are independent paths into the same counter, and a
3603    /// fixture carrying only arms cannot show the gate works). `dfl` is
3604    /// the control that was already correct, by accident rather than
3605    /// design: `goto default;` spells `Default`, which neither metric
3606    /// counts because it is the switch's unconditional fallthrough
3607    /// (#456, #469).
3608    ///
3609    /// [`assert_every_member_scores`] covers cyclomatic as well as ABC,
3610    /// through its per-member §8 parity assertion, and would catch the
3611    /// gate being removed from *either* side alone: dropping ABC's puts
3612    /// `jmp` at conditions 3 against the asserted 2, and dropping
3613    /// cyclomatic's leaves conditions 2 against a decision count of 3.
3614    /// Removing both restores the parity at `3 == 4 - 1` and is caught by
3615    /// the conditions value, which is why 2 is named rather than derived.
3616    ///
3617    /// The `Case` anchor is the second axis. Every member scores 2 from
3618    /// its two arms alone, so deleting `goto case 2;` leaves every
3619    /// assertion below satisfied and the construct under test gone; the
3620    /// anchor's count of 7 — six arms plus the one jump — fails by name
3621    /// instead.
3622    #[cfg(feature = "csharp")]
3623    #[test]
3624    fn csharp_goto_case_is_not_a_condition() {
3625        let src = "class A {
3626                int ctl(int x) { switch (x) { case 1: return 1; case 2: return 2; default: return 0; } }
3627                int jmp(int x) { switch (x) { case 1: goto case 2; case 2: return 2; default: return 0; } }
3628                int dfl(int x) { switch (x) { case 1: goto default; case 2: return 2; default: return 0; } }
3629            }";
3630        assert_csharp_fixture_spells(
3631            src,
3632            &[
3633                (Csharp::Case as u16, 7, "`case` tokens (6 arms + 1 jump)"),
3634                (Csharp::SwitchSection as u16, 9, "switch arms"),
3635                (Csharp::GotoStatement as u16, 2, "`goto` statements"),
3636                (
3637                    Csharp::Default as u16,
3638                    4,
3639                    "`default` tokens (3 arms + 1 jump)",
3640                ),
3641            ],
3642        );
3643        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
3644            assert_every_member_scores(
3645                &space.spaces[0],
3646                3,
3647                2,
3648                "one condition per `case` arm, none for a `goto` into one",
3649            );
3650        });
3651    }
3652
3653    #[cfg(feature = "cpp")]
3654    #[test]
3655    fn cpp_switch_default_not_a_condition() {
3656        // C++ (and plain C, which shares this grammar) already excluded
3657        // `default`; this pins the cross-language parity invariant.
3658        check_metrics::<CppParser>(
3659            "void f(int x) {
3660                 switch (x) { case 1: return; case 2: return; default: return; }
3661             }",
3662            "foo.cpp",
3663            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3664        );
3665    }
3666
3667    #[cfg(feature = "objc")]
3668    #[test]
3669    fn objc_abc() {
3670        // ObjC ABC reuses the C/C++ walker with two additions: a message
3671        // send `[obj msg]` is a call (B), and `@try` / `@catch` count as
3672        // conditions (C) like C++ try/catch.
3673        //   A: `int total = 0`, `int i = 0`, `i++`, `total = total + …`,
3674        //      `total = -1` = 5.
3675        //   B: `[self valueAt:i]`, `[self risky]` = 2 message sends.
3676        //   C: `i < n`, `@try`, `@catch`, `total >= 0` = 4.
3677        check_metrics::<ObjcParser>(
3678            "@implementation Foo\n\
3679             - (int)bar:(int)n {\n\
3680                 int total = 0;\n\
3681                 for (int i = 0; i < n; i++) {\n\
3682                     total = total + [self valueAt:i];\n\
3683                 }\n\
3684                 @try {\n\
3685                     [self risky];\n\
3686                 } @catch (NSException *e) {\n\
3687                     total = -1;\n\
3688                 }\n\
3689                 if (total >= 0) {\n\
3690                     return total;\n\
3691                 }\n\
3692                 return 0;\n\
3693             }\n\
3694             @end\n",
3695            "foo.m",
3696            |metric| {
3697                assert_eq!(metric.abc.assignments_sum(), 5);
3698                assert_eq!(metric.abc.branches_sum(), 2);
3699                assert_eq!(metric.abc.conditions_sum(), 4);
3700            },
3701        );
3702    }
3703
3704    #[cfg(feature = "objc")]
3705    #[test]
3706    fn objc_abc_conditions() {
3707        // Exercises the condition-slot arms shared with C/C++: a `while`
3708        // head, a `&&` chain, a `do … while` trailing condition, and a
3709        // `return <comparison>`. ObjC routes these through the same
3710        // grammar-agnostic `cpp_inspect_*` helpers.
3711        //   A: `p++`, `p--` = 2.   B: no calls = 0.
3712        //   C: `p != 0`, `*p > 0`, `*p < 9`, `*p == 0` = 4.
3713        check_metrics::<ObjcParser>(
3714            "@implementation Foo\n\
3715             - (int)g:(int *)p {\n\
3716                 while (p != 0 && *p > 0) {\n\
3717                     p++;\n\
3718                 }\n\
3719                 do {\n\
3720                     p--;\n\
3721                 } while (*p < 9);\n\
3722                 return *p == 0;\n\
3723             }\n\
3724             @end\n",
3725            "foo.m",
3726            |metric| {
3727                assert_eq!(metric.abc.assignments_sum(), 2);
3728                assert_eq!(metric.abc.branches_sum(), 0);
3729                assert_eq!(metric.abc.conditions_sum(), 4);
3730            },
3731        );
3732    }
3733
3734    #[cfg(feature = "objc")]
3735    #[test]
3736    fn objc_abc_message_send_unary_condition() {
3737        // A negated boolean passed as a message-send argument is a unary
3738        // condition (Fitzpatrick Rule 9), the same as in a C-call argument.
3739        // Message args are direct children of `message_expression` (no
3740        // `argument_list`), so they are inspected in the `MessageExpression`
3741        // arm. Here: `[self use:!a]` (1 call + 1 unary condition) +
3742        // `cFunc(!a)` (1 call + 1 unary condition) → B=2, C=2.
3743        check_metrics::<ObjcParser>(
3744            "@implementation Foo\n\
3745             - (void)bar:(int)a {\n\
3746                 [self use:!a];\n\
3747                 cFunc(!a);\n\
3748             }\n\
3749             @end\n",
3750            "foo.m",
3751            |metric| {
3752                assert_eq!(metric.abc.branches_sum(), 2);
3753                assert_eq!(metric.abc.conditions_sum(), 2);
3754            },
3755        );
3756    }
3757
3758    #[cfg(feature = "objc")]
3759    #[test]
3760    fn objc_message_send_is_a_bool_terminal_in_condition_slots() {
3761        // `[obj ok]` is Objective-C's call, so in a condition slot it is
3762        // the unary condition `ok()` is (Fitzpatrick Rule 9). Until
3763        // `message_expression` joined the former `cpp_bool_terminal_kinds!` every
3764        // row here scored zero conditions where its C-call twin scored
3765        // one — and #1276's `for` slot inherited the gap. Each row is a
3766        // message send as the *whole* slot (grammar-dispatch §11: a
3767        // comparison would be counted by its own operator arm anyway),
3768        // and branches are asserted beside conditions so the arm cannot
3769        // be read as counting the call twice. The last three are the
3770        // over-count guards: a value position counts nothing, as `ok()`'s
3771        // does not.
3772        let cases = [
3773            ("if ([self ok]) {}", 1, 1),
3774            ("while (![self ok]) {}", 1, 1),
3775            ("do {} while ([self ok]);", 1, 1),
3776            ("for (; [self ok]; ) {}", 1, 1),
3777            ("x = [self ok] ? 1 : 2;", 2, 1),
3778            ("if ([self ok] && [o ok]) {}", 2, 2),
3779            ("return [self ok];", 0, 1),
3780            ("[self use:[self ok]];", 0, 2),
3781            ("[self use:![self ok]];", 1, 2),
3782        ];
3783        let mut ran = 0;
3784        for (body, conditions, branches) in cases {
3785            let src = format!("@implementation Foo\n- (int)bar {{\n    {body}\n}}\n@end\n");
3786            let abc = metrics_verbatim(
3787                LANG::Objc,
3788                src.as_bytes(),
3789                MetricsOptions::default().with_only(&[crate::Metric::Abc]),
3790            )
3791            .abc;
3792            assert_eq!(abc.conditions_sum(), conditions, "`{body}` conditions");
3793            assert_eq!(abc.branches_sum(), branches, "`{body}` branches");
3794            ran += 1;
3795        }
3796        assert_eq!(ran, cases.len());
3797        // Both answers are present, so a walker stuck at 0 or at 1 cannot
3798        // pass half the table silently.
3799        assert!(cases.iter().any(|&(_, c, _)| c == 0));
3800        assert!(cases.iter().any(|&(_, c, _)| c == 2));
3801    }
3802
3803    #[cfg(feature = "objc")]
3804    #[test]
3805    fn objc_message_send_condition_agrees_with_c_call() {
3806        // The intra-ObjC parity C++ cannot express: a message send and a
3807        // C call in the same slot score alike. Non-degenerate by the
3808        // `assert_eq!(…, 1)` on the reference.
3809        for (send, call) in [
3810            ("if ([a ok]) {}", "if (ok()) {}"),
3811            ("for (; [a ok]; ) {}", "for (; ok(); ) {}"),
3812            ("x = [a ok] ? 1 : 2;", "x = ok() ? 1 : 2;"),
3813        ] {
3814            let wrap =
3815                |body: &str| format!("@implementation Foo\n- (int)bar {{\n    {body}\n}}\n@end\n");
3816            let reference = abc_conditions(LANG::Objc, &wrap(call));
3817            assert!(reference >= 1, "`{call}` must count at least the slot");
3818            assert_eq!(
3819                abc_conditions(LANG::Objc, &wrap(send)),
3820                reference,
3821                "`{send}`"
3822            );
3823        }
3824    }
3825
3826    #[cfg(feature = "groovy")]
3827    #[test]
3828    fn groovy_switch_default_not_a_condition() {
3829        check_metrics::<GroovyParser>(
3830            "class A {
3831                int m(int x) {
3832                    switch (x) { case 1: return 1; case 2: return 2; default: return 0 }
3833                }
3834            }",
3835            "foo.groovy",
3836            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3837        );
3838    }
3839
3840    #[cfg(feature = "javascript")]
3841    #[test]
3842    fn js_switch_default_not_a_condition() {
3843        check_metrics::<JavascriptParser>(
3844            "function f(x) {
3845                 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3846             }",
3847            "foo.js",
3848            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3849        );
3850    }
3851
3852    #[cfg(feature = "typescript")]
3853    #[test]
3854    fn ts_switch_default_not_a_condition() {
3855        check_metrics::<TypescriptParser>(
3856            "function f(x: number): number {
3857                 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3858             }",
3859            "foo.ts",
3860            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3861        );
3862    }
3863
3864    // Cross-language parity (lesson 11): the equivalent statement-`switch`
3865    // with a `default` arm reports the same ABC condition count across
3866    // Java / C# / C++. All three have two concrete case arms plus a
3867    // fallthrough `default`, so all three must count exactly 2 conditions
3868    // (the `default` excluded). `check_metrics` takes a non-capturing
3869    // `fn` pointer, so the shared expected value is asserted in each
3870    // callback; the matching constant is what enforces parity.
3871    #[cfg(all(feature = "cpp", feature = "csharp", feature = "java"))]
3872    #[test]
3873    fn java_csharp_cpp_switch_default_abc_parity() {
3874        check_metrics::<JavaParser>(
3875            "class A {
3876                int m(int x) {
3877                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3878                }
3879            }",
3880            "foo.java",
3881            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3882        );
3883        check_metrics::<CsharpParser>(
3884            "class A {
3885                int M(int x) {
3886                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3887                }
3888            }",
3889            "foo.cs",
3890            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3891        );
3892        check_metrics::<CppParser>(
3893            "void f(int x) {
3894                 switch (x) { case 1: return; case 2: return; default: return; }
3895             }",
3896            "foo.cpp",
3897            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3898        );
3899    }
3900
3901    // Pins the ABC-vs-cyclomatic agreement the fix is about (lesson 11):
3902    // on the method's own function space, the cyclomatic decision count
3903    // (`cyclomatic()` minus the per-space base of 1) must equal the ABC
3904    // `conditions()` for the same switch. Both must be 2 — the two case
3905    // arms — with the `default` excluded from each. Revert-verified: pre-
3906    // #469 ABC `conditions()` was 3 here while cyclomatic stayed at 2.
3907    #[cfg(all(feature = "cpp", feature = "csharp", feature = "java"))]
3908    #[test]
3909    fn java_csharp_cpp_switch_default_cyclomatic_parity() {
3910        check_func_space::<JavaParser, _>(
3911            "class A {
3912                int m(int x) {
3913                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3914                }
3915            }",
3916            "foo.java",
3917            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3918        );
3919        check_func_space::<CsharpParser, _>(
3920            "class A {
3921                int M(int x) {
3922                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3923                }
3924            }",
3925            "foo.cs",
3926            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3927        );
3928        check_func_space::<CppParser, _>(
3929            "void f(int x) {
3930                 switch (x) { case 1: return; case 2: return; default: return; }
3931             }",
3932            "foo.cpp",
3933            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3934        );
3935    }
3936
3937    // Issue #473: PHP `switch` `default:` (`DefaultStatement`) is the
3938    // unconditional fallthrough, not a condition. ABC `conditions()` must
3939    // equal the cyclomatic decision count (`cyclomatic() - 1`) on the
3940    // function's own space — both 2 for the two `case` arms, with the
3941    // `default` excluded. Revert-verified: re-adding `DefaultStatement` to
3942    // the PHP ABC condition arm makes `conditions()` 3 here while cyclomatic
3943    // stays at 2, failing the invariant.
3944    #[cfg(feature = "php")]
3945    #[test]
3946    fn php_switch_default_not_a_condition() {
3947        check_func_space::<PhpParser, _>(
3948            "<?php
3949            function f($x) {
3950                switch ($x) {
3951                    case 1: return 1;
3952                    case 2: return 2;
3953                    default: return 0;
3954                }
3955            }",
3956            "foo.php",
3957            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3958        );
3959    }
3960
3961    // Issue #473: PHP `match` `default =>` (`MatchDefaultExpression`) is the
3962    // unconditional fallthrough, mirroring the switch `default:` case above.
3963    // ABC `conditions()` must equal the cyclomatic decision count
3964    // (`cyclomatic() - 1`) — both 2 for the two non-default match arms.
3965    // Revert-verified: re-adding `MatchDefaultExpression` to the PHP ABC
3966    // condition arm makes `conditions()` 3 here while cyclomatic stays at 2.
3967    #[cfg(feature = "php")]
3968    #[test]
3969    fn php_match_default_not_a_condition() {
3970        check_func_space::<PhpParser, _>(
3971            "<?php
3972            function g($x) {
3973                return match ($x) {
3974                    1 => \"a\",
3975                    2 => \"b\",
3976                    default => \"z\",
3977                };
3978            }",
3979            "foo.php",
3980            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3981        );
3982    }
3983
3984    #[cfg(feature = "csharp")]
3985    #[test]
3986    fn csharp_if_bare_identifier_condition() {
3987        check_metrics::<CsharpParser>(
3988            "class A {
3989                void M(bool x) {
3990                    if (x) { System.Console.WriteLine(\"a\"); }
3991                }
3992            }",
3993            "foo.cs",
3994            |metric| {
3995                // `if (x)` contributes 1 condition (bare identifier).
3996                // `System.Console.WriteLine(...)` is the only call → 1 branch.
3997                // `*_sum()` is what the public JSON serializes as the
3998                // headline value (see `crate::wire::Abc`).
3999                assert_eq!(metric.abc.conditions_sum(), 1);
4000                assert_eq!(metric.abc.branches_sum(), 1);
4001                assert_eq!(metric.abc.assignments_sum(), 0);
4002            },
4003        );
4004    }
4005
4006    #[cfg(feature = "csharp")]
4007    #[test]
4008    fn csharp_while_bare_identifier_condition() {
4009        check_metrics::<CsharpParser>(
4010            "class A {
4011                void M(bool x) {
4012                    while (x) { x = false; }
4013                }
4014            }",
4015            "foo.cs",
4016            |metric| {
4017                // `while (x)` contributes 1 condition; `x = false` is 1 assignment.
4018                assert_eq!(metric.abc.conditions_sum(), 1);
4019                assert_eq!(metric.abc.assignments_sum(), 1);
4020                assert_eq!(metric.abc.branches_sum(), 0);
4021            },
4022        );
4023    }
4024
4025    #[cfg(feature = "csharp")]
4026    #[test]
4027    fn csharp_do_while_bare_identifier_condition() {
4028        check_metrics::<CsharpParser>(
4029            "class A {
4030                void M(bool x) {
4031                    do { x = true; } while (x);
4032                }
4033            }",
4034            "foo.cs",
4035            |metric| {
4036                // `do { ... } while (x)` contributes 1 condition;
4037                // `x = true` is 1 assignment.
4038                assert_eq!(metric.abc.conditions_sum(), 1);
4039                assert_eq!(metric.abc.assignments_sum(), 1);
4040                assert_eq!(metric.abc.branches_sum(), 0);
4041            },
4042        );
4043    }
4044
4045    #[cfg(feature = "csharp")]
4046    #[test]
4047    fn csharp_if_unary_not_condition() {
4048        // Two cases share one test:
4049        //
4050        //   if (!x) { … }  — the `if` predicate is a boolean slot, so
4051        //   `csharp_count_condition` pays for it once the peel
4052        //   (`csharp_wrapper_operand`) has taken the `!` off; the
4053        //   slot would pay without the `!` too.
4054        //
4055        //   return !x;  — a `return` value is not a slot, so the `!`
4056        //   is the *only* thing that makes the operand count, through
4057        //   `csharp_count_negated`. Asserting the `return !x;`
4058        //   case isolates the negation path from the slot path.
4059        check_metrics::<CsharpParser>(
4060            "class A {
4061                void M(bool x) {
4062                    if (!x) { System.Console.WriteLine(\"a\"); }
4063                }
4064                bool N(bool x) {
4065                    return !x;
4066                }
4067            }",
4068            "foo.cs",
4069            |metric| {
4070                // `if (!x)` contributes 1 condition (the slot pays for the
4071                // peeled `x`). `return !x;` contributes 1 condition (not a
4072                // slot — the unary `!` is what makes `x` count).
4073                // → 2 conditions total. 1 branch from WriteLine().
4074                assert_eq!(metric.abc.conditions_sum(), 2);
4075                assert_eq!(metric.abc.branches_sum(), 1);
4076                assert_eq!(metric.abc.assignments_sum(), 0);
4077            },
4078        );
4079    }
4080
4081    #[cfg(feature = "csharp")]
4082    #[test]
4083    fn csharp_if_double_parenthesized_condition() {
4084        // Audit-tests follow-up: with only the
4085        // `csharp_prefix_unary_expr_kinds!()` arm covered by
4086        // `csharp_if_unary_not_condition`, the
4087        // `csharp_paren_expr_kinds!()` delegation arm in
4088        // `csharp_count_condition` was a pure dead-code candidate —
4089        // disabling it caused zero existing tests to fail (verified
4090        // 2026-05-26).
4091        //
4092        // `if ((x))` puts a `ParenthesizedExpression` at child(2) of
4093        // the IfStatement (child(1) is the literal `(`, child(2) is
4094        // the inner parenthesised expression, child(3) is the literal
4095        // `)`). `csharp_count_condition` must peel that parenthesis
4096        // (`csharp_wrapper_operand`) down to the inner Identifier,
4097        // which does not score itself, so the slot pays once.
4098        check_metrics::<CsharpParser>(
4099            "class A {
4100                void M(bool x) {
4101                    if ((x)) { System.Console.WriteLine(\"a\"); }
4102                }
4103            }",
4104            "foo.cs",
4105            |metric| {
4106                assert_eq!(metric.abc.conditions_sum(), 1);
4107                assert_eq!(metric.abc.branches_sum(), 1);
4108                assert_eq!(metric.abc.assignments_sum(), 0);
4109            },
4110        );
4111    }
4112
4113    #[cfg(feature = "csharp")]
4114    #[test]
4115    fn csharp_bool_returning_terminal_kinds_count() {
4116        // Regression for issue #372 (lesson #19): before the fix,
4117        // `csharp_count_condition` / the former `csharp_inspect_container`
4118        // only recognised invocation / identifier / boolean literal as
4119        // terminal-bool operands, so the five idiomatic boolean
4120        // expressions in the `if (...)` slots below silently scored
4121        // zero conditions:
4122        //
4123        //   - `cfg.flag`        — MemberAccessExpression
4124        //   - `await c.Check()` — AwaitExpression
4125        //   - `(bool)v`         — CastExpression
4126        //   - `v is not null`   — IsPatternExpression
4127        //   - `flags[0]`        — ElementAccessExpression
4128        //
4129        // The `is` test has since moved out of the former terminal set to an
4130        // unconditional arm (#1461), so it no longer depends on the
4131        // slot to be counted. The total is unchanged, which is the
4132        // point of that change rather than an accident of it.
4133        //
4134        // expected: 5 conditions (one per `if`), 0 assignments,
4135        // 1 branch (the single `c.Check()` invocation; the other
4136        // `if`-condition expressions are not invocations).
4137        check_metrics::<CsharpParser>(
4138            "using System.Threading.Tasks;
4139            class A {
4140                async Task M(object v, bool[] flags, Cfg cfg, C c) {
4141                    if (cfg.flag) { }
4142                    if (await c.Check()) { }
4143                    if ((bool)v) { }
4144                    if (v is not null) { }
4145                    if (flags[0]) { }
4146                }
4147            }
4148            class Cfg { public bool flag; }
4149            class C { public Task<bool> Check() => null; }",
4150            "foo.cs",
4151            |metric| {
4152                assert_eq!(metric.abc.conditions_sum(), 5);
4153                assert_eq!(metric.abc.assignments_sum(), 0);
4154                assert_eq!(metric.abc.branches_sum(), 1);
4155            },
4156        );
4157    }
4158
4159    #[cfg(feature = "csharp")]
4160    #[test]
4161    fn csharp_if_method_call_condition() {
4162        check_metrics::<CsharpParser>(
4163            "class A {
4164                void M(string s) {
4165                    if (s.StartsWith(\"x\")) { System.Console.WriteLine(\"a\"); }
4166                }
4167            }",
4168            "foo.cs",
4169            |metric| {
4170                // `if (s.StartsWith("x"))` contributes 1 condition
4171                // (InvocationExpression) plus 1 branch for the call itself,
4172                // plus 1 branch for WriteLine.
4173                assert_eq!(metric.abc.conditions_sum(), 1);
4174                assert_eq!(metric.abc.branches_sum(), 2);
4175                assert_eq!(metric.abc.assignments_sum(), 0);
4176            },
4177        );
4178    }
4179
4180    #[cfg(feature = "csharp")]
4181    #[test]
4182    fn csharp_if_while_boolean_literal_condition() {
4183        // Regression for #371: the tree-sitter-c-sharp grammar wraps a
4184        // bare `true` / `false` literal used as the condition of
4185        // `if` / `while` / `do` / `?:` in a `boolean_literal` node,
4186        // not the leaf `true` / `false` tokens. `csharp_count_condition`
4187        // must therefore match `BooleanLiteral` (the wrapper),
4188        // mirroring the existing `csharp_walk_for_statement` arm.
4189        // Without that, every literal-condition statement scored 0
4190        // conditions. The `&&` / `||` chain-operand slot is covered
4191        // separately by `csharp_short_circuit_with_boolean_literal_operand`
4192        // and the negated-operand path (`csharp_count_negated`) by
4193        // `csharp_declarations_with_conditions` (`!true` / `!false`).
4194        check_metrics::<CsharpParser>(
4195            "class A {
4196                void M() {
4197                    if (true) { System.Console.WriteLine(\"a\"); }
4198                    if (false) { System.Console.WriteLine(\"b\"); }
4199                    while (true) { break; }
4200                    do { break; } while (false);
4201                    int t = true ? 1 : 0;
4202                }
4203            }",
4204            "foo.cs",
4205            |metric| {
4206                // Five literal-condition statements contribute 5
4207                // `BooleanLiteral` conditions (one per if/if/while/
4208                // do-while/ternary), plus the ternary's `?` token
4209                // adds one more via `csharp_count_token_condition`
4210                // → 6 total. The two `System.Console.WriteLine`
4211                // calls contribute 2 branches; the `int t = …`
4212                // initializer contributes 1 assignment.
4213                assert_eq!(metric.abc.conditions_sum(), 6);
4214                assert_eq!(metric.abc.branches_sum(), 2);
4215                assert_eq!(metric.abc.assignments_sum(), 1);
4216            },
4217        );
4218    }
4219
4220    #[cfg(feature = "csharp")]
4221    #[test]
4222    fn csharp_short_circuit_with_boolean_literal_operand() {
4223        // Regression for #371 (companion to
4224        // `csharp_if_while_boolean_literal_condition`): a bare
4225        // `true` / `false` operand of `&&` / `||` is a chain-operand
4226        // slot scored through `csharp_count_condition`. A
4227        // `BooleanLiteral` does not score itself, so the slot pays for
4228        // it — otherwise the operand silently scores zero.
4229        check_metrics::<CsharpParser>(
4230            "class A {
4231                void M(bool x) {
4232                    if (x && true) { System.Console.WriteLine(\"a\"); }
4233                    if (false || x) { System.Console.WriteLine(\"b\"); }
4234                }
4235            }",
4236            "foo.cs",
4237            |metric| {
4238                // `&&` and `||` themselves are NOT in
4239                // `csharp_count_token_condition`'s match list — they
4240                // route through `csharp_walk_for_conditions::AMPAMP|
4241                // PIPEPIPE`, which scores each operand of the parent
4242                // BinaryExpression through `csharp_count_condition`.
4243                // An operand pays one condition unless it scores
4244                // itself. For
4245                // `x && true`: 1 (Identifier x) + 1 (BooleanLiteral
4246                // true) = 2. For `false || x`: 1 (BooleanLiteral
4247                // false) + 1 (Identifier x) = 2. Total 4.
4248                assert_eq!(metric.abc.conditions_sum(), 4);
4249                assert_eq!(metric.abc.branches_sum(), 2);
4250                assert_eq!(metric.abc.assignments_sum(), 0);
4251            },
4252        );
4253    }
4254
4255    #[cfg(feature = "csharp")]
4256    #[test]
4257    fn csharp_return_without_conditions() {
4258        check_metrics::<CsharpParser>(
4259            "class A {
4260                int M() { return 42; }
4261                string N() { return \"hi\"; }
4262            }",
4263            "foo.cs",
4264            |metric| insta::assert_json_snapshot!(metric.abc),
4265        );
4266    }
4267
4268    #[cfg(feature = "csharp")]
4269    #[test]
4270    fn csharp_lambda_expressions_return_with_conditions() {
4271        check_metrics::<CsharpParser>(
4272            "class A {
4273                public void M() {
4274                    System.Func<int, bool> f = x => (x > 0);
4275                    System.Func<int, bool> g = x => !(x < 0);
4276                }
4277            }",
4278            "foo.cs",
4279            |metric| insta::assert_json_snapshot!(metric.abc),
4280        );
4281    }
4282
4283    #[cfg(feature = "csharp")]
4284    #[test]
4285    fn csharp_for_with_variable_declaration() {
4286        check_metrics::<CsharpParser>(
4287            "class A {
4288                void M() {
4289                    for (int i = 0; i < 10; i++) {
4290                        System.Console.WriteLine(i);
4291                    }
4292                }
4293            }",
4294            "foo.cs",
4295            |metric| insta::assert_json_snapshot!(metric.abc),
4296        );
4297    }
4298
4299    #[cfg(feature = "csharp")]
4300    #[test]
4301    fn csharp_for_without_variable_declaration() {
4302        check_metrics::<CsharpParser>(
4303            "class A {
4304                void M() {
4305                    int i;
4306                    for (i = 0; i < 10; i++) {
4307                        System.Console.WriteLine(i);
4308                    }
4309                }
4310            }",
4311            "foo.cs",
4312            |metric| insta::assert_json_snapshot!(metric.abc),
4313        );
4314    }
4315
4316    #[cfg(feature = "csharp")]
4317    #[test]
4318    fn csharp_for_identifier_condition() {
4319        check_metrics::<CsharpParser>(
4320            "class A {
4321                void M(bool ready) {
4322                    for (; ready ;) { }
4323                }
4324            }",
4325            "foo.cs",
4326            |metric| {
4327                // expected: assignments=0 (no `=` / `++` / `--`),
4328                // branches=0 (no invocation / object creation),
4329                // conditions=1 (bare-identifier for-loop condition).
4330                // Averages divide by 3 spaces (top-level + class + method).
4331                insta::assert_json_snapshot!(
4332                    metric.abc,
4333                    @r#"
4334                {
4335                  "assignments": 0,
4336                  "branches": 0,
4337                  "conditions": 1,
4338                  "magnitude": 1.0,
4339                  "value": 0.0,
4340                  "assignments_average": 0.0,
4341                  "branches_average": 0.0,
4342                  "conditions_average": 0.3333333333333333,
4343                  "assignments_min": 0,
4344                  "assignments_max": 0,
4345                  "branches_min": 0,
4346                  "branches_max": 0,
4347                  "conditions_min": 0,
4348                  "conditions_max": 1
4349                }
4350                "#
4351                );
4352            },
4353        );
4354    }
4355
4356    #[cfg(feature = "csharp")]
4357    #[test]
4358    fn csharp_for_invocation_condition() {
4359        check_metrics::<CsharpParser>(
4360            "class A {
4361                bool Ok() { return true; }
4362                void M() {
4363                    for (; Ok() ;) { }
4364                }
4365            }",
4366            "foo.cs",
4367            |metric| {
4368                // expected: assignments=0, branches=1 (the `Ok()` call),
4369                // conditions=1 (invocation as for-loop condition).
4370                // Averages divide by 4 spaces (top-level + class + two
4371                // methods).
4372                insta::assert_json_snapshot!(
4373                    metric.abc,
4374                    @r#"
4375                {
4376                  "assignments": 0,
4377                  "branches": 1,
4378                  "conditions": 1,
4379                  "magnitude": 1.4142135623730951,
4380                  "value": 0.0,
4381                  "assignments_average": 0.0,
4382                  "branches_average": 0.25,
4383                  "conditions_average": 0.25,
4384                  "assignments_min": 0,
4385                  "assignments_max": 0,
4386                  "branches_min": 0,
4387                  "branches_max": 1,
4388                  "conditions_min": 0,
4389                  "conditions_max": 1
4390                }
4391                "#
4392                );
4393            },
4394        );
4395    }
4396
4397    // Regression coverage for #279: the C# grammar wraps a literal
4398    // `true` / `false` for-loop condition in a `boolean_literal` node.
4399    // The `BooleanLiteral` arm in the `ForStatement` dispatch must
4400    // attribute one condition; without it, `for (; true ;)` would
4401    // contribute 0 (the bug fixed by this commit also affected this
4402    // shape).
4403    #[cfg(feature = "csharp")]
4404    #[test]
4405    fn csharp_for_boolean_literal_condition() {
4406        check_metrics::<CsharpParser>(
4407            "class A {
4408                void M() {
4409                    for (; true ;) { }
4410                }
4411            }",
4412            "foo.cs",
4413            |metric| {
4414                // expected: assignments=0, branches=0,
4415                // conditions=1 (the `true` literal as condition).
4416                assert_eq!(metric.abc.conditions_sum(), 1);
4417                assert_eq!(metric.abc.assignments_sum(), 0);
4418                assert_eq!(metric.abc.branches_sum(), 0);
4419            },
4420        );
4421    }
4422
4423    // Regression coverage for #279: an empty for-loop condition such as
4424    // `for (; ;) {}` must contribute 0 to conditions — there is no
4425    // condition node to count.
4426    #[cfg(feature = "csharp")]
4427    #[test]
4428    fn csharp_for_empty_condition() {
4429        check_metrics::<CsharpParser>(
4430            "class A {
4431                void M() {
4432                    for (; ;) { }
4433                }
4434            }",
4435            "foo.cs",
4436            |metric| {
4437                // expected: assignments=0, branches=0, conditions=0
4438                // (no condition expression in `for (; ;)`).
4439                insta::assert_json_snapshot!(
4440                    metric.abc,
4441                    @r#"
4442                {
4443                  "assignments": 0,
4444                  "branches": 0,
4445                  "conditions": 0,
4446                  "magnitude": 0.0,
4447                  "value": 0.0,
4448                  "assignments_average": 0.0,
4449                  "branches_average": 0.0,
4450                  "conditions_average": 0.0,
4451                  "assignments_min": 0,
4452                  "assignments_max": 0,
4453                  "branches_min": 0,
4454                  "branches_max": 0,
4455                  "conditions_min": 0,
4456                  "conditions_max": 0
4457                }
4458                "#
4459                );
4460            },
4461        );
4462    }
4463
4464    #[cfg(feature = "csharp")]
4465    #[test]
4466    fn csharp_ternary_conditions() {
4467        check_metrics::<CsharpParser>(
4468            "class A {
4469                int Sign(int x) {
4470                    return (x > 0) ? 1 : (x < 0 ? -1 : 0);
4471                }
4472            }",
4473            "foo.cs",
4474            |metric| insta::assert_json_snapshot!(metric.abc),
4475        );
4476    }
4477
4478    #[cfg(feature = "csharp")]
4479    #[test]
4480    fn csharp_malformed_parenthesized_no_panic() {
4481        check_metrics::<CsharpParser>("class A { void M() { if (( }) }", "foo.cs", |metric| {
4482            // Don't panic on malformed source.
4483            assert_eq!(metric.abc.assignments(), 0);
4484            assert_eq!(metric.abc.branches(), 0);
4485        });
4486    }
4487
4488    #[cfg(feature = "csharp")]
4489    #[test]
4490    fn csharp_function_pointer_type_no_double_count() {
4491        // EC1 extension — `<` and `>` are also parameter-list delimiters
4492        // for unsafe function-pointer types. Since #1297 the arm is an
4493        // allowlist, so `FunctionPointerType` is excluded by being
4494        // absent from it rather than by being named in a deny set;
4495        // either way these brackets must not accumulate `conditions`.
4496        check_metrics::<CsharpParser>(
4497            "unsafe class A {
4498                public delegate*<int, int, int> Adder;
4499                public delegate*<string, void> Logger;
4500            }",
4501            "foo.cs",
4502            |metric| {
4503                assert_eq!(
4504                    metric.abc.conditions(),
4505                    0,
4506                    "function-pointer-type angle brackets must not count"
4507                );
4508            },
4509        );
4510    }
4511
4512    #[cfg(feature = "csharp")]
4513    #[test]
4514    fn csharp_generic_type_args_no_double_count() {
4515        // EC1 — `<` and `>` inside TypeArgumentList must not count as
4516        // boolean conditions.
4517        check_metrics::<CsharpParser>(
4518            "class A {
4519                void M(System.Collections.Generic.Dictionary<string, System.Collections.Generic.List<int>> d) {
4520                    System.Console.WriteLine(d);
4521                }
4522            }",
4523            "foo.cs",
4524            |metric| insta::assert_json_snapshot!(metric.abc),
4525        );
4526    }
4527
4528    // #1297: `operator <` names the operator a type *defines*; applying
4529    // one is what a decision looks like. tree-sitter-c-sharp spells the
4530    // declaration's operator with the same bare token as a comparison,
4531    // and the previous denylist named only `type_argument_list`,
4532    // `type_parameter_list` and `function_pointer_type`, so every
4533    // comparison-operator overload scored a condition.
4534    //
4535    // The fixture carries the two overloads plus one `a < b` inside a
4536    // `binary_expression` and one `x is > 0 ? 2 : 3`, which the grammar
4537    // parses as a `relational_pattern` whose operand is the ternary (see
4538    // the assertion). Every mis-aim lands on its own number: 7
4539    // with neither gate, 5 if `RelationalPattern` is readmitted to the
4540    // allowlist (#1383 dropped it), 3 if the gate swallows
4541    // `BinaryExpression` too (only the ternary's `?` and slot and the
4542    // `is` test survive), 0 if the fixture stops parsing. Each is two
4543    // higher than before #1461 and #1526, which added the `is` test and
4544    // the ternary's slot as counts no gate on the `<` / `>` token can
4545    // reach.
4546    #[cfg(feature = "csharp")]
4547    #[test]
4548    fn csharp_operator_declaration_is_not_a_condition() {
4549        check_func_space::<CsharpParser, _>(
4550            "class V {
4551                public static bool operator <(V a, V b) { return true; }
4552                public static bool operator >(V a, V b) { return true; }
4553                int m(int a, int b, int x) {
4554                    if (a < b) { return 1; }
4555                    return x is > 0 ? 2 : 3;
4556                }
4557            }",
4558            "foo.cs",
4559            |space| {
4560                // Assert the claim per space rather than through the
4561                // file total, which is 2 with the two overloads and 2
4562                // without them — an aggregate assertion would pass on a
4563                // fixture that had lost the very construct under test.
4564                let class = &space.spaces[0];
4565                assert_eq!(class.spaces.len(), 3, "two overloads plus `m`");
4566                for (i, member) in class.spaces.iter().enumerate().take(2) {
4567                    assert_eq!(
4568                        member.metrics.abc.conditions(),
4569                        0,
4570                        "operator declaration {i} must score no condition"
4571                    );
4572                }
4573                // 4: the `a < b` comparison, the ternary `?`, its
4574                // condition slot, and the `is` test. tree-sitter-c-sharp
4575                // 0.23.5 parses `x is > 0 ? 2 : 3` as `x is > (0 ? 2 :
4576                // 3)` — the ternary is the pattern's operand, so the
4577                // pattern sits in no decision slot and its `>` still
4578                // scores nothing (#1383), while the ternary's condition
4579                // is the literal `0`, which pays as a slot since #1526.
4580                // The fourth count is the enclosing
4581                // `is_pattern_expression` itself, which since #1461
4582                // scores by use rather than only inside a boolean slot.
4583                // C# itself binds the source `(x is > 0) ? 2 : 3`; a
4584                // grammar that agrees reads 3 here, because the `is`
4585                // test in the condition slot already pays for it.
4586                assert_eq!(class.spaces[2].metrics.abc.conditions(), 4);
4587            },
4588        );
4589    }
4590
4591    // #1420, the other four spellings of the test above. C# overloads six
4592    // comparison operators and #1297 gated two: `<=` `>=` `==` `!=` are
4593    // distinct tokens that reach a different arm of
4594    // `csharp_count_token_condition` and each still scored one spurious
4595    // condition on its declaration — measured 1 apiece against `<` / `>`'s
4596    // 0, on a class where `cyclomatic()` is 1 for all six.
4597    //
4598    // Each token gets its own member so a partial fix names the spelling
4599    // it missed: gating `EQEQ | BANGEQ` and forgetting `GTEQ | LTEQ` fails
4600    // here by operator, where the file total cannot tell the two apart —
4601    // it reads 4 before the fix and 0 after, so any two-of-four fix halves
4602    // it and still looks like movement.
4603    //
4604    // `n` is the deliberate control (§11): the same four tokens *applied*,
4605    // inside `binary_expression` parents, which must keep scoring one
4606    // condition each. Without it the gate could be satisfied by refusing
4607    // to count these tokens at all.
4608    #[cfg(feature = "csharp")]
4609    #[test]
4610    fn csharp_comparison_operator_overloads_are_not_conditions() {
4611        let src = "class V {
4612                public static bool operator <=(V a, V b) { return true; }
4613                public static bool operator >=(V a, V b) { return true; }
4614                public static bool operator ==(V a, V b) { return true; }
4615                public static bool operator !=(V a, V b) { return true; }
4616                int n(int a, int b) {
4617                    if (a <= b) { return 1; }
4618                    if (a >= b) { return 2; }
4619                    if (a == b) { return 3; }
4620                    if (a != b) { return 4; }
4621                    return 0;
4622                }
4623            }";
4624        // Two of each token: one declaring, one applying. Trimming either
4625        // half out collapses this test into the other half's control and
4626        // is what this anchor exists to catch.
4627        assert_csharp_fixture_spells(
4628            src,
4629            &[
4630                (
4631                    Csharp::OperatorDeclaration as u16,
4632                    4,
4633                    "operator declarations",
4634                ),
4635                (Csharp::LTEQ as u16, 2, "`<=`"),
4636                (Csharp::GTEQ as u16, 2, "`>=`"),
4637                (Csharp::EQEQ as u16, 2, "`==`"),
4638                (Csharp::BANGEQ as u16, 2, "`!=`"),
4639            ],
4640        );
4641        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
4642            let class = &space.spaces[0];
4643            assert_eq!(class.spaces.len(), 5, "four overloads plus `n`");
4644            for (member, spelling) in class.spaces.iter().zip(["<=", ">=", "==", "!="]) {
4645                assert_eq!(
4646                    member.metrics.abc.conditions(),
4647                    0,
4648                    "`operator {spelling}` declares an operator, it does not apply one"
4649                );
4650                assert_eq!(
4651                    member.metrics.abc.conditions(),
4652                    member.metrics.cyclomatic.cyclomatic() - 1,
4653                    "`operator {spelling}`: §8 parity with the cyclomatic decision count"
4654                );
4655            }
4656            // The control, found by name so a reordered fixture cannot
4657            // silently re-point it at an overload. 0 here would mean the
4658            // gate swallowed the applying form along with the declaring
4659            // one.
4660            let control = child_space(class, "n");
4661            assert_eq!(
4662                control.metrics.abc.conditions(),
4663                4,
4664                "`n` applies all four tokens: one condition each"
4665            );
4666        });
4667    }
4668
4669    // The allowlist #1420 installed is the only thing keeping `#if A == B`
4670    // a condition, and it keeps it for a reason no arm states: the grammar
4671    // aliases `preproc_binary_expression` onto `binary_expression`, so the
4672    // preprocessor's `==` arrives with kind id `BinaryExpression` (369)
4673    // and not with one of its own. Before #1420 the token was ungated and
4674    // no fact about its parent mattered; now a grammar bump that gave the
4675    // alias a distinct id would stop counting every `#if` equality with
4676    // nothing red. This is that pin.
4677    //
4678    // `BinaryExpression2` (469) is the pre-alias symbol the arm also
4679    // lists, defensively, per `.claude/rules/grammar-dispatch.md` §1. §2
4680    // asks that such an entry be kept *and* have its unreachability
4681    // asserted, so that a pin promoting the symbol changes behaviour
4682    // loudly rather than invisibly.
4683    #[cfg(feature = "csharp")]
4684    #[test]
4685    fn csharp_preproc_equality_counts_through_the_binary_expression_alias() {
4686        let src = "class P {
4687            #if A == B
4688                int x;
4689            #endif
4690            #if C != D
4691                int y;
4692            #endif
4693            }";
4694        let parser = CsharpParser::new(
4695            src.as_bytes().to_vec(),
4696            std::path::Path::new("foo.cs"),
4697            None,
4698        );
4699        assert!(
4700            ast_has_kind_id(&parser, Csharp::BinaryExpression as u16),
4701            "the `#if` expressions must parse as `binary_expression` — \
4702             the alias is what the arm's allowlist matches"
4703        );
4704        assert!(
4705            !ast_has_kind_id(&parser, Csharp::BinaryExpression2 as u16),
4706            "`BinaryExpression2` is the pre-alias symbol and must stay \
4707             unreachable; if the grammar starts emitting it, re-derive the \
4708             allowlist rather than trusting this arm"
4709        );
4710        // 2, one per `#if` expression. The two directives carry no
4711        // declarations the walk would score, so nothing else contributes.
4712        check_metrics::<CsharpParser>(src, "foo.cs", |metric| {
4713            assert_eq!(metric.abc.conditions_sum(), 2);
4714        });
4715    }
4716
4717    // #1448, the C++ / Mozcpp instance of #1420. C++ overloads seven
4718    // comparison operators; before the fix only `<` / `>` were gated on a
4719    // `binary_expression` parent, so `operator<=` `>=` `==` `!=` and the
4720    // spaceship `operator<=>` each scored one spurious condition on a
4721    // struct where every member has `cyclomatic()` 1.
4722    //
4723    // Members are looked up by name, so a partial fix fails on the
4724    // spelling it missed rather than on a total that merely moved. `n`
4725    // is the §11 control: the same seven tokens applied, which must
4726    // keep scoring one condition each. The census anchors both halves —
4727    // trimming an overload or an application out of the fixture fails
4728    // by name instead of decaying into its counterpart.
4729    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
4730    const CPP_COMPARISON_OVERLOADS: &str = "struct V {
4731            bool operator<(const V& o) const { return true; }
4732            bool operator>(const V& o) const { return true; }
4733            bool operator<=(const V& o) const { return true; }
4734            bool operator>=(const V& o) const { return true; }
4735            bool operator==(const V& o) const { return true; }
4736            bool operator!=(const V& o) const { return true; }
4737            auto operator<=>(const V& o) const { return 0; }
4738        };
4739        struct W {
4740            int n;
4741            bool operator==(const W&) const = default;
4742        };
4743        int n(int a, int b) {
4744            return (a < b) + (a > b) + (a <= b) + (a >= b) + (a == b) + (a != b) + (a <=> b);
4745        }";
4746
4747    // `operator_name` id, then the seven token ids in source order, for
4748    // whichever of the two grammars `P` parses.
4749    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
4750    fn assert_cpp_comparison_overloads_are_not_conditions<P: MetricSuite>(
4751        operator_name: u16,
4752        tokens: [u16; 7],
4753    ) {
4754        #[cfg(any(feature = "cpp", feature = "mozcpp"))]
4755        const SPELLINGS: [&str; 7] = ["<", ">", "<=", ">=", "==", "!=", "<=>"];
4756        let mut census = vec![(operator_name, 8, "operator names")];
4757        // Two of each token, one declaring and one applying, plus the
4758        // third `==` that `W`'s defaulted comparison declares.
4759        census.extend(
4760            tokens
4761                .iter()
4762                .zip(SPELLINGS)
4763                .map(|(&id, s)| (id, if s == "==" { 3 } else { 2 }, s)),
4764        );
4765        assert_fixture_spells::<P>(CPP_COMPARISON_OVERLOADS, "foo.cpp", &census);
4766        check_func_space::<P, _>(CPP_COMPARISON_OVERLOADS, "foo.cpp", |space| {
4767            let class = child_space(&space, "V");
4768            assert_eq!(class.spaces.len(), 7, "seven overloads");
4769            for spelling in SPELLINGS {
4770                let member = child_space(class, &format!("operator{spelling}"));
4771                assert_eq!(
4772                    member.metrics.abc.conditions(),
4773                    0,
4774                    "`operator{spelling}` declares an operator, it does not apply one"
4775                );
4776                assert_eq!(
4777                    member.metrics.abc.conditions(),
4778                    member.metrics.cyclomatic.cyclomatic() - 1,
4779                    "`operator{spelling}`: §8 parity with the cyclomatic decision count"
4780                );
4781            }
4782            assert_eq!(
4783                child_space(&space, "n").metrics.abc.conditions(),
4784                7,
4785                "`n` applies all seven tokens: one condition each"
4786            );
4787            // A defaulted C++20 comparison declares the operator with no
4788            // body; it is the common modern spelling and must score
4789            // nothing anywhere in the file.
4790            assert_eq!(
4791                space.metrics.abc.conditions_sum(),
4792                7,
4793                "only `n`'s seven applications are conditions"
4794            );
4795        });
4796    }
4797
4798    #[cfg(feature = "cpp")]
4799    #[test]
4800    fn cpp_comparison_operator_overloads_are_not_conditions() {
4801        assert_cpp_comparison_overloads_are_not_conditions::<CppParser>(
4802            Cpp::OperatorName as u16,
4803            [
4804                Cpp::LT,
4805                Cpp::GT,
4806                Cpp::LTEQ,
4807                Cpp::GTEQ,
4808                Cpp::EQEQ,
4809                Cpp::BANGEQ,
4810                Cpp::LTEQGT,
4811            ]
4812            .map(|k| k as u16),
4813        );
4814    }
4815
4816    // Mozcpp owns no file extension, so this is its only coverage of the
4817    // arm: no integration snapshot routes to it.
4818    #[cfg(feature = "mozcpp")]
4819    #[test]
4820    fn mozcpp_comparison_operator_overloads_are_not_conditions() {
4821        use crate::Mozcpp;
4822        assert_cpp_comparison_overloads_are_not_conditions::<crate::MozcppParser>(
4823            Mozcpp::OperatorName as u16,
4824            [
4825                Mozcpp::LT,
4826                Mozcpp::GT,
4827                Mozcpp::LTEQ,
4828                Mozcpp::GTEQ,
4829                Mozcpp::EQEQ,
4830                Mozcpp::BANGEQ,
4831                Mozcpp::LTEQGT,
4832            ]
4833            .map(|k| k as u16),
4834        );
4835    }
4836
4837    // ISO C++ spells `!`, `&&`, `||` and `!=` equally as `not`, `and`,
4838    // `or` and `not_eq` ([lex.digraph]), and the C++ grammars give each
4839    // alternative token a kind of its own. ABC tested only the symbols,
4840    // so `if (not b)` scored 0 against `if (!b)`'s 1, `b and c` scored 0
4841    // against `b && c`'s 2, and `b not_eq c` scored 0 against `b != c`'s
4842    // 1. Each row pairs a symbolic spelling with its alternative and the
4843    // value both must give; the census proves the alternative fixture
4844    // really carries the alternative token rather than parsing it as an
4845    // identifier, which is what tree-sitter-c does with the same text.
4846    //
4847    // Arguments: the `not`, `and`, `or` and `not_eq` ids of `P`'s grammar.
4848    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
4849    fn assert_cpp_alternative_tokens_score_like_symbols<P: MetricSuite>(alt: [u16; 4]) {
4850        let [not, and, or, not_eq] = alt;
4851        let rows = [
4852            ("if (!b) {}", "if (not b) {}", not, 1),
4853            ("bool x = !b && c;", "bool x = not b && c;", not, 2),
4854            ("bool x = c ? !b : c;", "bool x = c ? not b : c;", not, 3),
4855            ("bool x = b && c;", "bool x = b and c;", and, 2),
4856            ("if (b || c) {}", "if (b or c) {}", or, 2),
4857            ("if (b != c) {}", "if (b not_eq c) {}", not_eq, 1),
4858        ];
4859        for (symbolic, alternative, token, expected) in rows {
4860            let alt_source = format!("void f(bool b, bool c) {{\n  {alternative}\n}}\n");
4861            assert_fixture_spells::<P>(&alt_source, "foo.cpp", &[(token, 1, alternative)]);
4862            for source in [symbolic, alternative] {
4863                let source = format!("void f(bool b, bool c) {{\n  {source}\n}}\n");
4864                check_func_space::<P, _>(&source, "foo.cpp", |space| {
4865                    assert_eq!(
4866                        space.metrics.abc.conditions_sum(),
4867                        expected,
4868                        "`{source}` must score like its symbolic spelling `{symbolic}`"
4869                    );
4870                });
4871            }
4872        }
4873    }
4874
4875    #[cfg(feature = "cpp")]
4876    #[test]
4877    fn cpp_alternative_tokens_score_like_symbols() {
4878        assert_cpp_alternative_tokens_score_like_symbols::<CppParser>(
4879            [Cpp::Not, Cpp::And, Cpp::Or, Cpp::NotEq].map(|k| k as u16),
4880        );
4881    }
4882
4883    // Mozcpp owns no file extension, so this is its only coverage of the
4884    // alternative tokens: no integration snapshot routes to it.
4885    #[cfg(feature = "mozcpp")]
4886    #[test]
4887    fn mozcpp_alternative_tokens_score_like_symbols() {
4888        use crate::Mozcpp;
4889        assert_cpp_alternative_tokens_score_like_symbols::<crate::MozcppParser>(
4890            [Mozcpp::Not, Mozcpp::And, Mozcpp::Or, Mozcpp::NotEq].map(|k| k as u16),
4891        );
4892    }
4893
4894    // The two other productions #1448's allowlist decides, alongside the
4895    // operator overloads above.
4896    //
4897    // `#if A <= B` is a real decision and must keep counting: the grammar
4898    // aliases `preproc_binary_expression` onto `binary_expression`, and
4899    // the allowlist is the only thing that admits it now that `<=` / `!=`
4900    // are gated. The fixture's two directives are the root's only
4901    // conditions.
4902    //
4903    // A fold over a comparison operator counts once, through the fold's
4904    // `operator` field: before the fix `lt` scored 0, `le` 1 and the
4905    // binary fold `eq` 2 (one per spelling of its single operator).
4906    // `idiom` is the control — its `==` sits in a `binary_expression`
4907    // inside the fold and counts there, while the fold's own `&&` is not
4908    // a comparison: it scores what its twin `(a == 0) && (a == 0)` does,
4909    // 2 (#1533). `neg` is the same twice-spelled operator under the `||`
4910    // chain arm: it scores what `true || !a` does, 2, where walking the
4911    // fold once per spelling would pay 4.
4912    //
4913    // The census also pins §2: every `binary_expression` here carries the
4914    // `BinaryExpression2` id and the pre-alias `BinaryExpression` is never
4915    // emitted. `cpp_operator_is_applied` matches the kind *name*, which
4916    // covers both, so a grammar that starts emitting the pre-alias id
4917    // changes nothing — this census is what would notice it.
4918    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
4919    fn assert_cpp_comparison_folds_and_preproc_operands<P: MetricSuite>(
4920        binary_expression: u16,
4921        binary_expression2: u16,
4922        fold_expression: u16,
4923    ) {
4924        let src = "#if A <= B
4925            #endif
4926            #if C != D
4927            #endif
4928            template <typename... T> bool lt(T... a) { return (a < ...); }
4929            template <typename... T> bool le(T... a) { return (a <= ...); }
4930            template <typename... T> bool eq(T... a) { return (0 == ... == a); }
4931            template <typename... T> bool idiom(T... a) { return ((a == 0) && ...); }
4932            template <typename... T> bool neg(T... a) { return (true || ... || !a); }";
4933        assert_fixture_spells::<P>(
4934            src,
4935            "foo.cpp",
4936            &[
4937                (fold_expression, 5, "fold expressions"),
4938                (binary_expression2, 3, "binary expressions"),
4939                (binary_expression, 0, "pre-alias binary expressions"),
4940            ],
4941        );
4942        check_func_space::<P, _>(src, "foo.cpp", |space| {
4943            for (name, want) in [("lt", 1), ("le", 1), ("eq", 1), ("idiom", 2), ("neg", 2)] {
4944                assert_eq!(
4945                    child_space(&space, name).metrics.abc.conditions(),
4946                    want,
4947                    "fold in `{name}`"
4948                );
4949            }
4950            assert_eq!(
4951                space.metrics.abc.conditions(),
4952                2,
4953                "one condition per `#if` comparison"
4954            );
4955        });
4956    }
4957
4958    #[cfg(feature = "cpp")]
4959    #[test]
4960    fn cpp_comparison_folds_and_preproc_operands() {
4961        assert_cpp_comparison_folds_and_preproc_operands::<CppParser>(
4962            Cpp::BinaryExpression as u16,
4963            Cpp::BinaryExpression2 as u16,
4964            Cpp::FoldExpression as u16,
4965        );
4966    }
4967
4968    #[cfg(feature = "mozcpp")]
4969    #[test]
4970    fn mozcpp_comparison_folds_and_preproc_operands() {
4971        use crate::Mozcpp;
4972        assert_cpp_comparison_folds_and_preproc_operands::<crate::MozcppParser>(
4973            Mozcpp::BinaryExpression as u16,
4974            Mozcpp::BinaryExpression2 as u16,
4975            Mozcpp::FoldExpression as u16,
4976        );
4977    }
4978
4979    // #1383: a `relational_pattern`'s operator is not a condition of
4980    // its own — the `switch_expression_arm` that owns it already scores
4981    // the decision, exactly as it does for the constant arm `5 => 1`.
4982    // Counting both charged a relational arm twice, and twice C#'s own
4983    // cyclomatic decision count.
4984    //
4985    // Per `.claude/rules/grammar-dispatch.md` §11 the `operator <`
4986    // fixture above cannot prove this alone: its `is > 0` sits beside a
4987    // `binary_expression` comparison that supplies a plausible total on
4988    // its own. Here the pattern operators are the *only* `<` / `>` /
4989    // `>=` / `<=` in the file, so a readmitted `RelationalPattern`
4990    // parent is the only thing that can lift the count.
4991    //
4992    // Both spellings are covered, and asserted per member rather than
4993    // through a total, because they reached two different arms when this
4994    // was written: `n`'s `>` / `<` were gated by a `GT | LT` parent
4995    // allowlist and `g`'s `>=` / `<=` by a separate `RelationalPattern`
4996    // denial, so fixing one and not the other left the *other method* at
4997    // 4. #1420 merged all six comparison tokens onto the one allowlist,
4998    // which removes that particular way to get it half right — but the
4999    // per-member shape still earns its keep, since a total of 6 is what
5000    // both `{2, 2, 2}` and a regressed `{4, 2, 0}` report:
5001    //
5002    // | state | `n` | `g` | `c` |
5003    // |---|---|---|---|
5004    // | `relational_pattern` excluded (shipped) | 2 | 2 | 2 |
5005    // | readmitted to the allowlist             | 4 | 4 | 2 |
5006    //
5007    // `c` is the constant-pattern control and reads 2 in both rows: it is
5008    // what the relational methods are supposed to agree with.
5009    #[cfg(feature = "csharp")]
5010    #[test]
5011    fn csharp_relational_pattern_does_not_double_count_its_arm() {
5012        let src = "class A {
5013                int n(int x) => x switch { > 5 => 1, < 0 => 2, _ => 3 };
5014                int g(int x) => x switch { >= 5 => 1, <= 0 => 2, _ => 3 };
5015                int c(int x) => x switch { 5 => 1, 0 => 2, _ => 3 };
5016            }";
5017        assert_csharp_fixture_spells(
5018            src,
5019            &[
5020                (Csharp::RelationalPattern as u16, 4, "relational patterns"),
5021                (Csharp::GT as u16, 1, "`>`"),
5022                (Csharp::LT as u16, 1, "`<`"),
5023                (Csharp::GTEQ as u16, 1, "`>=`"),
5024                (Csharp::LTEQ as u16, 1, "`<=`"),
5025            ],
5026        );
5027        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5028            // Per space, not through the root: the root can only see
5029            // the sum, and a `{4, 2, 0}` regression sums to the same 6
5030            // as the correct `{2, 2, 2}`. `cyclomatic()` parity is also
5031            // only expressible per space — `cyclomatic_sum()` folds in
5032            // a base of 1 for each one.
5033            assert_every_member_scores(&space.spaces[0], 3, 2, "one condition per non-discard arm");
5034        });
5035    }
5036
5037    // The `is` half of #1383, and the one whose over-count came from a
5038    // different owner: `if (x is > 0)` scored the `IfStatement`
5039    // condition slot *and* the pattern's `>`. `f`'s guard is the
5040    // deliberate control — a `binary_expression` comparison in the same
5041    // slot still scores exactly one, so the fix cannot be read as
5042    // "stop counting `if` conditions".
5043    //
5044    // The two combinator methods pin the compound forms the issue asks
5045    // about. `and` / `or` are keyword tokens C# ABC never listed and C#
5046    // cyclomatic never counts, so the arm stays the single decision
5047    // however many relational operands it carries — which is exactly
5048    // what would regress if a later fix re-derived the gate from the
5049    // operand instead of the parent.
5050    #[cfg(feature = "csharp")]
5051    #[test]
5052    fn csharp_is_pattern_and_combinators_score_one_decision() {
5053        let src = "class A {
5054                int m(int x) { if (x is > 0) { return 1; } return 0; }
5055                int f(int x) { if (x > 0) { return 1; } return 0; }
5056                int a(int x) { if (x is > 0 and < 9) { return 1; } return 0; }
5057                int o(int x) { if (x is < 0 or >= 100) { return 1; } return 0; }
5058                int n(int x) { if (x is not > 5) { return 1; } return 0; }
5059            }";
5060        assert_csharp_fixture_spells(
5061            src,
5062            &[
5063                (Csharp::RelationalPattern as u16, 6, "relational patterns"),
5064                (Csharp::IsPatternExpression as u16, 4, "`is` pattern tests"),
5065                (Csharp::AndPattern as u16, 1, "`and`"),
5066                (Csharp::OrPattern as u16, 1, "`or`"),
5067                (Csharp::NegatedPattern as u16, 1, "`not`"),
5068                (Csharp::GTEQ as u16, 1, "`>=` (only `o` carries one)"),
5069            ],
5070        );
5071        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5072            assert_every_member_scores(&space.spaces[0], 5, 1, "the `if` is the only decision");
5073        });
5074    }
5075
5076    // #1383's second, quieter effect, and the one its issue does not
5077    // mention: a relational pattern outside any decision slot went from
5078    // 1 to 0 as well, because the gate is on the operator's parent and
5079    // not on what encloses the pattern. That left `q` — the equivalent
5080    // binary comparison `x > 5` in the same slot — scoring 1 against
5081    // these four's 0, so a relational pattern read one *lower* than the
5082    // comparison it is sugar for. #1383 recorded that as a deliberate
5083    // exception to Fitzpatrick Rule 5 and kept it, because the only
5084    // route it had was to readmit the pattern's `<` / `>` token, which
5085    // would have scored the decision-slot case twice.
5086    //
5087    // #1461 closed it from the other end, and the test now pins the
5088    // agreement rather than the exception. The enclosing
5089    // `is_pattern_expression` scores by use, so `x is > 5` reads 1
5090    // wherever it is written — level with `q`, and level with the plain
5091    // type test `x is int` (`t`, whose `is_expression` moved for the
5092    // same reason). The pattern's own `>` still scores nothing, which
5093    // is what keeps the whole test at 1 rather than 2: one relational
5094    // operation, one condition.
5095    //
5096    // The four probes stay because they are four different enclosings —
5097    // a declarator, a bare `return`, an argument, a type test — and the
5098    // point was never that the pattern is special in one of them.
5099    #[cfg(feature = "csharp")]
5100    #[test]
5101    fn csharp_relational_pattern_scores_one_wherever_it_is_written() {
5102        let src = "class A {
5103                static bool M(bool b) { return b; }
5104                bool p(int x) { bool b = x is > 5; return b; }
5105                bool q(int x) { bool b = x > 5; return b; }
5106                bool r(int x) { return x is > 5; }
5107                bool s(int x) { return M(x is > 5); }
5108                bool t(object x) { bool b = x is int; return b; }
5109            }";
5110        assert_csharp_fixture_spells(
5111            src,
5112            &[
5113                (Csharp::RelationalPattern as u16, 3, "relational patterns"),
5114                (Csharp::IsPatternExpression as u16, 3, "`is` pattern tests"),
5115                (Csharp::IsExpression as u16, 1, "the `is int` control"),
5116            ],
5117        );
5118        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5119            let class = &space.spaces[0];
5120            assert_eq!(class.spaces.len(), 6, "`M` plus five probes");
5121            let by_name = |n: &str| {
5122                class
5123                    .spaces
5124                    .iter()
5125                    .find(|s| s.name.as_deref() == Some(n))
5126                    .unwrap_or_else(|| panic!("fixture lost `{n}`"))
5127                    .metrics
5128                    .abc
5129                    .conditions()
5130            };
5131            // Named rather than indexed: the claim is about which
5132            // spelling scores what, so a reordering of the fixture must
5133            // not silently re-point the assertions.
5134            for probe in ["p", "r", "s"] {
5135                assert_eq!(by_name(probe), 1, "`{probe}`: the `is` test, once");
5136            }
5137            assert_eq!(by_name("t"), 1, "the bare type test, the same once");
5138            assert_eq!(by_name("q"), 1, "the comparison it is sugar for");
5139            assert_eq!(by_name("M"), 0, "no relational operator anywhere");
5140        });
5141    }
5142
5143    // The boundary of #1383: the pattern's operator stops counting, a
5144    // *binary* operator in the arm's `when` guard keeps counting (`w`).
5145    // Both sit under the same `switch_expression_arm`, so this is what
5146    // stops a later "patterns don't count" pass from suppressing the
5147    // guard too.
5148    //
5149    // A relational *pattern* in the guard (`g`) is the other side of that
5150    // line: the operator itself still scores nothing, because the gate is
5151    // on the operator's parent — but since #1422 the guard is a condition
5152    // slot, and an `is` test scores itself (`csharp_condition_scores_itself`),
5153    // so the slot does not pay again. That is what closed the gap this test used to
5154    // carry a `FIXME(#1422)` for: `when n is > 5` now reads level with
5155    // `when n > 5` instead of one below it.
5156    //
5157    // §8 holds throughout, which it did not before #1422. The old table
5158    // here recorded the defect: neither metric modelled the guard, so
5159    // ABC's count was whatever operator token happened to sit inside it
5160    // and cyclomatic's was nothing at all. Both tiers measured, before
5161    // and after:
5162    //
5163    // | guard | `conditions()` | `cyclomatic() - 1` |
5164    // |---|---|---|
5165    // | `when x % 2 == 0` (this fixture) | 3 → 3 | 2 → 3 |
5166    // | `when x > 2`                     | 3 → 3 | 2 → 3 |
5167    // | `when IsEven(x)`                 | 2 → 3 | 2 → 3 |
5168    //
5169    // The `==` is the guard's own operator and the only `==` in the
5170    // file, so trimming the `when` clause out of `w` drops its count to
5171    // 2 rather than leaving the assertion satisfied by something else;
5172    // `assert_csharp_fixture_spells` pins both guards by kind against
5173    // the same decay.
5174    #[cfg(feature = "csharp")]
5175    #[test]
5176    fn csharp_switch_arm_guard_operator_still_counts() {
5177        let src = "class A {
5178                int w(int x) => x switch { > 0 when x % 2 == 0 => 1, > 0 => 2, _ => 3 };
5179                int g(int x) => x switch { int n when n is > 5 => 1, _ => 0 };
5180            }";
5181        assert_csharp_fixture_spells(
5182            src,
5183            &[
5184                (Csharp::WhenClause as u16, 2, "`when` guards"),
5185                (Csharp::EQEQ as u16, 1, "the guard's `==`"),
5186                (Csharp::RelationalPattern as u16, 3, "relational patterns"),
5187            ],
5188        );
5189        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5190            let m = &space.spaces[0].spaces[0];
5191            assert_eq!(m.name.as_deref(), Some("w"));
5192            // 4 if the guard slot started counting a `binary_expression`
5193            // on top of its `==`, 2 if the `==` were suppressed along
5194            // with the pattern operators — both are live regressions.
5195            assert_eq!(
5196                m.metrics.abc.conditions(),
5197                3,
5198                "two arms plus the guard's `==`"
5199            );
5200            assert_eq!(m.metrics.cyclomatic.cyclomatic(), 4);
5201
5202            let g = &space.spaces[0].spaces[1];
5203            assert_eq!(g.name.as_deref(), Some("g"));
5204            assert_eq!(
5205                g.metrics.abc.conditions(),
5206                2,
5207                "one arm plus the guard slot, which scores the `is` test"
5208            );
5209            assert_eq!(g.metrics.cyclomatic.cyclomatic(), 3);
5210        });
5211    }
5212
5213    // #1422's headline: four spellings of one guard, previously worth
5214    // two different ABC numbers because nothing modelled the guard and
5215    // ABC counted whatever operator token happened to sit inside it —
5216    // `when x % 2 == 0` and `when x > 2` scored 1 through the
5217    // comparison-token arm, `when IsEven(x)` scored 0. Modelled as a
5218    // condition slot each contributes exactly 1, and the cyclomatic
5219    // `WhenClause` arm lifts the decision count to match.
5220    //
5221    // `none` is the unguarded control and the anchor: a two-arm switch
5222    // over the same relational patterns carrying no guard at all, so
5223    // every member here is exactly one guard away from it. Without that
5224    // row a fix that scored the *arms* differently would satisfy the
5225    // four guarded rows alike. Its arms run `> 2` before `> 0` because
5226    // the reverse order is a C# compile error (CS8510, the later arm
5227    // subsumed) — the guarded members escape that only because a guard
5228    // can fail, which is what makes their repeated `> 0` legal.
5229    //
5230    // `paren` is not a fourth spelling for its own sake — it is the only
5231    // member whose guard reaches the slot as a
5232    // `parenthesized_expression` for the peel (`csharp_wrapper_operand`)
5233    // to take off, since `when_clause` wraps a parenthesised guard in a
5234    // real `parenthesized_expression` rather than the anonymous parens
5235    // `catch_filter_clause` uses.
5236    //
5237    // `nullc` is #1459's row and the counter-example that falsified the
5238    // claim above when it was first written. The slot leaves an
5239    // operator guard to the arm that already counts the operator, and
5240    // no ABC arm counted `??`, so this one spelling read 2 where the
5241    // other four read 3. Counting the token levels it without touching
5242    // the slot; an unconditional `+1` in the slot instead would have
5243    // taken `cmp` to 4, re-creating the double count #1422 removed.
5244    //
5245    // It is the one guarded member whose cyclomatic is not 4, and that
5246    // is not a discrepancy to fix: C# cyclomatic counts `??` as a
5247    // decision *in addition to* the guard clause, so the arm below
5248    // narrows the ABC-versus-cyclomatic gap from two to one rather than
5249    // closing it. The remaining one is the slot's standing policy of
5250    // leaving a counted operator to its arm, and `cmp` pays it
5251    // too — it just happens to break even there.
5252    #[cfg(feature = "csharp")]
5253    #[test]
5254    fn csharp_switch_arm_guard_scores_one_condition_however_spelled() {
5255        let src = "class A {
5256                static bool IsEven(int x) { return true; }
5257                int tok(int x) => x switch { > 0 when x % 2 == 0 => 1, > 0 => 2, _ => 3 };
5258                int cmp(int x) => x switch { > 0 when x > 2 => 1, > 0 => 2, _ => 3 };
5259                int call(int x) => x switch { > 0 when IsEven(x) => 1, > 0 => 2, _ => 3 };
5260                int paren(int x) => x switch { > 0 when (IsEven(x)) => 1, > 0 => 2, _ => 3 };
5261                int nullc(int x, bool? b) => x switch { > 0 when b ?? false => 1, > 0 => 2, _ => 3 };
5262                int none(int x) => x switch { > 2 => 1, > 0 => 2, _ => 3 };
5263            }";
5264        assert_csharp_fixture_spells(
5265            src,
5266            &[
5267                (Csharp::WhenClause as u16, 5, "`when` guards"),
5268                (Csharp::EQEQ as u16, 1, "`tok`'s `==`"),
5269                (Csharp::QMARKQMARK as u16, 1, "`nullc`'s `??`"),
5270                (
5271                    Csharp::ParenthesizedExpression as u16,
5272                    1,
5273                    "`paren`'s parenthesised guard",
5274                ),
5275                // By the call's *argument list*, which carries no
5276                // numeric-suffix alias, rather than by
5277                // `invocation_expression`, which has three
5278                // (`InvocationExpression` / `2` / `3`) and would pin this
5279                // anchor to whichever one the pinned grammar happens to
5280                // emit. `call` and `paren` hold the only two argument
5281                // lists in the fixture — `IsEven(int x)` is a
5282                // `parameter_list`.
5283                (Csharp::ArgumentList as u16, 2, "the `IsEven` calls"),
5284            ],
5285        );
5286        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5287            assert_members_score(
5288                &space.spaces[0],
5289                &[
5290                    ("IsEven", 0, 1),
5291                    ("tok", 3, 4),
5292                    ("cmp", 3, 4),
5293                    ("call", 3, 4),
5294                    ("paren", 3, 4),
5295                    // Was 2 — the guard spelling nothing counted.
5296                    // Cyclomatic 5 rather than 4 because `??` is a
5297                    // decision there on top of the guard clause.
5298                    ("nullc", 3, 5),
5299                    ("none", 2, 3),
5300                ],
5301            );
5302        });
5303    }
5304
5305    // The bare type test `x is int` is `is_expression` (391); only once
5306    // a pattern is involved (`x is int y`, `x is null`) does the grammar
5307    // emit `is_pattern_expression` (392). They are distinct kinds, and
5308    // the former `csharp_bool_terminal_kinds!()` listed only the
5309    // second, so the two spellings of one test disagreed: `if (x is int)` scored zero
5310    // conditions against a cyclomatic decision of one while
5311    // `if (x is int y)` scored one.
5312    //
5313    // Found by the whole-branch review of this batch, and it falsified
5314    // #1422's own claim in the arm above that a guard "scores one
5315    // however it is spelled" — `when x is int` read 1 where
5316    // `when IsEven(x)` read 2, which is the spelling-dependence that
5317    // fix exists to remove. It is the C# half of the gap #1421 closed
5318    // for Kotlin by adding `IsExpression | InExpression` there. Both
5319    // languages' type tests have since moved out of the terminal set to
5320    // an unconditional arm (#1461), which leaves every number here
5321    // unchanged — the slot no longer counts the test and the arm does.
5322    //
5323    // The plain `if` members are the control that keeps this honest: a
5324    // guard-only fixture could be satisfied by a guard-specific rule,
5325    // and the defect was never guard-specific. `q` and `iq` are the
5326    // already-correct `is_pattern_expression` twins, so a regression
5327    // that reintroduced the asymmetry fails on the pair rather than on
5328    // an absolute number.
5329    #[cfg(feature = "csharp")]
5330    #[test]
5331    fn csharp_bare_is_type_test_scores_one_condition() {
5332        let src = "class A {
5333                static bool IsEven(int x) { return true; }
5334                int g(object x) => x switch { object o when x is int => 1, _ => 0 };
5335                int gq(object x) => x switch { object o when x is int y => 1, _ => 0 };
5336                int gc(object x) => x switch { object o when IsEven(1) => 1, _ => 0 };
5337                int p(object x) { if (x is int) { return 1; } return 0; }
5338                int q(object x) { if (x is int y) { return 1; } return 0; }
5339            }";
5340        assert_csharp_fixture_spells(
5341            src,
5342            &[
5343                (Csharp::IsExpression as u16, 2, "the bare `is` type tests"),
5344                (
5345                    Csharp::IsPatternExpression as u16,
5346                    2,
5347                    "the designation-pattern twins",
5348                ),
5349                (Csharp::WhenClause as u16, 3, "`when` guards"),
5350            ],
5351        );
5352        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5353            assert_members_score(
5354                &space.spaces[0],
5355                &[
5356                    ("IsEven", 0, 1),
5357                    // Level with `gq` and `gc`: every guard spelling
5358                    // scores one, which is what #1422 claims.
5359                    ("g", 2, 3),
5360                    ("gq", 2, 3),
5361                    ("gc", 2, 3),
5362                    // Level with `q`: the two spellings of one type
5363                    // test agree outside a guard too.
5364                    ("p", 1, 2),
5365                    ("q", 1, 2),
5366                ],
5367            );
5368        });
5369    }
5370
5371    // The null-forgiving `b!` is transparent: it scores whatever the
5372    // operand it wraps scores (#1463).
5373    //
5374    // `postfix_unary_expression` was in neither
5375    // the former `csharp_bool_terminal_kinds!()` nor the wrapper peel,
5376    // so every spelling below scored **zero** conditions against a cyclomatic
5377    // decision of one, while the bare `b` control scored one. In a
5378    // codebase with nullable reference types enabled the suffix is
5379    // everywhere, so the gap is not an exotic corner.
5380    //
5381    // Both walker paths, per `.claude/rules/grammar-dispatch.md` §11:
5382    // `p*` go through the `if` predicate slot
5383    // (`csharp_count_condition`) and `c*` through the `&&` chain slot
5384    // (also `csharp_count_condition`, per chain operand), which reach
5385    // the peel independently. A fixture of only one
5386    // reads correct with the other path dead.
5387    //
5388    // The controls are load-bearing in both directions. `p` / `c` pin
5389    // what the wrapper must score, so a regression fails on the pair
5390    // rather than on an absolute number; and the cyclomatic column pins
5391    // that only `conditions` moved — the suffix is not a decision in any
5392    // language, so a fix that moved both together would be a different
5393    // bug.
5394    #[cfg(feature = "csharp")]
5395    #[test]
5396    fn csharp_null_forgiving_operand_scores_like_its_operand() {
5397        let src = "class A {
5398                int p(bool b) { if (b) { return 1; } return 0; }
5399                int pn(bool b) { if (b!) { return 1; } return 0; }
5400                int pp(bool b) { if ((b!)) { return 1; } return 0; }
5401                int pd(bool b) { if (b!!) { return 1; } return 0; }
5402                int pb(bool b) { if (!b!) { return 1; } return 0; }
5403                int c(bool b) { if (b && b) { return 1; } return 0; }
5404                int cn(bool b) { if (b! && b) { return 1; } return 0; }
5405                int cp(bool b) { if ((b!) && b) { return 1; } return 0; }
5406            }";
5407        assert_csharp_fixture_spells(
5408            src,
5409            &[
5410                // Seven, not five: `b!!` is two nested nodes, and each
5411                // chain member carries one. Trimming a `!` out of any
5412                // member fails here by name instead of turning that
5413                // member into a silent copy of its control.
5414                (
5415                    Csharp::PostfixUnaryExpression as u16,
5416                    7,
5417                    "the null-forgiving suffixes",
5418                ),
5419                // The `if (…)` parens are anonymous tokens rather than
5420                // nodes in this grammar, so these two are `pp`'s and
5421                // `cp`'s explicit ones — the wrappers that make the peel
5422                // chain rather than peel once.
5423                (
5424                    Csharp::ParenthesizedExpression as u16,
5425                    2,
5426                    "the explicit parenthesised spellings",
5427                ),
5428                (Csharp::PrefixUnaryExpression as u16, 1, "`pb`'s `!`"),
5429            ],
5430        );
5431        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5432            assert_members_score(
5433                &space.spaces[0],
5434                &[
5435                    ("p", 1, 2),
5436                    ("pn", 1, 2),
5437                    ("pp", 1, 2),
5438                    ("pd", 1, 2),
5439                    ("pb", 1, 2),
5440                    ("c", 2, 3),
5441                    ("cn", 2, 3),
5442                    ("cp", 2, 3),
5443                ],
5444            );
5445        });
5446    }
5447
5448    // The null-forgiving operand is read by position, but from the end
5449    // the grammar cannot move: a `postfix_unary_expression` *starts* at
5450    // its operand, so `child(0)` is the operand whatever `extra` sits
5451    // between it and the operator. The operator is identified by
5452    // membership rather than by index for the same reason.
5453    //
5454    // The other two wrappers name no field either — the C# grammar gives
5455    // `parenthesized_expression`, `prefix_unary_expression` and
5456    // `postfix_unary_expression` an empty `fields` map in
5457    // node-types.json — so `child(1)` was a comment whenever one was
5458    // written there, and `nc` / `rc` scored zero. They read their
5459    // operand as the first one that is not an extra since #1455, so all
5460    // three pairs agree with their controls.
5461    #[cfg(feature = "csharp")]
5462    #[test]
5463    fn csharp_null_forgiving_operand_survives_an_interposed_comment() {
5464        let src = "class A {
5465                int p(bool b) { if (b!) { return 1; } return 0; }
5466                int pc(bool b) { if (b /*c*/ !) { return 1; } return 0; }
5467                int n(bool b) { if (!b) { return 1; } return 0; }
5468                int nc(bool b) { if (! /*c*/ b) { return 1; } return 0; }
5469                int r(bool b) { if ((b)) { return 1; } return 0; }
5470                int rc(bool b) { if (( /*c*/ b)) { return 1; } return 0; }
5471            }";
5472        assert_csharp_fixture_spells(
5473            src,
5474            &[
5475                (
5476                    Csharp::Comment as u16,
5477                    3,
5478                    "the interposed comments — without them every row is its own control",
5479                ),
5480                (Csharp::PostfixUnaryExpression as u16, 2, "`p` and `pc`"),
5481                (Csharp::PrefixUnaryExpression as u16, 2, "`n` and `nc`"),
5482                (
5483                    Csharp::ParenthesizedExpression as u16,
5484                    2,
5485                    "`r` and `rc` — the `if` parens are anonymous tokens",
5486                ),
5487            ],
5488        );
5489        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5490            assert_members_score(
5491                &space.spaces[0],
5492                &[
5493                    ("p", 1, 2),
5494                    // The row #1463 fixes.
5495                    ("pc", 1, 2),
5496                    ("n", 1, 2),
5497                    // #1455: `child(1)` of the prefix wrapper is the
5498                    // comment.
5499                    ("nc", 1, 2),
5500                    ("r", 1, 2),
5501                    // #1455 again, through the paren wrapper.
5502                    ("rc", 1, 2),
5503                ],
5504            );
5505        });
5506    }
5507
5508    // The statement `switch` reaches the same `when_clause` kind through
5509    // a different parent — `switch_section` rather than
5510    // `switch_expression_arm` — which makes it an independent path in
5511    // the sense of `.claude/rules/grammar-dispatch.md` §11: the arms
5512    // themselves are counted by two different owners (a `Case` token
5513    // here, the `SwitchExpressionArm` node there), so a guard rule
5514    // written against one shape could be dead for the other and every
5515    // expression-form fixture would still read correct.
5516    #[cfg(feature = "csharp")]
5517    #[test]
5518    fn csharp_statement_switch_section_guard_counts() {
5519        let src = "class A {
5520                static bool IsEven(int x) { return true; }
5521                int guarded(int x) {
5522                    switch (x) {
5523                        case > 0 when IsEven(x): return 1;
5524                        case > 0: return 2;
5525                        default: return 3;
5526                    }
5527                }
5528                int plain(int x) {
5529                    switch (x) {
5530                        case > 2: return 1;
5531                        case > 0: return 2;
5532                        default: return 3;
5533                    }
5534                }
5535            }";
5536        assert_csharp_fixture_spells(
5537            src,
5538            &[
5539                (Csharp::WhenClause as u16, 1, "the `case … when` guard"),
5540                (Csharp::SwitchSection as u16, 6, "switch sections"),
5541                (
5542                    Csharp::SwitchExpressionArm as u16,
5543                    0,
5544                    "expression arms (this fixture is the statement form)",
5545                ),
5546            ],
5547        );
5548        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5549            assert_members_score(
5550                &space.spaces[0],
5551                &[("IsEven", 0, 1), ("guarded", 3, 4), ("plain", 2, 3)],
5552            );
5553        });
5554    }
5555
5556    // The sibling #1422's issue body does not mention, measured to have
5557    // the identical defect: `catch (E e) when (…)` is a second guard
5558    // spelling on its own `catch_filter_clause` kind, and it scored
5559    // conditions 3 / 2 / 2 for the comparison / call spellings and the
5560    // unfiltered control while `cyclomatic()` sat at 2 for all three.
5561    //
5562    // ABC runs one above `cyclomatic() - 1` on every row, guarded and
5563    // unguarded alike, and that offset is not this fix's: C# ABC counts
5564    // the `Try` token as a condition (Fitzpatrick Rule 5, the same
5565    // family as `else`) where cyclomatic counts only the `catch`. The
5566    // guard moves both tiers by exactly one, which is what `none`
5567    // anchors — the offset is a constant here, not something a guard
5568    // introduces.
5569    //
5570    // `paren` carries the *double* parenthesis on purpose. The grammar
5571    // spells `catch_filter_clause`'s own parentheses as anonymous tokens
5572    // the way `if_statement` does, so `when (IsEven(x))` hands the slot a
5573    // bare `invocation_expression` with no parenthesis for the peel
5574    // (`csharp_wrapper_operand`) to take off; only a
5575    // second pair of parentheses produces a `parenthesized_expression`
5576    // there.
5577    #[cfg(feature = "csharp")]
5578    #[test]
5579    fn csharp_catch_filter_guard_scores_one_condition_however_spelled() {
5580        let src = "class A {
5581                static bool IsEven(int x) { return true; }
5582                int tok(int x) { try { return 1; } catch (System.Exception e) when (x % 2 == 0) { return 2; } }
5583                int cmp(int x) { try { return 1; } catch (System.Exception e) when (x > 0) { return 2; } }
5584                int call(int x) { try { return 1; } catch (System.Exception e) when (IsEven(x)) { return 2; } }
5585                int paren(int x) { try { return 1; } catch (System.Exception e) when ((IsEven(x))) { return 2; } }
5586                int none(int x) { try { return 1; } catch (System.Exception e) { return 2; } }
5587            }";
5588        assert_csharp_fixture_spells(
5589            src,
5590            &[
5591                (
5592                    Csharp::CatchFilterClause as u16,
5593                    4,
5594                    "`catch … when` filters",
5595                ),
5596                (Csharp::CatchClause as u16, 5, "catch clauses"),
5597                (Csharp::WhenClause as u16, 0, "switch guards (none here)"),
5598                (
5599                    Csharp::ParenthesizedExpression as u16,
5600                    1,
5601                    "`paren`'s inner parentheses",
5602                ),
5603            ],
5604        );
5605        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5606            assert_members_score(
5607                &space.spaces[0],
5608                &[
5609                    ("IsEven", 0, 1),
5610                    ("tok", 3, 3),
5611                    ("cmp", 3, 3),
5612                    ("call", 3, 3),
5613                    ("paren", 3, 3),
5614                    ("none", 2, 2),
5615                ],
5616            );
5617        });
5618    }
5619
5620    // A guarded discard is where the two rules meet. `_ when g =>` is
5621    // already exempt from the `default:` exclusion —
5622    // `csharp_switch_expression_arm_is_bare_discard` looks for a
5623    // `when_clause` for exactly this reason — so after #1422 it scores
5624    // twice: once as an arm that is no longer unconditional, once for
5625    // the guard that makes it conditional. `bare` is the control that
5626    // keeps the exclusion itself pinned: drop the guard and the arm goes
5627    // back to costing nothing.
5628    #[cfg(feature = "csharp")]
5629    #[test]
5630    fn csharp_guarded_discard_arm_scores_arm_and_guard() {
5631        let src = "class A {
5632                static bool IsEven(int x) { return true; }
5633                int guarded(int x) => x switch { > 0 => 1, _ when IsEven(x) => 2, _ => 3 };
5634                int bare(int x) => x switch { > 0 => 1, _ => 3 };
5635            }";
5636        assert_csharp_fixture_spells(
5637            src,
5638            &[
5639                (Csharp::WhenClause as u16, 1, "the guarded discard"),
5640                (Csharp::Discard as u16, 3, "discard patterns"),
5641            ],
5642        );
5643        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5644            assert_members_score(
5645                &space.spaces[0],
5646                &[("IsEven", 0, 1), ("guarded", 3, 4), ("bare", 1, 2)],
5647            );
5648        });
5649    }
5650
5651    // A comment inside the guard must not cost it its condition.
5652    // tree-sitter `extra`s are *named* nodes, so `comment` can be the
5653    // first named child of either guard clause — and a slot that read
5654    // only the first named child scored `when /*c*/ IsEven(x)` as 1
5655    // where `when IsEven(x)` scores 2, silently reinstating the very
5656    // spelling-dependence #1422 removes and breaking the §8 parity the
5657    // fix establishes. Found in review of #1422 before it shipped.
5658    //
5659    // This is the C# instance of the class #1181 fixed for the ternary,
5660    // where a comment between a token and its operand shifted every
5661    // positional read. Each commented member is asserted against the
5662    // same literal pair as its uncommented twin in the same table, so a
5663    // regression that moved only the commented spelling fails here; the
5664    // literals themselves are pinned by
5665    // `csharp_switch_arm_guard_scores_one_condition_however_spelled` and
5666    // its catch-filter sibling.
5667    #[cfg(feature = "csharp")]
5668    #[test]
5669    fn csharp_guard_keeps_its_condition_across_a_comment() {
5670        let src = "class A {
5671                static bool IsEven(int x) => true;
5672                int plain(int x) => x switch { > 0 when IsEven(x) => 1, _ => 0 };
5673                int cmt(int x) => x switch { > 0 when /*c*/ IsEven(x) => 1, _ => 0 };
5674                int cplain(int x) { try { return 1; } catch (System.Exception e) when (IsEven(x)) { return 2; } }
5675                int ccmt(int x) { try { return 1; } catch (System.Exception e) when (/*c*/ IsEven(x)) { return 2; } }
5676            }";
5677        assert_csharp_fixture_spells(
5678            src,
5679            &[
5680                (Csharp::Comment as u16, 2, "the in-guard comments"),
5681                (Csharp::WhenClause as u16, 2, "switch guards"),
5682                (Csharp::CatchFilterClause as u16, 2, "catch filters"),
5683            ],
5684        );
5685        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5686            assert_members_score(
5687                &space.spaces[0],
5688                &[
5689                    ("IsEven", 0, 1),
5690                    ("plain", 2, 3),
5691                    ("cmt", 2, 3),
5692                    ("cplain", 3, 3),
5693                    ("ccmt", 3, 3),
5694                ],
5695            );
5696        });
5697    }
5698
5699    // Why the guard is a condition *slot* and not a flat +1: a compound
5700    // guard keeps its sub-structure. `when a > 1 && b < 2` scores the
5701    // two comparisons through the token arm and nothing extra from the
5702    // slot, because a `&&` chain scores itself
5703    // (`csharp_condition_scores_itself`) — so it reads one above `single`'s
5704    // lone comparison rather than collapsing to the same number, and
5705    // cyclomatic agrees because it counts the `&&`.
5706    #[cfg(feature = "csharp")]
5707    #[test]
5708    fn csharp_compound_guard_keeps_its_sub_structure() {
5709        let src = "class A {
5710                int compound(int a, int b) => a switch { > 0 when a > 1 && b < 2 => 1, _ => 0 };
5711                int single(int a, int b) => a switch { > 0 when a > 1 => 1, _ => 0 };
5712            }";
5713        assert_csharp_fixture_spells(
5714            src,
5715            &[
5716                (Csharp::WhenClause as u16, 2, "`when` guards"),
5717                (Csharp::AMPAMP as u16, 1, "`compound`'s `&&`"),
5718                (Csharp::LT as u16, 1, "`compound`'s `<`"),
5719            ],
5720        );
5721        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5722            assert_members_score(&space.spaces[0], &[("compound", 3, 4), ("single", 2, 3)]);
5723        });
5724    }
5725
5726    // #1275: tree-sitter-c-sharp spells `int?`, `where T : class?`, the
5727    // ternary and `a?.b` with one and the same bare `?` token, so the
5728    // unguarded `QMARK` arm scored the two type-syntax forms as
5729    // decisions.
5730    //
5731    // The fixture carries two `nullable_type` `?`, one
5732    // `type_parameter_constraint` `?`, and one real ternary over a real
5733    // `>` comparison, so every way of getting the gate wrong lands on
5734    // its own number: 5 pre-fix, 3 if only `NullableType` is denied, 4
5735    // if only `TypeParameterConstraint` is, 1 if the gate swallows the
5736    // genuine ternary, 0 if the fixture stops parsing. Asserting 0 on a
5737    // nullable-only body would have been vacuous — an unparsable file
5738    // scores 0 too.
5739    #[cfg(feature = "csharp")]
5740    #[test]
5741    fn csharp_nullable_type_syntax_is_not_a_condition() {
5742        check_metrics::<CsharpParser>(
5743            "class A {
5744                int M<T>(int? p, int a, int b) where T : class? {
5745                    int? q = null;
5746                    return a > b ? 1 : 2;
5747                }
5748            }",
5749            "foo.cs",
5750            |metric| {
5751                assert_eq!(metric.abc.conditions_sum(), 2);
5752            },
5753        );
5754    }
5755
5756    // The polarity pin for #1275's C# half. `csharp_count_token_condition`
5757    // denies the two type-syntax parents rather than allowing the two
5758    // decision-bearing ones, so that `a?.b` and `a?[0]` keep counting by
5759    // construction: both spell their operator as the same bare `?` under
5760    // `conditional_access_expression`, and C# cyclomatic counts that node
5761    // (`safe_navigation_chain_parity` pins C# at one decision per
5762    // operator alongside every other safe-navigation language).
5763    //
5764    // This test is what stops a later "make all three languages
5765    // consistent" pass from flipping C# to a `ConditionalExpression`
5766    // allowlist: that would silently drop both `?` counts here while
5767    // every other ABC test still passed. The third condition is the
5768    // `??`, which #1459 added to close the gap this comment used to
5769    // record as out of scope for #1275 — so a flipped `QMARK` polarity
5770    // now reads 1 rather than 0, and the discrimination is unchanged.
5771    #[cfg(feature = "csharp")]
5772    #[test]
5773    fn csharp_conditional_access_still_counts_as_a_condition() {
5774        let src = "class A {
5775                object M(string s, int[] xs) {
5776                    return s?.Length ?? xs?[0];
5777                }
5778            }";
5779        // The total is now fed by two token arms rather than one, so it
5780        // carries the per-source claim only alongside these counts: 3 is
5781        // two bare `?` plus one `??`, and without them a `QMARK` arm
5782        // that had stopped counting `a?[0]` while `??` gained a second
5783        // count elsewhere would still read 3.
5784        assert_csharp_fixture_spells(
5785            src,
5786            &[
5787                (Csharp::QMARK as u16, 2, "the `?.` and `?[` operators"),
5788                (Csharp::QMARKQMARK as u16, 1, "the `??`"),
5789            ],
5790        );
5791        check_metrics::<CsharpParser>(src, "foo.cs", |metric| {
5792            assert_eq!(metric.abc.conditions_sum(), 3);
5793        });
5794    }
5795
5796    // The `QMARKQMARK` arm away from any guard, where nothing else can
5797    // supply the count (#1459). `coalesce`'s body holds no comparison,
5798    // no call and no control-flow keyword, so its one condition is the
5799    // `??` and nothing else; it read 0 before the arm while C#
5800    // cyclomatic scored the decision, which is the direction ABC is not
5801    // allowed to sit on.
5802    //
5803    // `plain` is the control that keeps this from being an assertion
5804    // about `return` or about the method shape: same body without the
5805    // operator, 0 conditions and 1 decision.
5806    #[cfg(feature = "csharp")]
5807    #[test]
5808    fn csharp_null_coalescing_is_a_condition() {
5809        let src = "class A {
5810                int coalesce(int? x) { return x ?? 0; }
5811                int plain(int x) { return x; }
5812            }";
5813        assert_csharp_fixture_spells(src, &[(Csharp::QMARKQMARK as u16, 1, "the `??`")]);
5814        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5815            assert_members_score(&space.spaces[0], &[("coalesce", 1, 2), ("plain", 0, 1)]);
5816        });
5817    }
5818
5819    // The other direction of #1275's C# gate, isolated: narrowing
5820    // `QMARK` must not swallow a real ternary. The fixture carries no
5821    // comparison operator at all, so the ternary is the only thing that
5822    // can put the count above the walker's own contribution — in the
5823    // test above, a surviving `>` would have masked an over-suppressed
5824    // `?` at 1 instead of 0 (grammar-dispatch §11). A nullable
5825    // parameter keeps the denied production live in the same parse, so
5826    // one fixture proves both directions: 3 pre-fix, 2 after, 1 if the
5827    // allowlist/denylist is aimed at `ConditionalExpression`.
5828    //
5829    // The 2 is the `?` token plus the ternary's condition slot, which
5830    // `csharp_walk_for_conditions` counts as a Fitzpatrick unary
5831    // condition. That is why this is not written as an
5832    // `assert_deepest_conditions_match_cyclomatic` parity pin the way
5833    // the Java `<` / `>` sibling is: C# cyclomatic scores this method 1
5834    // decision, and the divergence is the pre-existing unary-condition
5835    // rule, not the `?` gate.
5836    #[cfg(feature = "csharp")]
5837    #[test]
5838    fn csharp_ternary_still_counts_alongside_nullable_types() {
5839        check_metrics::<CsharpParser>(
5840            "class A {
5841                int M(int? n, bool c) {
5842                    return c ? 1 : 2;
5843                }
5844            }",
5845            "foo.cs",
5846            |metric| {
5847                assert_eq!(metric.abc.conditions_sum(), 2);
5848            },
5849        );
5850    }
5851
5852    #[cfg(feature = "csharp")]
5853    #[test]
5854    fn csharp_aliased_invocation_expression_branches() {
5855        // Regression for issue #94 (lesson #2): the C# grammar emits three
5856        // aliased `kind_id`s for `invocation_expression`. Code that matches
5857        // only the unsuffixed `Csharp::InvocationExpression` undercounts ABC
5858        // branches whenever the AST emits an aliased variant. The three
5859        // method calls live in `M`, so the per-method maximum (visible at
5860        // the unit-space aggregate as `branches_max`) must be 3.
5861        check_metrics::<CsharpParser>(
5862            "class A {
5863                void M() {
5864                    System.Console.WriteLine(1);
5865                    System.Console.WriteLine(2);
5866                    System.Console.WriteLine(3);
5867                }
5868            }",
5869            "foo.cs",
5870            |metric| {
5871                assert_eq!(metric.abc.branches_max(), 3);
5872                assert_eq!(metric.abc.conditions_max(), 0);
5873            },
5874        );
5875    }
5876
5877    #[cfg(feature = "php")]
5878    #[test]
5879    fn php_zero_abc() {
5880        check_metrics::<PhpParser>("<?php\n", "foo.php", |metric| {
5881            assert_eq!(metric.abc.assignments_sum(), 0);
5882            assert_eq!(metric.abc.branches_sum(), 0);
5883            assert_eq!(metric.abc.conditions_sum(), 0);
5884            insta::assert_json_snapshot!(metric.abc);
5885        });
5886    }
5887
5888    #[cfg(feature = "php")]
5889    #[test]
5890    fn php_simple_assignment() {
5891        check_metrics::<PhpParser>(
5892            "<?php
5893function f(): void {
5894    $a = 1;
5895    $b = 2;
5896}",
5897            "foo.php",
5898            |metric| insta::assert_json_snapshot!(metric.abc),
5899        );
5900    }
5901
5902    #[cfg(feature = "php")]
5903    #[test]
5904    fn php_augmented_assignment() {
5905        check_metrics::<PhpParser>(
5906            "<?php
5907function f(int $x): int {
5908    $a = 0;
5909    $a += $x;
5910    $a -= 1;
5911    $a *= 2;
5912    return $a;
5913}",
5914            "foo.php",
5915            |metric| insta::assert_json_snapshot!(metric.abc),
5916        );
5917    }
5918
5919    #[cfg(feature = "php")]
5920    #[test]
5921    fn php_const_excluded() {
5922        // Constant declarations and enum cases are NOT counted as
5923        // assignments — they declare immutable values.
5924        check_metrics::<PhpParser>(
5925            "<?php
5926class A {
5927    const PI = 3.14;
5928    const E = 2.71;
5929}
5930enum Color {
5931    case Red;
5932    case Green;
5933}",
5934            "foo.php",
5935            |metric| insta::assert_json_snapshot!(metric.abc),
5936        );
5937    }
5938
5939    #[cfg(feature = "php")]
5940    #[test]
5941    fn php_function_call() {
5942        check_metrics::<PhpParser>(
5943            "<?php
5944function f(): void {
5945    foo();
5946    bar(1, 2);
5947}",
5948            "foo.php",
5949            |metric| insta::assert_json_snapshot!(metric.abc),
5950        );
5951    }
5952
5953    #[cfg(feature = "php")]
5954    #[test]
5955    fn php_method_call() {
5956        check_metrics::<PhpParser>(
5957            "<?php
5958function f($obj): void {
5959    $obj->m1();
5960    $obj->m2(1);
5961}",
5962            "foo.php",
5963            |metric| insta::assert_json_snapshot!(metric.abc),
5964        );
5965    }
5966
5967    #[cfg(feature = "php")]
5968    #[test]
5969    fn php_static_call() {
5970        check_metrics::<PhpParser>(
5971            "<?php
5972function f(): void {
5973    Foo::bar();
5974    Foo::baz(1);
5975}",
5976            "foo.php",
5977            |metric| insta::assert_json_snapshot!(metric.abc),
5978        );
5979    }
5980
5981    #[cfg(feature = "php")]
5982    #[test]
5983    fn php_nullsafe_call() {
5984        check_metrics::<PhpParser>(
5985            "<?php
5986function f($obj): void {
5987    $obj?->m1();
5988    $obj?->m2(1);
5989}",
5990            "foo.php",
5991            |metric| insta::assert_json_snapshot!(metric.abc),
5992        );
5993    }
5994
5995    #[cfg(feature = "php")]
5996    #[test]
5997    fn php_object_creation() {
5998        check_metrics::<PhpParser>(
5999            "<?php
6000function f(): void {
6001    new Foo();
6002    new Bar(1);
6003}",
6004            "foo.php",
6005            |metric| insta::assert_json_snapshot!(metric.abc),
6006        );
6007    }
6008
6009    #[cfg(feature = "php")]
6010    #[test]
6011    fn php_comparison_eq() {
6012        check_metrics::<PhpParser>(
6013            "<?php
6014function f(int $a, int $b): bool {
6015    return $a == $b || $a != $b;
6016}",
6017            "foo.php",
6018            |metric| insta::assert_json_snapshot!(metric.abc),
6019        );
6020    }
6021
6022    #[cfg(feature = "php")]
6023    #[test]
6024    fn php_comparison_strict() {
6025        check_metrics::<PhpParser>(
6026            "<?php
6027function f(int $a, int $b): bool {
6028    return $a === $b || $a !== $b;
6029}",
6030            "foo.php",
6031            |metric| insta::assert_json_snapshot!(metric.abc),
6032        );
6033    }
6034
6035    #[cfg(feature = "php")]
6036    #[test]
6037    fn php_spaceship() {
6038        check_metrics::<PhpParser>(
6039            "<?php
6040function f(int $a, int $b): int {
6041    return $a <=> $b;
6042}",
6043            "foo.php",
6044            |metric| insta::assert_json_snapshot!(metric.abc),
6045        );
6046    }
6047
6048    #[cfg(feature = "php")]
6049    #[test]
6050    fn php_instanceof() {
6051        check_metrics::<PhpParser>(
6052            "<?php
6053function f($x): bool {
6054    return $x instanceof Foo;
6055}",
6056            "foo.php",
6057            |metric| insta::assert_json_snapshot!(metric.abc),
6058        );
6059    }
6060
6061    #[cfg(feature = "php")]
6062    #[test]
6063    fn php_complex_function() {
6064        // One snippet exercising A, B, C buckets together.
6065        check_metrics::<PhpParser>(
6066            "<?php
6067function f(int $a, int $b): int {
6068    $sum = $a + $b;
6069    $prod = $a * $b;
6070    if ($sum > 0 && $prod === 0) {
6071        return foo($sum);
6072    }
6073    return bar()->double();
6074}",
6075            "foo.php",
6076            |metric| insta::assert_json_snapshot!(metric.abc),
6077        );
6078    }
6079
6080    #[cfg(feature = "php")]
6081    #[test]
6082    fn php_if_boolean_literal_condition() {
6083        check_metrics::<PhpParser>(
6084            "<?php\n\
6085             function f() {\n\
6086             \x20   if (true) {}                 // +1c\n\
6087             \x20   if (!false) {}               // +1c\n\
6088             \x20   while (true) {}              // +1c\n\
6089             \x20   do {} while (false);         // +1c\n\
6090             }\n",
6091            "foo.php",
6092            |metric| {
6093                assert_eq!(metric.abc.conditions_sum(), 4);
6094                insta::assert_json_snapshot!(metric.abc);
6095            },
6096        );
6097    }
6098
6099    #[cfg(feature = "php")]
6100    #[test]
6101    fn php_methods_arguments_with_conditions() {
6102        check_metrics::<PhpParser>(
6103            "<?php\n\
6104             function f($a, $b) {\n\
6105             \x20   m($a, $b);                   // +1b\n\
6106             \x20   m(!$a, !$b);                 // +1b +2c\n\
6107             }\n",
6108            "foo.php",
6109            |metric| {
6110                assert_eq!(metric.abc.branches_sum(), 2);
6111                assert_eq!(metric.abc.conditions_sum(), 2);
6112                insta::assert_json_snapshot!(metric.abc);
6113            },
6114        );
6115    }
6116
6117    #[cfg(feature = "php")]
6118    #[test]
6119    fn php_return_with_conditions() {
6120        check_metrics::<PhpParser>(
6121            "<?php\n\
6122             function m1($z) { return !($z >= 0); }\n\
6123             function m2($x) { return (((!$x))); }\n\
6124             function m3($x, $y) { return $x && $y; }\n",
6125            "foo.php",
6126            |metric| {
6127                // m1: `>=` (1). m2: walker unwraps to $x (1).
6128                // m3: `&&` walker counts both (2). Sum: 4.
6129                assert_eq!(metric.abc.conditions_sum(), 4);
6130                insta::assert_json_snapshot!(metric.abc);
6131            },
6132        );
6133    }
6134
6135    #[cfg(feature = "php")]
6136    #[test]
6137    fn php_name2_hidden_rule_drift_marker() {
6138        // Drift marker (findings.md round-2 #3): `Php::Name2` maps
6139        // to the hidden grammar rule `_name`. At the pinned
6140        // tree-sitter-php version it is never emitted as a concrete
6141        // node — the visible `Name` (= 1) carries every name.
6142        // The former `php_bool_terminal_kinds!()` listed `Name2`
6143        // defensively (lesson 34); if a future grammar bump promotes `_name`
6144        // to a visible rule, this assertion fails loudly.
6145        let src = "<?php\nfunction f($x) { if ($x) { foo($x); } }\n";
6146        let parser = PhpParser::new(
6147            src.as_bytes().to_vec(),
6148            &std::path::PathBuf::from("foo.php"),
6149            None,
6150        );
6151        assert!(!ast_has_kind_id(&parser, Php::Name2 as u16));
6152    }
6153
6154    #[cfg(feature = "php")]
6155    #[test]
6156    fn php_scoped_property_access_condition_counts() {
6157        // Regression for findings.md round-2 #1 (PHP):
6158        // `if (Config::$enabled) {}` parses with
6159        // `scoped_property_access_expression` as the condition
6160        // node (kind_id 333 at the pinned grammar version — the
6161        // `*2` alias). Pre-fix, neither `ScopedPropertyAccessExpression`
6162        // nor its alias was in the former `php_bool_terminal_kinds!()`.
6163        // The walker reached the access node, found it non-terminal,
6164        // and broke. Mirrors C#'s `MemberAccessExpression` rule
6165        // (lesson 19, #372).
6166        check_metrics::<PhpParser>(
6167            "<?php\n\
6168             class Config { public static $enabled = true; }\n\
6169             function f() { if (Config::$enabled) { } }\n",
6170            "foo.php",
6171            |metric| {
6172                assert_eq!(metric.abc.conditions_sum(), 1);
6173                insta::assert_json_snapshot!(metric.abc);
6174            },
6175        );
6176    }
6177
6178    #[cfg(feature = "php")]
6179    #[test]
6180    fn php_named_argument_unary_conditional_counts() {
6181        // Regression for the code-review finding: PHP 8 named-argument
6182        // syntax `m(name: !$a)` parses as `argument(name, ':',
6183        // unary_op_expression)`. Pre-fix, the count walker took
6184        // `argument.child(0)` (the name) and missed the value at the
6185        // last child. Now it picks the last named child as the value.
6186        check_metrics::<PhpParser>(
6187            "<?php\nfunction f($a) { m(name: !$a); }\n",
6188            "foo.php",
6189            |metric| {
6190                // 1 call (branch) + 1 unary-conditional named argument.
6191                assert_eq!(metric.abc.branches_sum(), 1);
6192                assert_eq!(metric.abc.conditions_sum(), 1);
6193                insta::assert_json_snapshot!(metric.abc);
6194            },
6195        );
6196    }
6197
6198    #[cfg(feature = "php")]
6199    #[test]
6200    fn php_low_precedence_keyword_logical_ops_trigger_walker() {
6201        // Regression: pre-fix, `$a or $b` reported 0 conditions
6202        // because the dispatcher only handled `AMPAMP|PIPEPIPE`,
6203        // skipping the PHP-specific `and` / `or` keyword forms even
6204        // though they parse under the same `binary_expression` shape.
6205        // (`xor` is no chain: it is a value since #1536.)
6206        check_metrics::<PhpParser>(
6207            "<?php\n\
6208             function f($a, $b) {\n\
6209             \x20   return $a or $b;\n\
6210             }\n",
6211            "foo.php",
6212            |metric| {
6213                assert_eq!(metric.abc.conditions_sum(), 2);
6214                insta::assert_json_snapshot!(metric.abc);
6215            },
6216        );
6217    }
6218
6219    #[cfg(feature = "php")]
6220    #[test]
6221    fn php_if_multiple_conditions() {
6222        check_metrics::<PhpParser>(
6223            "<?php\n\
6224             function f($a, $b, $c, $d) {\n\
6225             \x20   if ($a || $b || $c || $d) {}     // +4c\n\
6226             \x20   if ($a && $b && $c) {}           // +3c\n\
6227             \x20   if (!$a && !$b) {}               // +2c\n\
6228             }\n",
6229            "foo.php",
6230            |metric| {
6231                assert_eq!(metric.abc.conditions_sum(), 9);
6232                insta::assert_json_snapshot!(metric.abc);
6233            },
6234        );
6235    }
6236
6237    #[cfg(feature = "php")]
6238    #[test]
6239    fn php_while_and_do_while_conditions() {
6240        check_metrics::<PhpParser>(
6241            "<?php\n\
6242             function f($a, $b) {\n\
6243             \x20   while ($a || $b) {}              // +2c\n\
6244             \x20   do {} while ($a && !$b);         // +2c\n\
6245             }\n",
6246            "foo.php",
6247            |metric| {
6248                assert_eq!(metric.abc.conditions_sum(), 4);
6249                insta::assert_json_snapshot!(metric.abc);
6250            },
6251        );
6252    }
6253
6254    #[cfg(feature = "php")]
6255    #[test]
6256    fn php_short_circuit_with_boolean_literal_operand() {
6257        check_metrics::<PhpParser>(
6258            "<?php\nfunction f($a) { return $a && true; }\n",
6259            "foo.php",
6260            |metric| {
6261                assert_eq!(metric.abc.conditions_sum(), 2);
6262                insta::assert_json_snapshot!(metric.abc);
6263            },
6264        );
6265    }
6266
6267    // Issue #1102, PHP half. See
6268    // `cpp_ternary_operand_slots_count_as_unary_conditions` for the
6269    // rule. PHP's ABC dispatcher has no `?`-token arm — the grammar
6270    // does emit the token, but the `conditional_expression` node is
6271    // what carries the tally's +1 — so the arm keeps that increment and
6272    // adds the operand slots.
6273    #[cfg(feature = "php")]
6274    #[test]
6275    fn php_ternary_operand_slots_count_as_unary_conditions() {
6276        // ternary (1) + condition `$a` (1) + `!$b` (1) + `!$c` (1) = 4.
6277        check_metrics::<PhpParser>(
6278            "<?php\nfunction f() { $x = $a ? !$b : !$c; }\n",
6279            "foo.php",
6280            |metric| assert_eq!(metric.abc.conditions_sum(), 4),
6281        );
6282        // No-double-count pin: ternary (1) + `>` (1) = 2, unchanged by
6283        // the fix.
6284        check_metrics::<PhpParser>(
6285            "<?php\nfunction f() { $x = ($a > 0) ? $b : -$b; }\n",
6286            "foo.php",
6287            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
6288        );
6289        // Nested (PHP 8 requires the inner ternary parenthesised): two
6290        // ternary nodes plus the two bare-variable conditions = 4.
6291        check_metrics::<PhpParser>(
6292            "<?php\nfunction f() { $x = $a ? ($b ? $c : $d) : $e; }\n",
6293            "foo.php",
6294            |metric| assert_eq!(metric.abc.conditions_sum(), 4),
6295        );
6296        // A negated condition is the only input reaching the walker's
6297        // `else` fallback — see the C++ sibling for why. ternary (1) +
6298        // `!$a` (1) = 2.
6299        check_metrics::<PhpParser>(
6300            "<?php\nfunction f() { $x = !$a ? $b : $c; }\n",
6301            "foo.php",
6302            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
6303        );
6304    }
6305
6306    // PHP's short ternary `$a ?: $b` elides the consequence, which the
6307    // grammar names `body` (not `consequence`) and marks optional. The
6308    // alternative lands at child(3), so addressing the slot by field
6309    // name rather than a fixed child(4) is what keeps `!$b` counted.
6310    #[cfg(feature = "php")]
6311    #[test]
6312    fn php_elided_ternary_body_still_walks_the_alternative() {
6313        // ternary (1) + condition `$a` (1) + `!$b` (1) = 3.
6314        check_metrics::<PhpParser>(
6315            "<?php\nfunction f() { $x = $a ?: !$b; }\n",
6316            "foo.php",
6317            |metric| assert_eq!(metric.abc.conditions_sum(), 3),
6318        );
6319    }
6320
6321    // Issue #1276, PHP half. The `for` header's condition slot was the
6322    // one condition slot `PhpCode::compute` never dispatched, so a
6323    // bare / negated / parenthesised loop condition scored zero while
6324    // the identical predicate in an `if` header scored one. Each
6325    // fixture below is a shape only the new arm can classify — a
6326    // comparison-shaped condition proves nothing here, because the `<`
6327    // token arm counts it either way (grammar-dispatch §11).
6328    #[cfg(feature = "php")]
6329    #[test]
6330    fn php_for_condition_slot_counts_unary_conditions() {
6331        // Bare variable: the whole condition, no operator token.
6332        check_metrics::<PhpParser>(
6333            "<?php\nfunction f($a) { for (; $a; ) {} }\n",
6334            "foo.php",
6335            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
6336        );
6337        // Negation: the slot peels the `!` (`php_wrapper_operand`) and
6338        // pays for the operand beneath it.
6339        check_metrics::<PhpParser>(
6340            "<?php\nfunction f($a) { for (; !$a; ) {} }\n",
6341            "foo.php",
6342            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
6343        );
6344        // Parentheses: counted because the slot peels them
6345        // (`php_wrapper_operand`), the path #1276 found dead.
6346        check_metrics::<PhpParser>(
6347            "<?php\nfunction f($a) { for (; ($a); ) {} }\n",
6348            "foo.php",
6349            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
6350        );
6351        // No-double-count pin: the `<` token arm already counted this
6352        // shape before the fix, and the walker must not add a second.
6353        // Two assignments (`$i = 0`, `$i++`) confirm the header parsed
6354        // as the three-clause form rather than degenerating.
6355        check_metrics::<PhpParser>(
6356            "<?php\nfunction f($n) { for ($i = 0; $i < $n; $i++) {} }\n",
6357            "foo.php",
6358            |metric| {
6359                assert_eq!(metric.abc.conditions_sum(), 1);
6360                assert_eq!(metric.abc.assignments_sum(), 2);
6361            },
6362        );
6363        // Empty condition: no `condition` field, no decision, zero.
6364        check_metrics::<PhpParser>(
6365            "<?php\nfunction f() { for (;;) { break; } }\n",
6366            "foo.php",
6367            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
6368        );
6369    }
6370
6371    // --- Kotlin ABC tests -------------------------------------------------
6372
6373    #[cfg(feature = "kotlin")]
6374    #[test]
6375    fn kotlin_empty_class() {
6376        check_metrics::<KotlinParser>("class C {}", "foo.kt", |metric| {
6377            assert_eq!(metric.abc.assignments_sum(), 0);
6378            assert_eq!(metric.abc.branches_sum(), 0);
6379            assert_eq!(metric.abc.conditions_sum(), 0);
6380            insta::assert_json_snapshot!(metric.abc);
6381        });
6382    }
6383
6384    #[cfg(feature = "kotlin")]
6385    #[test]
6386    fn kotlin_val_declarations_are_not_assignments() {
6387        // `val` introduces an immutable binding — the `=` initialising it
6388        // is not an assignment in the ABC sense.
6389        check_metrics::<KotlinParser>(
6390            "class C {
6391                val a: Int = 1
6392                val b: Int = 2
6393                val c: Int = 3
6394            }",
6395            "foo.kt",
6396            |metric| {
6397                assert_eq!(metric.abc.assignments_sum(), 0);
6398                assert_eq!(metric.abc.branches_sum(), 0);
6399                insta::assert_json_snapshot!(metric.abc);
6400            },
6401        );
6402    }
6403
6404    #[cfg(feature = "kotlin")]
6405    #[test]
6406    fn kotlin_var_declarations_count_assignment() {
6407        // `var` initialisers count as assignments (mutable binding).
6408        check_metrics::<KotlinParser>(
6409            "class C {
6410                var a: Int = 1
6411                var b: Int = 2
6412            }",
6413            "foo.kt",
6414            |metric| {
6415                assert_eq!(metric.abc.assignments_sum(), 2);
6416                insta::assert_json_snapshot!(metric.abc);
6417            },
6418        );
6419    }
6420
6421    #[cfg(feature = "kotlin")]
6422    #[test]
6423    fn kotlin_val_then_assignments_count() {
6424        // Regression for #455: a `val` initialiser must not suppress the
6425        // standalone `=` assignments that follow it. tree-sitter-kotlin
6426        // emits no `SEMI` token (even for explicit semicolons), so the
6427        // pre-#455 `SEMI`-cleared declaration stack never cleared and the
6428        // immutable-`val` sentinel leaked, reporting A=0 here.
6429        check_metrics::<KotlinParser>(
6430            "fun f() {
6431                val cfg = 0
6432                a = 1
6433                b = 2
6434            }",
6435            "foo.kt",
6436            |metric| {
6437                // val initialiser suppressed; `a = 1` and `b = 2` count.
6438                assert_eq!(metric.abc.assignments_sum(), 2);
6439                insta::assert_json_snapshot!(metric.abc);
6440            },
6441        );
6442    }
6443
6444    #[cfg(feature = "kotlin")]
6445    #[test]
6446    fn kotlin_var_then_assignments_count() {
6447        // Companion to the #455 regression: a `var` declaration leaves a
6448        // mutable-binding sentinel that *permits* the `=` — this path
6449        // accidentally masked the leak (its `Var` sentinel never suppressed
6450        // anything), so it must keep counting both the initialiser and the
6451        // following standalone assignments.
6452        check_metrics::<KotlinParser>(
6453            "fun f() {
6454                var cfg = 0
6455                a = 1
6456                b = 2
6457            }",
6458            "foo.kt",
6459            |metric| {
6460                // var initialiser (+1) plus `a = 1` and `b = 2` (+2).
6461                assert_eq!(metric.abc.assignments_sum(), 3);
6462                insta::assert_json_snapshot!(metric.abc);
6463            },
6464        );
6465    }
6466
6467    #[cfg(feature = "kotlin")]
6468    #[test]
6469    fn kotlin_augmented_assignments_count() {
6470        // Augmented operators (+=, -=, etc.) and ++/-- always count.
6471        check_metrics::<KotlinParser>(
6472            "fun m() {
6473                var x = 0
6474                x += 1
6475                x -= 2
6476                x *= 3
6477                x++
6478                --x
6479            }",
6480            "foo.kt",
6481            |metric| {
6482                // var declaration (var x = 0): +1
6483                // x += 1, x -= 2, x *= 3, x++, --x: +5
6484                assert_eq!(metric.abc.assignments_sum(), 6);
6485                insta::assert_json_snapshot!(metric.abc);
6486            },
6487        );
6488    }
6489
6490    #[cfg(feature = "kotlin")]
6491    #[test]
6492    fn kotlin_branches_call_expression() {
6493        check_metrics::<KotlinParser>(
6494            "fun m() {
6495                println(\"a\")
6496                println(\"b\")
6497                println(\"c\")
6498            }",
6499            "foo.kt",
6500            |metric| {
6501                assert_eq!(metric.abc.branches_sum(), 3);
6502                insta::assert_json_snapshot!(metric.abc);
6503            },
6504        );
6505    }
6506
6507    #[cfg(feature = "kotlin")]
6508    #[test]
6509    fn kotlin_object_construction_branch() {
6510        // Kotlin's object construction is just `Foo()` — a `CallExpression`.
6511        check_metrics::<KotlinParser>(
6512            "class P(val x: Int)
6513            fun m(): P = P(1)",
6514            "foo.kt",
6515            |metric| {
6516                assert_eq!(metric.abc.branches_sum(), 1);
6517                insta::assert_json_snapshot!(metric.abc);
6518            },
6519        );
6520    }
6521
6522    #[cfg(feature = "kotlin")]
6523    #[test]
6524    fn kotlin_comparisons_count_conditions() {
6525        check_metrics::<KotlinParser>(
6526            "fun m(a: Int, b: Int): Boolean {
6527                val r1 = a < b
6528                val r2 = a > b
6529                val r3 = a <= b
6530                val r4 = a >= b
6531                val r5 = a == b
6532                val r6 = a != b
6533                return r1 || r2 || r3 || r4 || r5 || r6
6534            }",
6535            "foo.kt",
6536            |metric| {
6537                // Six comparison operators in the `val` initialisers
6538                // (<, >, <=, >=, ==, !=) → 6, plus the six bare-identifier
6539                // operands of the `r1 || … || r6` return chain, each a
6540                // Fitzpatrick Rule 9 unary condition (issue #557) → 6.
6541                // Total 12. Before the Kotlin walker was wired the chain
6542                // operands were silently dropped and this read 6.
6543                assert_eq!(metric.abc.conditions_sum(), 12);
6544                insta::assert_json_snapshot!(metric.abc);
6545            },
6546        );
6547    }
6548
6549    #[cfg(feature = "kotlin")]
6550    #[test]
6551    fn kotlin_identity_equality_conditions() {
6552        // `===` / `!==` are referential equality in Kotlin; they count too.
6553        check_metrics::<KotlinParser>(
6554            "fun m(a: Any, b: Any): Boolean {
6555                return a === b || a !== b
6556            }",
6557            "foo.kt",
6558            |metric| {
6559                assert_eq!(metric.abc.conditions_sum(), 2);
6560                insta::assert_json_snapshot!(metric.abc);
6561            },
6562        );
6563    }
6564
6565    #[cfg(feature = "kotlin")]
6566    #[test]
6567    fn kotlin_else_branch_counts() {
6568        check_metrics::<KotlinParser>(
6569            "fun m(x: Int): Int {
6570                return if (x > 0) 1 else -1
6571            }",
6572            "foo.kt",
6573            |metric| {
6574                // condition: > (1) + else (1) = 2
6575                assert_eq!(metric.abc.conditions_sum(), 2);
6576                insta::assert_json_snapshot!(metric.abc);
6577            },
6578        );
6579    }
6580
6581    #[cfg(feature = "kotlin")]
6582    #[test]
6583    fn kotlin_when_entries_count() {
6584        check_metrics::<KotlinParser>(
6585            "fun m(x: Int): Int {
6586                return when (x) {
6587                    1 -> 10
6588                    2 -> 20
6589                    else -> 0
6590                }
6591            }",
6592            "foo.kt",
6593            |metric| {
6594                // Non-`else` WhenEntry arms count; the `else ->` fallback
6595                // arm does not (issue #456). Two case arms + zero for the
6596                // `else` arm = 2.
6597                assert_eq!(metric.abc.conditions_sum(), 2);
6598                insta::assert_json_snapshot!(metric.abc);
6599            },
6600        );
6601    }
6602
6603    // Pins the `else ->` exclusion directly: a `when` whose only fallback
6604    // is `else ->` must not count that arm. Revert-verified — gating the
6605    // `WhenEntry` arm on `!kotlin_when_entry_is_else` is what drops this
6606    // from 3 to 2 (issue #456, lesson 11). Mirrors the cyclomatic gate.
6607    #[cfg(feature = "kotlin")]
6608    #[test]
6609    fn kotlin_when_else_not_a_condition() {
6610        check_metrics::<KotlinParser>(
6611            "fun m(x: Int): Int {
6612                return when (x) { 1 -> 10; 2 -> 20; else -> 0 }
6613            }",
6614            "foo.kt",
6615            |metric| {
6616                // case `1 ->` (+1) + case `2 ->` (+1) + `else ->` (+0) = 2.
6617                assert_eq!(metric.abc.conditions_sum(), 2);
6618            },
6619        );
6620    }
6621
6622    // The Kotlin binding of the two assertions every #1421 fixture needs,
6623    // coupled on purpose. A per-member score table is worth only as much
6624    // as the anchor proving the fixture still spells what it claims to
6625    // (`.claude/rules/testing.md`, "Perturb the fixture as well as the
6626    // production line"), and the `when_subject` count is the anchor no
6627    // score can stand in for: a `when` that grew a subject scores its
6628    // entries the pre-#1421 way with every other row still satisfied.
6629    // Each fixture is one class, so the members are `spaces[0]`'s.
6630    #[cfg(feature = "kotlin")]
6631    #[track_caller]
6632    fn assert_kotlin_class_members(
6633        src: &str,
6634        kinds: &[(u16, usize, &str)],
6635        expected: &[(&str, u64, u64)],
6636    ) {
6637        assert_fixture_spells::<KotlinParser>(src, "foo.kt", kinds);
6638        check_func_space::<KotlinParser, _>(src, "foo.kt", |space| {
6639            assert_members_score(&space.spaces[0], expected);
6640        });
6641    }
6642
6643    // #1421's headline. A subject-less `when` arm's condition is an
6644    // ordinary boolean expression, so its comparison operator is already
6645    // an ABC condition through the token arms; the `WhenEntry` arm added
6646    // a second one on top, and `two` reported 4 against a decision count
6647    // of 2.
6648    //
6649    // `bare` is the control that isolates the defect rather than merely
6650    // observing it: same arm count, same entry shape, no comparison
6651    // operator anywhere — so it scored the right 2 before the fix and
6652    // after it. A fix that changed both members alike would be attacking
6653    // the entry count, which was never wrong.
6654    //
6655    // `ge` and `eq` exist because the issue named `<` and `>` only. All
6656    // six comparison spellings share the defect: `>=` and `==` reach
6657    // `stats.conditions` through the `LTEQ | GTEQ | EQEQ | …` arm rather
6658    // than the parent-gated `LT | GT` arm, and both scored 2 against a
6659    // decision count of 1.
6660    //
6661    // `iff` is the member that legitimately sits *above* its decision
6662    // count, and it is here so the table cannot be read as "§8 parity
6663    // everywhere". ABC counts the `else` (Fitzpatrick Rule 5) and
6664    // cyclomatic does not, so 2 conditions against 1 decision is correct
6665    // and unchanged by this fix.
6666    //
6667    // The `when_subject` anchor is the load-bearing one: the whole test
6668    // turns on these `when`s having no subject, and nothing in the
6669    // measured numbers would notice a fixture that grew one — a
6670    // subject-ful `when (x) { … }` scores its entries the old way and
6671    // `two` would read 4 again with every other row still satisfied.
6672    #[cfg(feature = "kotlin")]
6673    #[test]
6674    fn kotlin_subjectless_when_arm_counts_its_condition_once() {
6675        let src = "class K {
6676                fun f(): Boolean = true
6677                fun two(x: Int): Int = when { x > 5 -> 1; x < 0 -> 2; else -> 0 }
6678                fun bare(x: Boolean, y: Boolean): Int = when { x -> 1; y -> 2; else -> 0 }
6679                fun ge(x: Int): Int = when { x >= 5 -> 1; else -> 0 }
6680                fun eq(x: Int): Int = when { x == 5 -> 1; else -> 0 }
6681                fun andd(a: Int, b: Int): Int = when { a > 1 && b < 2 -> 1; else -> 0 }
6682                fun call(): Int = when { f() -> 1; else -> 0 }
6683                fun iff(x: Int): Int = if (x > 5) 1 else 0
6684            }";
6685        assert_kotlin_class_members(
6686            src,
6687            &[
6688                (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
6689                (Kotlin::WhenEntry as u16, 14, "`when` entries"),
6690                (
6691                    Kotlin::BinaryExpression as u16,
6692                    8,
6693                    "comparison / chain expressions",
6694                ),
6695                (Kotlin::AMPAMP as u16, 1, "`andd`'s `&&`"),
6696            ],
6697            &[
6698                // (member, abc.conditions, cyclomatic)
6699                ("f", 0, 1),
6700                // Was 4: two entries, two comparisons, counted twice.
6701                ("two", 2, 3),
6702                // The no-comparison control — 2 before and after.
6703                ("bare", 2, 3),
6704                // Was 2 each: the `GTEQ` / `EQEQ` spellings.
6705                ("ge", 1, 2),
6706                ("eq", 1, 2),
6707                // Was 3. A compound condition keeps both of its
6708                // comparisons — suppressing the operators instead
6709                // would have collapsed this one to 1.
6710                ("andd", 2, 3),
6711                // The condition slot scores a call directly, so a
6712                // spelling with no operator in it still reads 1.
6713                ("call", 1, 2),
6714                // Above its decision count on purpose: ABC counts
6715                // the `else`, cyclomatic does not.
6716                ("iff", 2, 2),
6717            ],
6718        );
6719    }
6720
6721    // #1459, the regression #1421 left behind. Turning the `when`
6722    // entry's blanket count into a condition slot moved the payment from
6723    // the entry to the condition — so every shape the slot cannot
6724    // classify went from 1 to 0, silently. Two shapes did:
6725    //
6726    // - `a!!`. `unary_expression` is one kind for *both* Kotlin unary
6727    //   spellings, and the slot routed it to the peel, so this reads as
6728    //   handled at every call site. The peel then looked for the operand
6729    //   at child(1) — right for the prefix `!x`, wrong for a postfix
6730    //   `x!!`, whose child(1) is the `!!` token — and stopped.
6731    // - `a as Boolean`. `as_expression` was not a wrapper the peel knew
6732    //   at all, so the slot fell off the end of its `else if`.
6733    //
6734    // The controls are `bare` / `cmp` / `call`, one per surviving path
6735    // (terminal, comparison token, invocation): a fix that moved the
6736    // entry count back would take those to 2 and fail here, which is
6737    // what distinguishes repairing the peel from reverting #1421.
6738    //
6739    // `safe` is the §5 row and the one measurement decided rather than
6740    // taste. `as?` is already a condition *token*
6741    // (`src/metrics/abc/kotlin.rs`), so a peel that treated `as` and
6742    // `as?` alike would score it 2 — the double count that arm's own
6743    // fix was avoiding. `kotlin_wrapper_operand` excludes `as?` for
6744    // that reason, and this member is what fails if the exclusion goes:
6745    // `(a as? Boolean)!!` is valid Kotlin whose peel reaches the cast
6746    // through two other wrappers, so it needs no invalid fixture to
6747    // discriminate.
6748    //
6749    // `iff` carries the same `a!!` through the `if` slot. That half is
6750    // not #1421 fallout — `if (a!!)` has read 0 since the slot landed in
6751    // #773 — but it is the same peel and the same edit, and a fix
6752    // narrowed to `when` entries would leave it at 0.
6753    //
6754    // Every member is valid Kotlin: `a!!` on a `Boolean?`, `as` on an
6755    // `Any`, and `(a as? Boolean)!!` back to a `Boolean`. The kind
6756    // anchors are what stop a later edit from trimming a spelling out
6757    // and turning its member into a silent copy of `bare`.
6758    //
6759    // Since #1533 the slot pays for any operand it cannot see a
6760    // comparison or chain inside, so `bang`, `paren`, `cast` and `chain`
6761    // score 1 with or without the peel. `bangCmp` and `castCmp` are what
6762    // still need it: unpeeled, the wrapper pays as a value on top of the
6763    // `>` it hides, and they read 2.
6764    #[cfg(feature = "kotlin")]
6765    #[test]
6766    fn kotlin_condition_slot_peels_null_assertions_and_casts() {
6767        let src = "class K {
6768                fun g(x: Boolean): Boolean = true
6769                fun bare(a: Boolean): Int = when { a -> 1; else -> 0 }
6770                fun cmp(a: Int): Int = when { a > 5 -> 1; else -> 0 }
6771                fun call(a: Boolean): Int = when { g(a) -> 1; else -> 0 }
6772                fun bang(a: Boolean?): Int = when { a!! -> 1; else -> 0 }
6773                fun paren(a: Boolean?): Int = when { (a!!) -> 1; else -> 0 }
6774                fun cast(a: Any): Int = when { a as Boolean -> 1; else -> 0 }
6775                fun chain(a: Any?): Int = when { a!! as Boolean -> 1; else -> 0 }
6776                fun safe(a: Any): Int = when { (a as? Boolean)!! -> 1; else -> 0 }
6777                fun cmt(a: Boolean): Int = when { ! /*c*/ a -> 1; else -> 0 }
6778                fun iff(a: Boolean?): Int { if (a!!) { return 1 }; return 0 }
6779                fun bangCmp(a: Int): Int = when { (a > 5)!! -> 1; else -> 0 }
6780                fun castCmp(a: Int): Int = when { (a > 5) as Boolean -> 1; else -> 0 }
6781            }";
6782        assert_kotlin_class_members(
6783            src,
6784            &[
6785                (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
6786                (
6787                    Kotlin::BANGBANG as u16,
6788                    6,
6789                    "the `!!` null assertions of `bang` / `paren` / `chain` / `safe` / `iff` / \
6790                     `bangCmp`",
6791                ),
6792                (
6793                    Kotlin::AsExpression as u16,
6794                    4,
6795                    "the casts of `cast` / `chain` / `safe` / `castCmp`",
6796                ),
6797                (Kotlin::AsQMARK as u16, 1, "`safe`'s safe cast"),
6798                (
6799                    Kotlin::BlockComment as u16,
6800                    1,
6801                    "`cmt`'s interposed comment, spaces and all",
6802                ),
6803                (
6804                    Kotlin::ParenthesizedExpression as u16,
6805                    4,
6806                    "the parenthesised operands of `paren` / `safe` / `bangCmp` / `castCmp`",
6807                ),
6808            ],
6809            &[
6810                // (member, abc.conditions, cyclomatic)
6811                ("g", 0, 1),
6812                // The three controls, unchanged by this fix.
6813                ("bare", 1, 2),
6814                ("cmp", 1, 2),
6815                ("call", 1, 2),
6816                // Each was 0: the peel stopped on the wrapper.
6817                ("bang", 1, 2),
6818                ("paren", 1, 2),
6819                ("cast", 1, 2),
6820                ("chain", 1, 2),
6821                // 1, not 2 — the `as?` token owns this one, and the
6822                // peel declines to charge for it a second time.
6823                ("safe", 1, 2),
6824                // The peel reads its operand through the `operator` /
6825                // `argument` fields, so an `extra` between the two no
6826                // longer displaces it. The spacing is load-bearing and
6827                // must not be tidied away: `!/*c*/a` unspaced parses
6828                // with the comment somewhere a `child(1)` read still
6829                // skips, so only the spaced spelling can tell the field
6830                // read from the positional one. Measured both ways.
6831                ("cmt", 1, 2),
6832                // The `if` slot, which has read 0 since #773.
6833                ("iff", 1, 2),
6834                // The `>` pays; the peel keeps the wrapper from paying too.
6835                ("bangCmp", 1, 2),
6836                ("castCmp", 1, 2),
6837            ],
6838        );
6839    }
6840
6841    // The other half of #1421: a subject-ful entry must not move. Its
6842    // condition is a *pattern* matched against the subject, not an
6843    // independent boolean expression, so the implicit `subject ==
6844    // pattern` is the decision and the entry itself pays for it —
6845    // `range_test`, `type_test` and a bare constant carry no token the
6846    // comparison arms would count.
6847    //
6848    // `cmp` is the interesting row and the reason `nested` sits beside
6849    // it. `when (x) { y > 5 -> … }` is legal when `x` is `Boolean`, and
6850    // it reads 2 conditions against 1 decision — the entry's implicit
6851    // equality plus the `>` inside the operand. That is not the #1421
6852    // double-count reappearing: `nested` spells the same thing as an
6853    // `if` (`x == (y > 5)`) and ABC has always scored it 2 against the
6854    // same 1 decision. A comparison nested inside a comparison is two
6855    // comparisons; only one of them is a branch.
6856    #[cfg(feature = "kotlin")]
6857    #[test]
6858    fn kotlin_subjectful_when_arms_keep_the_per_entry_count() {
6859        let src = "class K {
6860                fun inn(a: Int): Int = when (a) { in 1..2 -> 1; else -> 0 }
6861                fun notin(a: Int): Int = when (a) { !in 1..2 -> 1; else -> 0 }
6862                fun iss(a: Any): Int = when (a) { is String -> 1; else -> 0 }
6863                fun konst(a: Int): Int = when (a) { 1 -> 10; 2 -> 20; else -> 0 }
6864                fun cmp(x: Boolean, y: Int): Int = when (x) { y > 5 -> 1; else -> 0 }
6865                fun nested(x: Boolean, y: Int): Int { if (x == (y > 5)) { return 1 }; return 0 }
6866            }";
6867        assert_kotlin_class_members(
6868            src,
6869            &[
6870                (Kotlin::WhenSubject as u16, 5, "`when` subjects"),
6871                (Kotlin::WhenEntry as u16, 11, "`when` entries"),
6872                (Kotlin::RangeTest as u16, 2, "`in` / `!in` patterns"),
6873                (Kotlin::TypeTest as u16, 1, "the `is` pattern"),
6874                (
6875                    Kotlin::BinaryExpression as u16,
6876                    3,
6877                    "`cmp`'s and `nested`'s comparisons",
6878                ),
6879            ],
6880            &[
6881                ("inn", 1, 2),
6882                ("notin", 1, 2),
6883                ("iss", 1, 2),
6884                ("konst", 2, 3),
6885                // Above its decision count, like `nested` below it
6886                // and for the same reason.
6887                ("cmp", 2, 2),
6888                ("nested", 2, 2),
6889            ],
6890        );
6891    }
6892
6893    // `kotlin_enclosing_when_has_subject` stops scanning at the `{`, so
6894    // that a subject-less `when` does not walk all N arms once per arm.
6895    // The bound has to clear the header first, and the header can carry
6896    // an `extra`: tree-sitter hangs `when /*c*/ (x)`'s comment off the
6897    // `when_expression` between the keyword and the subject, so a bound
6898    // one child tighter — `child(1)`, or a stop at the first *named*
6899    // child — reads `cmtSubject` as subject-less. It would then score
6900    // its `1` arm through the slot, where a `number_literal` is not a
6901    // Kotlin terminal, and report 0 conditions instead of 1. That is
6902    // what discriminates here; `cmtNone` is the other direction, where
6903    // the brace is all the bound has to stop on.
6904    //
6905    // The `block_comment` census is the fixture anchor: delete either
6906    // comment and the members still read 1, because a plain `when (x)`
6907    // and a plain `when {` both do. The count fails by name instead.
6908    #[cfg(feature = "kotlin")]
6909    #[test]
6910    fn kotlin_when_subject_scan_clears_a_comment_before_the_brace() {
6911        let src = "class K {
6912                fun cmtSubject(x: Int): Int = when /*c*/ (x) { 1 -> 1; else -> 0 }
6913                fun cmtNone(a: Boolean): Int = when /*c*/ { a -> 1; else -> 0 }
6914            }";
6915        assert_kotlin_class_members(
6916            src,
6917            &[
6918                (Kotlin::BlockComment as u16, 2, "the two header comments"),
6919                (Kotlin::WhenSubject as u16, 1, "`cmtSubject`'s subject"),
6920                (Kotlin::WhenEntry as u16, 4, "`when` entries"),
6921            ],
6922            &[("cmtSubject", 1, 2), ("cmtNone", 1, 2)],
6923        );
6924    }
6925
6926    // A subject-less condition reaches the slot through the same
6927    // `kotlin_wrapper_operand` peel an `if` predicate does, which
6928    // is what separates this design from suppressing the entry's
6929    // operator. `notted` and `paren` unwrap to a bare terminal and score
6930    // through the slot; `notCmp` and `parenCmp` unwrap to a
6931    // `binary_expression`, score nothing from the slot, and take their
6932    // one condition from the `>` the token arm already owned. Both pairs
6933    // land on 1, which no blanket `+1` can produce for the second pair —
6934    // they read 2 before the fix.
6935    //
6936    // `paren` once reached the slot only through a `WhenEntry` seed in the
6937    // retired walker's boolean-context set, and dropped to 0 without it.
6938    // On the shared slot (#1533) it pins that a `when` entry peels a
6939    // parenthesis as `if ((a))` does; `notted` cannot stand in for it,
6940    // since a `!` is a wrapper of its own.
6941    #[cfg(feature = "kotlin")]
6942    #[test]
6943    fn kotlin_subjectless_when_condition_wrappers_count_once() {
6944        let src = "class K {
6945                fun notted(a: Boolean): Int = when { !a -> 1; else -> 0 }
6946                fun paren(a: Boolean): Int = when { (a) -> 1; else -> 0 }
6947                fun notCmp(a: Int): Int = when { !(a > 1) -> 1; else -> 0 }
6948                fun parenCmp(a: Int): Int = when { (a > 1) -> 1; else -> 0 }
6949                fun nest(x: Int, y: Int): Int =
6950                    when { x > 1 -> when { y > 2 -> 1; else -> 2 }; else -> 0 }
6951                fun blockBody(x: Int): Int { when { x > 5 -> { return 1 } }; return 0 }
6952            }";
6953        assert_kotlin_class_members(
6954            src,
6955            &[
6956                // Paired, not bare: "no subjects" alone is satisfied by a
6957                // fixture that lost its `when`s altogether.
6958                (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
6959                (Kotlin::WhenEntry as u16, 13, "`when` entries"),
6960                (Kotlin::UnaryExpression as u16, 2, "the two `!` operands"),
6961                (
6962                    Kotlin::ParenthesizedExpression as u16,
6963                    3,
6964                    "the three parenthesised operands",
6965                ),
6966                (Kotlin::BinaryExpression as u16, 5, "comparisons"),
6967            ],
6968            &[
6969                ("notted", 1, 2),
6970                ("paren", 1, 2),
6971                // Both were 2: the entry's blanket count plus the `>`.
6972                ("notCmp", 1, 2),
6973                ("parenCmp", 1, 2),
6974                // Was 4 — one entry and one comparison per `when`,
6975                // each counted twice.
6976                ("nest", 2, 3),
6977                // The `when`-as-statement shape: a `block` body and no
6978                // `else` arm at all. Reading the condition from the
6979                // `condition` field rather than by position is what keeps
6980                // the block out of the slot; it was 2 before the fix.
6981                ("blockBody", 1, 2),
6982            ],
6983        );
6984    }
6985
6986    // The one place this design deliberately does not reach parity, put
6987    // under test so the choice cannot drift unremarked. A `when` entry
6988    // may list alternatives (`when { a, b -> … }`, an implicit `or`), and
6989    // the `condition` field is `multiple` — but cyclomatic scores the
6990    // whole entry as one decision, so `bareAlts` is at parity only
6991    // because the slot reads the *first* alternative and stops. Routing
6992    // every alternative through the slot would push it to 2.
6993    //
6994    // `alts` is the row that cannot be brought to parity from the ABC
6995    // side at all: the comparison-token arms count `>` and `<` wherever
6996    // they appear, so it reads 2 against 1 decision no matter what the
6997    // entry slot does. It was 3 before #1421 and the remaining gap is
6998    // cyclomatic's single count per multi-alternative entry, not a
6999    // double-count here.
7000    //
7001    // `subjAlts` is the subject-ful control, unchanged and at parity: its
7002    // alternatives are constants carrying no token to count.
7003    #[cfg(feature = "kotlin")]
7004    #[test]
7005    fn kotlin_subjectless_when_multi_alternative_entry() {
7006        let src = "class K {
7007                fun alts(x: Int, y: Int): Int = when { x > 5, y < 0 -> 1; else -> 0 }
7008                fun bareAlts(x: Boolean, y: Boolean): Int = when { x, y -> 1; else -> 0 }
7009                fun subjAlts(a: Int): Int = when (a) { 1, 2 -> 10; else -> 0 }
7010            }";
7011        assert_kotlin_class_members(
7012            src,
7013            &[
7014                (Kotlin::WhenSubject as u16, 1, "`subjAlts`'s subject"),
7015                (Kotlin::WhenEntry as u16, 6, "`when` entries"),
7016                (
7017                    Kotlin::BinaryExpression as u16,
7018                    2,
7019                    "`alts`'s two comparisons",
7020                ),
7021            ],
7022            &[
7023                // Above its decision count: cyclomatic scores the entry
7024                // once, the token arms score both comparisons. Was 3.
7025                ("alts", 2, 2),
7026                ("bareAlts", 1, 2),
7027                ("subjAlts", 1, 2),
7028            ],
7029        );
7030    }
7031
7032    // `is_expression` and `in_expression` are the two relational forms
7033    // tree-sitter-kotlin-ng spells as their own production rather than as
7034    // a `binary_expression`, so no comparison-token arm sees them and
7035    // nothing counted them: `if (a is String)` scored 0 against a
7036    // decision count of 1, and the same test in a subject-less `when`
7037    // scored 1 only because the entry's blanket `+1` happened to cover
7038    // it. Removing that blanket without counting these two somewhere
7039    // would have regressed `whenIs` and `whenIn` to 0, and `isAnd` —
7040    // where the `is` test is one operand of a `&&` chain and so reaches
7041    // the walker rather than the slot — from 2 to 1.
7042    //
7043    // The `if` members are here because the fix was never `when`-
7044    // specific: they are the call sites that prove it reaches the `if` /
7045    // `while` predicate slot too.
7046    //
7047    // #1421 counted them through the former
7048    // `kotlin_bool_terminal_kinds!()` and
7049    // #1461 moved them to an unconditional arm, which leaves every
7050    // number below unchanged — the slot no longer counts the test and
7051    // the arm does. `kotlin_is_and_in_score_outside_a_boolean_slot` is
7052    // the half this fixture cannot see, every member of it being inside
7053    // a slot.
7054    #[cfg(feature = "kotlin")]
7055    #[test]
7056    fn kotlin_is_and_in_expressions_are_unary_conditions() {
7057        let src = "class K {
7058                fun ifIs(a: Any): Int { if (a is String) { return 1 }; return 0 }
7059                fun ifIn(a: Int): Int { if (a in 1..2) { return 1 }; return 0 }
7060                fun whenIs(a: Any): Int = when { a is String -> 1; else -> 0 }
7061                fun whenIn(a: Int): Int = when { a in 1..2 -> 1; else -> 0 }
7062                fun isAnd(a: Any, b: Boolean): Int = when { a is String && b -> 1; else -> 0 }
7063            }";
7064        assert_kotlin_class_members(
7065            src,
7066            &[
7067                // Paired, not bare — see the wrappers test above.
7068                (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
7069                (Kotlin::WhenEntry as u16, 6, "`when` entries"),
7070                (Kotlin::IsExpression as u16, 3, "`is` tests"),
7071                (Kotlin::InExpression as u16, 2, "`in` tests"),
7072                (Kotlin::AMPAMP as u16, 1, "`isAnd`'s `&&`"),
7073            ],
7074            &[
7075                // Both were 0 — the predicate slot saw a kind it did
7076                // not classify and the token arms saw no operator.
7077                ("ifIs", 1, 2),
7078                ("ifIn", 1, 2),
7079                ("whenIs", 1, 2),
7080                ("whenIn", 1, 2),
7081                ("isAnd", 2, 3),
7082            ],
7083        );
7084    }
7085
7086    // Kotlin spells boolean `and` / `or` / `xor` as infix *functions*, so
7087    // `a and b` parses as `infix_expression` — not `binary_expression`,
7088    // and not any token arm. A slot pays for it for the same reason it
7089    // pays for a `call_expression`: `a and b` is `a.and(b)`.
7090    //
7091    // This was a regression of #1421, not a pre-existing gap, and it is
7092    // the third shape the blanket per-entry count turned out to be
7093    // propping up — after `is` / `in` and bare parens, which that fix
7094    // caught. Found by the whole-branch review. Measured on both sides:
7095    // `when { a and b -> … }` scored 1 before and 0 after, while the
7096    // `if (a and b)` it is supposed to agree with still scored 1.
7097    //
7098    // The `if` members are the anchor rather than a second `when`: the
7099    // whole premise of #1421 is that a subject-less arm scores like an
7100    // `if` predicate, so a fixture where the two disagree is the
7101    // statement of the bug. `andAmp` holds the pair level against the
7102    // `&&` spelling, which never regressed and would otherwise be the
7103    // only form under test.
7104    #[cfg(feature = "kotlin")]
7105    #[test]
7106    fn kotlin_infix_boolean_functions_are_unary_conditions() {
7107        let src = "class K {
7108                fun whenAnd(a: Boolean, b: Boolean): Int = when { a and b -> 1; else -> 0 }
7109                fun whenOr(a: Boolean, b: Boolean): Int = when { a or b -> 1; else -> 0 }
7110                fun whenXor(a: Boolean, b: Boolean): Int = when { a xor b -> 1; else -> 0 }
7111                fun whenAmp(a: Boolean, b: Boolean): Int = when { a && b -> 1; else -> 0 }
7112                fun ifAnd(a: Boolean, b: Boolean): Int { if (a and b) { return 1 }; return 0 }
7113            }";
7114        assert_kotlin_class_members(
7115            src,
7116            &[
7117                (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
7118                (Kotlin::InfixExpression as u16, 4, "the infix calls"),
7119                (Kotlin::AMPAMP as u16, 1, "`whenAmp`'s `&&`"),
7120            ],
7121            &[
7122                // 0 before this fix: the predicate slot saw a kind it
7123                // did not classify and no token arm matched.
7124                ("whenAnd", 1, 2),
7125                ("whenOr", 1, 2),
7126                ("whenXor", 1, 2),
7127                // Never regressed — `&&` is a `binary_expression` whose
7128                // operator the token arm owns. Holds the others level.
7129                ("whenAmp", 2, 3),
7130                // The agreement #1421 exists to establish: a
7131                // subject-less arm scores what the equivalent `if`
7132                // predicate scores.
7133                ("ifAnd", 1, 2),
7134            ],
7135        );
7136    }
7137
7138    #[cfg(feature = "kotlin")]
7139    #[test]
7140    fn kotlin_catch_block_counts() {
7141        check_metrics::<KotlinParser>(
7142            "fun m() {
7143                try {
7144                    println(\"ok\")
7145                } catch (e: Exception) {
7146                    println(\"err\")
7147                }
7148            }",
7149            "foo.kt",
7150            |metric| {
7151                // `try` (+1) and `catch` (+1) each contribute one condition,
7152                // matching Java / C# / C++ / Groovy (Fitzpatrick counts both
7153                // keywords). Before #696 Kotlin counted only the catch block.
7154                assert_eq!(metric.abc.conditions_sum(), 2);
7155                insta::assert_json_snapshot!(metric.abc);
7156            },
7157        );
7158    }
7159
7160    #[cfg(feature = "kotlin")]
7161    #[test]
7162    fn kotlin_elvis_and_safe_cast() {
7163        // `?:` (elvis) and `as?` (safe cast) are condition-like.
7164        check_metrics::<KotlinParser>(
7165            "fun m(s: String?): Int {
7166                val n = (s as? Int) ?: 0
7167                return n
7168            }",
7169            "foo.kt",
7170            |metric| {
7171                // as? (+1) + ?: (+1) = 2 conditions.
7172                assert_eq!(metric.abc.conditions_sum(), 2);
7173                insta::assert_json_snapshot!(metric.abc);
7174            },
7175        );
7176    }
7177
7178    #[cfg(feature = "kotlin")]
7179    #[test]
7180    fn kotlin_generic_brackets_not_conditions() {
7181        // `<` / `>` used as type-parameter brackets must not be counted.
7182        check_metrics::<KotlinParser>(
7183            "class Box<T>(val v: T)
7184            fun <T> wrap(x: T): Box<T> = Box(x)",
7185            "foo.kt",
7186            |metric| {
7187                // No comparisons — only generic brackets.
7188                assert_eq!(metric.abc.conditions_sum(), 0);
7189                insta::assert_json_snapshot!(metric.abc);
7190            },
7191        );
7192    }
7193
7194    // #1297: a qualified super call disambiguates its supertype with
7195    // the same two bare tokens a comparison uses — `super<A>.g()` is a
7196    // `super_expression`, which the previous denylist
7197    // (`type_arguments` / `type_parameters`) did not name, so it scored
7198    // two conditions. One `super<A>` against one genuine `a < b`
7199    // separates every mis-aim: 3 pre-fix, 1 once the arm allows
7200    // `BinaryExpression`, 0 if it allows the wrong parent or the
7201    // fixture stops parsing.
7202    #[cfg(feature = "kotlin")]
7203    #[test]
7204    fn kotlin_super_type_argument_is_not_a_condition() {
7205        check_metrics::<KotlinParser>(
7206            "class B : A() {
7207                override fun g(a: Int, b: Int): Int {
7208                    if (a < b) { return super<A>.g(a, b) }
7209                    return 0
7210                }
7211            }",
7212            "foo.kt",
7213            |metric| {
7214                assert_eq!(metric.abc.conditions_sum(), 1);
7215                // Non-vacuity guard: 1 is also what a body whose `if`
7216                // survived but whose super call did not would score, so
7217                // pin the call itself. Measured: dropping
7218                // `super<A>.g(a, b)` takes `branches_sum()` from 2 to 1 —
7219                // the remaining branch is the `A()` primary-constructor
7220                // delegation in the class header, which #1384 taught this
7221                // metric to count.
7222                assert_eq!(metric.abc.branches_sum(), 2);
7223            },
7224        );
7225    }
7226
7227    #[cfg(feature = "kotlin")]
7228    #[test]
7229    fn kotlin_class_with_methods_and_branches() {
7230        check_metrics::<KotlinParser>(
7231            "class C {
7232                var counter: Int = 0
7233                fun bump() {
7234                    counter += 1
7235                    println(counter)
7236                }
7237            }",
7238            "foo.kt",
7239            |metric| {
7240                // assignments: var counter = 0 (+1), counter += 1 (+1) = 2
7241                // branches: println(counter) = 1
7242                assert_eq!(metric.abc.assignments_sum(), 2);
7243                assert_eq!(metric.abc.branches_sum(), 1);
7244                assert_eq!(metric.abc.conditions_sum(), 0);
7245                insta::assert_json_snapshot!(metric.abc);
7246            },
7247        );
7248    }
7249
7250    #[cfg(feature = "kotlin")]
7251    #[test]
7252    fn kotlin_object_singleton_abc() {
7253        check_metrics::<KotlinParser>(
7254            "object Util {
7255                fun work(x: Int): Int {
7256                    var y = x
7257                    y += 1
7258                    if (y > 0) {
7259                        return y
7260                    }
7261                    return -1
7262                }
7263            }",
7264            "foo.kt",
7265            |metric| {
7266                // assignments: var y = x (+1), y += 1 (+1) = 2
7267                // branches: 0 (return is not a call)
7268                // conditions: y > 0 (+1) = 1
7269                assert_eq!(metric.abc.assignments_sum(), 2);
7270                assert_eq!(metric.abc.branches_sum(), 0);
7271                assert_eq!(metric.abc.conditions_sum(), 1);
7272                insta::assert_json_snapshot!(metric.abc);
7273            },
7274        );
7275    }
7276
7277    #[cfg(feature = "kotlin")]
7278    #[test]
7279    fn kotlin_interface_abc() {
7280        // Pure-abstract interface with no bodies — all-zero.
7281        check_metrics::<KotlinParser>(
7282            "interface I {
7283                fun work(): Int
7284                fun describe(): String
7285            }",
7286            "foo.kt",
7287            |metric| {
7288                assert_eq!(metric.abc.assignments_sum(), 0);
7289                assert_eq!(metric.abc.branches_sum(), 0);
7290                assert_eq!(metric.abc.conditions_sum(), 0);
7291                insta::assert_json_snapshot!(metric.abc);
7292            },
7293        );
7294    }
7295
7296    #[cfg(feature = "kotlin")]
7297    #[test]
7298    fn kotlin_nested_class_abc() {
7299        check_metrics::<KotlinParser>(
7300            "class Outer {
7301                var o: Int = 0
7302                class Nested {
7303                    var n: Int = 0
7304                    fun bump() { n += 1 }
7305                }
7306            }",
7307            "foo.kt",
7308            |metric| {
7309                // Outer: var o = 0 (+1)
7310                // Nested: var n = 0 (+1), n += 1 (+1) = 2
7311                // total assignments = 3
7312                assert_eq!(metric.abc.assignments_sum(), 3);
7313                insta::assert_json_snapshot!(metric.abc);
7314            },
7315        );
7316    }
7317
7318    #[cfg(feature = "kotlin")]
7319    #[test]
7320    fn kotlin_data_class_abc() {
7321        // `data class` with primary-constructor `val`s — no assignments
7322        // (vals don't count) and no body conditions.
7323        check_metrics::<KotlinParser>(
7324            "data class Point(val x: Int, val y: Int)",
7325            "foo.kt",
7326            |metric| {
7327                assert_eq!(metric.abc.assignments_sum(), 0);
7328                assert_eq!(metric.abc.branches_sum(), 0);
7329                assert_eq!(metric.abc.conditions_sum(), 0);
7330                insta::assert_json_snapshot!(metric.abc);
7331            },
7332        );
7333    }
7334
7335    #[cfg(feature = "kotlin")]
7336    #[test]
7337    fn kotlin_primary_constructor_default_value_not_assignment() {
7338        // Regression: default values on primary-constructor `val`
7339        // parameters are initialisers, not assignments. Without
7340        // `ClassParameter` pushing a declaration sentinel, the `=` token
7341        // here would be counted unconditionally as a standalone
7342        // assignment.
7343        check_metrics::<KotlinParser>("class C(val a: Int = 5)", "foo.kt", |metric| {
7344            // `val a = 5` → suppressed (Const sentinel).
7345            assert_eq!(metric.abc.assignments_sum(), 0);
7346            insta::assert_json_snapshot!(metric.abc);
7347        });
7348    }
7349
7350    #[cfg(feature = "kotlin")]
7351    #[test]
7352    fn kotlin_unary_conditions_in_chain() {
7353        // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
7354        // `&&` / `||` chain is one condition. `a && b || c` → a, b, c each
7355        // contribute one; the `&&` / `||` operators contribute nothing.
7356        // expected: 3 unary conditions, no comparisons, no `if`-keyword
7357        // condition in Kotlin (matches the Java byte-equivalent of 3).
7358        check_metrics::<KotlinParser>(
7359            "fun f(a: Boolean, b: Boolean, c: Boolean) {
7360                if (a && b || c) { println(\"x\") }
7361            }",
7362            "foo.kt",
7363            |metric| {
7364                assert_eq!(metric.abc.conditions_sum(), 3);
7365            },
7366        );
7367    }
7368
7369    #[cfg(feature = "kotlin")]
7370    #[test]
7371    fn kotlin_comparison_operands_add_nothing() {
7372        // Isolation check: comparison operands of a `&&` chain are nested
7373        // `binary_expression` nodes, not bare boolean leaves, so the
7374        // walker adds nothing — only the two `>` comparisons count.
7375        // expected: 2 (the two `>` tokens), walker contributes 0.
7376        check_metrics::<KotlinParser>(
7377            "fun g(x: Int, y: Int) {
7378                if (x > 0 && y > 0) { println(\"x\") }
7379            }",
7380            "foo.kt",
7381            |metric| {
7382                assert_eq!(metric.abc.conditions_sum(), 2);
7383            },
7384        );
7385    }
7386
7387    #[cfg(feature = "kotlin")]
7388    #[test]
7389    fn kotlin_negated_operand_is_unary_condition() {
7390        // A `!`-negated operand is still a unary condition: `a && !b`
7391        // unwraps the `unary_expression` to reach the inner identifier.
7392        // expected: 2 (`a` and the `!b` operand).
7393        check_metrics::<KotlinParser>(
7394            "fun f(a: Boolean, b: Boolean) {
7395                if (a && !b) { println(\"x\") }
7396            }",
7397            "foo.kt",
7398            |metric| {
7399                assert_eq!(metric.abc.conditions_sum(), 2);
7400            },
7401        );
7402    }
7403
7404    // The wrapper peel has two callers, and #1459's fixture members all
7405    // reach it through the *slot* (`kotlin_count_condition`). This is the
7406    // other one: the `&&` / `||` walker (`count_field_operands`),
7407    // which hands each operand to the same slot. The two wrappers #1459
7408    // taught it therefore have a
7409    // second, structurally independent path into the count, and a
7410    // fixture covering only the slot leaves it untested
7411    // (`.claude/rules/grammar-dispatch.md` §11).
7412    //
7413    // Each chain is one identifier plus one wrapped comparison, so 2 is
7414    // "the walker peeled the wrapper to the comparison, which pays for
7415    // itself" and 3 is "it paid the wrapper as a value as well". Since
7416    // the slot pays for any operand it cannot see a comparison inside
7417    // (#1533), a wrapped bare identifier scores 2 either way, so only a
7418    // wrapped comparison tells the peel from its absence. The bare
7419    // `a && b` control is `kotlin_unary_conditions_in_chain`'s shape at
7420    // 2, so a regression cannot be read as the chain itself changing.
7421    #[cfg(feature = "kotlin")]
7422    #[test]
7423    fn kotlin_chain_operands_peel_null_assertions_and_casts() {
7424        // Anchored per row, because the assertion alone cannot tell the
7425        // wrapper from its operand: `a && x > 1` scores the same 2 / 3 as
7426        // every row here, so trimming the `!!` or the cast out of a
7427        // fixture would leave this green with its subject gone
7428        // (`.claude/rules/testing.md`, "Perturb the fixture as well as
7429        // the production line").
7430        for (chain, spelling, anchors) in [
7431            (
7432                "a && (x > 1)!!",
7433                "null assertion",
7434                &[(Kotlin::BANGBANG as u16, 1usize, "the `!!`")][..],
7435            ),
7436            (
7437                "a && (x > 1) as Boolean",
7438                "cast",
7439                &[(Kotlin::AsExpression as u16, 1, "the `as` cast")][..],
7440            ),
7441            (
7442                "a && ((x > 1)!!)",
7443                "parenthesised null assertion",
7444                &[
7445                    (Kotlin::BANGBANG as u16, 1, "the `!!`"),
7446                    (Kotlin::ParenthesizedExpression as u16, 2, "the parens"),
7447                ][..],
7448            ),
7449        ] {
7450            let src = format!("fun f(a: Boolean, x: Int) {{ if ({chain}) {{ println(\"x\") }} }}");
7451            assert_fixture_spells::<KotlinParser>(&src, "foo.kt", anchors);
7452            check_func_space::<KotlinParser, _>(&src, "foo.kt", |space| {
7453                let f = child_space(&space, "f");
7454                assert_eq!(
7455                    f.metrics.abc.conditions(),
7456                    2,
7457                    "`{chain}`: the identifier plus the comparison under the {spelling}"
7458                );
7459                assert_eq!(f.metrics.cyclomatic.cyclomatic(), 3, "`{chain}`: decisions");
7460            });
7461        }
7462    }
7463
7464    #[cfg(feature = "kotlin")]
7465    #[test]
7466    fn kotlin_bare_if_predicate_is_one_condition() {
7467        // Issue #773: a bare-boolean `if` predicate (`if (flag)`) is one
7468        // Fitzpatrick unary condition. Before the Phase-2B arm it counted
7469        // 0, so `if (flag) 1 else -1` scored 1 (only the `else`) instead of
7470        // 2, dropping below Kotlin's own cyclomatic decision count.
7471        // expected: predicate (1) + else (1) = 2.
7472        check_metrics::<KotlinParser>(
7473            "fun m(flag: Boolean): Int { return if (flag) 1 else -1 }",
7474            "foo.kt",
7475            |metric| {
7476                assert_eq!(metric.abc.conditions_sum(), 2);
7477            },
7478        );
7479    }
7480
7481    #[cfg(feature = "kotlin")]
7482    #[test]
7483    fn kotlin_bare_while_predicate_is_one_condition() {
7484        // Issue #773: the bare predicate of a `while` loop counts one
7485        // condition via the `condition` field. expected: 1.
7486        check_metrics::<KotlinParser>(
7487            "fun m(running: Boolean) { while (running) { println(\"x\") } }",
7488            "foo.kt",
7489            |metric| {
7490                assert_eq!(metric.abc.conditions_sum(), 1);
7491            },
7492        );
7493    }
7494
7495    #[cfg(feature = "kotlin")]
7496    #[test]
7497    fn kotlin_bare_do_while_predicate_is_one_condition() {
7498        // Issue #773: the bare predicate of a `do`/`while` loop counts one
7499        // condition. expected: 1.
7500        check_metrics::<KotlinParser>(
7501            "fun m(ok: Boolean) { do { println(\"x\") } while (ok) }",
7502            "foo.kt",
7503            |metric| {
7504                assert_eq!(metric.abc.conditions_sum(), 1);
7505            },
7506        );
7507    }
7508
7509    #[cfg(feature = "kotlin")]
7510    #[test]
7511    fn kotlin_comparison_predicate_not_double_counted() {
7512        // Double-count guard (#773): a comparison predicate (`if (a == b)`)
7513        // is a nested `binary_expression` already counted by the `==` token
7514        // arm, so the Phase-2B condition-slot arm must add nothing here.
7515        // expected: `==` (1) + else (1) = 2 — unchanged by the new arm.
7516        check_metrics::<KotlinParser>(
7517            "fun m(a: Int, b: Int): Int { return if (a == b) 1 else -1 }",
7518            "foo.kt",
7519            |metric| {
7520                assert_eq!(metric.abc.conditions_sum(), 2);
7521            },
7522        );
7523    }
7524
7525    #[cfg(feature = "kotlin")]
7526    #[test]
7527    fn kotlin_short_circuit_predicate_not_double_counted() {
7528        // Double-count guard (#773): an `&&`/`||` predicate is counted by
7529        // the Rule 9 chain walker (each operand once); the Phase-2B arm
7530        // must add nothing for it. expected: `x` (1) + `y` (1) + else (1)
7531        // = 3 — unchanged by the new arm.
7532        check_metrics::<KotlinParser>(
7533            "fun m(x: Boolean, y: Boolean): Int { return if (x && y) 1 else -1 }",
7534            "foo.kt",
7535            |metric| {
7536                assert_eq!(metric.abc.conditions_sum(), 3);
7537            },
7538        );
7539    }
7540
7541    #[cfg(feature = "kotlin")]
7542    #[test]
7543    fn kotlin_parenthesised_bare_predicate_is_one_condition() {
7544        // A parenthesised bare predicate (`if ((flag))`) is unwrapped by
7545        // `kotlin_wrapper_operand` and still counts one condition (#773).
7546        // expected: 1.
7547        check_metrics::<KotlinParser>(
7548            "fun m(flag: Boolean) { if ((flag)) { println(\"x\") } }",
7549            "foo.kt",
7550            |metric| {
7551                assert_eq!(metric.abc.conditions_sum(), 1);
7552            },
7553        );
7554    }
7555
7556    // --- TypeScript / TSX ABC tests --------------------------------------
7557    //
7558    // Assignment, branch, condition counting per Fitzpatrick:
7559    // - Augmented assignment / `++` / `--` always count.
7560    // - Plain `=` counts unless inside `const` declaration.
7561    // - `call_expression` / `new_expression` count as branches.
7562    // - Comparison / equality operators, ternary `?`, `??`, control-flow
7563    //   arms (`else`, `case`, `default`, `catch`, `try`, `instanceof`),
7564    //   and `<`/`>` (outside `type_arguments` / `type_parameters`) count
7565    //   as conditions.
7566
7567    #[cfg(feature = "typescript")]
7568    #[test]
7569    fn typescript_assignments_basic() {
7570        check_metrics::<TypescriptParser>(
7571            "class C {
7572                m(): void {
7573                    let x = 0;          // +1 — only a `const` initializer is suppressed
7574                    x = 1;              // +1
7575                    x += 2;             // +1
7576                    x++;                // +1
7577                }
7578            }",
7579            "foo.ts",
7580            |metric| {
7581                assert_eq!(metric.abc.assignments_sum(), 4);
7582                insta::assert_json_snapshot!(metric.abc);
7583            },
7584        );
7585    }
7586
7587    #[cfg(feature = "typescript")]
7588    #[test]
7589    fn typescript_const_excluded_from_assignments() {
7590        check_metrics::<TypescriptParser>(
7591            "class C {
7592                m(): void {
7593                    const a = 1;        // suppressed (`const` initializer)
7594                    const b = 2;        // suppressed
7595                    let c = 3;          // +1 — `let` initializers count
7596                }
7597            }",
7598            "foo.ts",
7599            |metric| {
7600                assert_eq!(metric.abc.assignments_sum(), 1);
7601                insta::assert_json_snapshot!(metric.abc);
7602            },
7603        );
7604    }
7605
7606    // Regression cluster for #1277. The pre-fix implementation decided
7607    // "is this `=` a `const` initializer?" from a sentinel stack cleared
7608    // only on a `SEMI` token, so the answer for one statement depended on
7609    // the *previous* statement's terminator. Automatic semicolon
7610    // insertion makes that terminator optional in all four JS-family
7611    // languages. The replacement is structural — see
7612    // `impl_js_family_const_binding!` in `src/metrics/abc/js_family.rs`.
7613
7614    #[cfg(feature = "typescript")]
7615    #[test]
7616    fn typescript_asi_const_does_not_suppress_later_assignments() {
7617        check_metrics::<TypescriptParser>(
7618            "function f() {
7619                const a = 1
7620                x = 2
7621                y = 3
7622            }",
7623            "foo.ts",
7624            |metric| {
7625                // `const a = 1` suppressed; `x = 2` and `y = 3` count.
7626                // Pre-#1277 this reported 0: the unterminated `const`
7627                // never popped its sentinel.
7628                assert_eq!(metric.abc.assignments_sum(), 2);
7629            },
7630        );
7631    }
7632
7633    #[cfg(feature = "typescript")]
7634    #[test]
7635    fn typescript_semicolon_const_does_not_suppress_later_assignments() {
7636        // The semicolon-terminated spelling of the fixture above, which
7637        // must score the same as it. Pre-#1277 they scored 2 and 0 — the
7638        // pair is what pins the terminator out of the answer.
7639        check_metrics::<TypescriptParser>(
7640            "function f() {
7641                const a = 1;
7642                x = 2;
7643                y = 3;
7644            }",
7645            "foo.ts",
7646            |metric| {
7647                assert_eq!(metric.abc.assignments_sum(), 2);
7648            },
7649        );
7650    }
7651
7652    #[cfg(feature = "typescript")]
7653    #[test]
7654    fn typescript_as_const_does_not_suppress_later_assignments() {
7655        // The sentinel stack was also reachable from the other side: the
7656        // `const` token of a TypeScript `x as const` assertion promoted a
7657        // live `let` slot to `Const` and suppressed every `=` until the
7658        // next `;`. Structurally that `const` is a child of an
7659        // `as_expression`, not a declaration keyword.
7660        check_metrics::<TypescriptParser>(
7661            "function f(x: number) {
7662                let y = x as const
7663                w = 3
7664            }",
7665            "foo.ts",
7666            |metric| {
7667                // `let` initializer (+1) and `w = 3` (+1); pre-#1277: 1.
7668                assert_eq!(metric.abc.assignments_sum(), 2);
7669            },
7670        );
7671    }
7672
7673    #[cfg(feature = "typescript")]
7674    #[test]
7675    fn typescript_const_declarator_shapes_stay_suppressed() {
7676        // Shapes the sentinel stack handled implicitly, which the
7677        // structural predicate must reproduce: *every* declarator under a
7678        // `const`-bearing `lexical_declaration` is suppressed, including
7679        // the second element of a multi-declarator list, a destructuring
7680        // pattern, and the defaults inside one — `c = 5`, the nested
7681        // `e = 6` and its `= {}`, `g = 7`, and the rest element's `h = 8`,
7682        // each one more pattern layer the predicate's climb has to cross
7683        // — while `for (const x of xs)` carries no `=` at all. Only
7684        // `let i = 3` and `var j = 4` count — the deliberate deviation
7685        // documented on `js_abc_compute!`. Gating on
7686        // `variable_declaration` instead of `lexical_declaration`,
7687        // dropping the `const` check, or stopping the climb at the
7688        // declarator's own `=`, each flips one of these rows. The
7689        // `for`-of row is the exception: it carries no `=`, so no change
7690        // to the predicate can move it. It pins the grammar shape instead
7691        // — a future grammar emitting a `variable_declarator` there would
7692        // start suppressing something that never counted.
7693        check_metrics::<TypescriptParser>(
7694            "function f(o: any, xs: number[]) {
7695                const a = 1, b = 2
7696                const {c = 5, d: {e = 6} = {}} = o
7697                const [g = 7, ...[h = 8]] = xs
7698                let i = 3
7699                var j = 4
7700                for (const x of xs) { k(x) }
7701            }",
7702            "foo.ts",
7703            |metric| {
7704                assert_eq!(metric.abc.assignments_sum(), 2);
7705            },
7706        );
7707    }
7708
7709    #[cfg(feature = "typescript")]
7710    #[test]
7711    fn typescript_const_initializer_value_assignments_still_count() {
7712        // TypeScript half of
7713        // `javascript_const_initializer_value_assignments_still_count`.
7714        check_metrics::<TypescriptParser>(
7715            "function m(o: any, a: any, b: any) { const x = (o.p = 1); const y = a || (b = 2); }",
7716            "foo.ts",
7717            |metric| {
7718                assert_eq!(metric.abc.assignments_sum(), 2);
7719            },
7720        );
7721    }
7722
7723    #[cfg(feature = "typescript")]
7724    #[test]
7725    fn typescript_branches_function_calls() {
7726        check_metrics::<TypescriptParser>(
7727            "class C {
7728                m(): void {
7729                    foo();              // +1
7730                    bar(1, 2);          // +1
7731                    new Date();         // +1
7732                }
7733            }",
7734            "foo.ts",
7735            |metric| {
7736                assert_eq!(metric.abc.branches_sum(), 3);
7737                insta::assert_json_snapshot!(metric.abc);
7738            },
7739        );
7740    }
7741
7742    #[cfg(feature = "typescript")]
7743    #[test]
7744    fn typescript_conditions_comparison_operators() {
7745        check_metrics::<TypescriptParser>(
7746            "class C {
7747                m(x: number, y: number): boolean {
7748                    return x == y       // +1
7749                        || x === y      // +1
7750                        || x != y       // +1
7751                        || x !== y      // +1
7752                        || x < y        // +1
7753                        || x <= y       // +1
7754                        || x > y        // +1
7755                        || x >= y;      // +1
7756                }
7757            }",
7758            "foo.ts",
7759            |metric| {
7760                assert_eq!(metric.abc.conditions_sum(), 8);
7761                insta::assert_json_snapshot!(metric.abc);
7762            },
7763        );
7764    }
7765
7766    #[cfg(feature = "typescript")]
7767    #[test]
7768    fn typescript_conditions_control_flow_arms() {
7769        check_metrics::<TypescriptParser>(
7770            "class C {
7771                m(x: number): number {
7772                    try {                       // +1 (try)
7773                        if (x > 0) {            // +1 (>)
7774                            return 1;
7775                        } else {                // +1 (else)
7776                            return -1;
7777                        }
7778                    } catch (e) {               // +1 (catch)
7779                        return 0;
7780                    }
7781                }
7782            }",
7783            "foo.ts",
7784            |metric| {
7785                assert_eq!(metric.abc.conditions_sum(), 4);
7786                insta::assert_json_snapshot!(metric.abc);
7787            },
7788        );
7789    }
7790
7791    #[cfg(feature = "typescript")]
7792    #[test]
7793    fn typescript_conditions_switch_case() {
7794        check_metrics::<TypescriptParser>(
7795            "class C {
7796                m(x: number): number {
7797                    switch (x) {
7798                        case 1:                 // +1
7799                            return 1;
7800                        case 2:                 // +1
7801                            return 2;
7802                        default:                // +0 (fallthrough, #469)
7803                            return 0;
7804                    }
7805                }
7806            }",
7807            "foo.ts",
7808            |metric| {
7809                assert_eq!(metric.abc.conditions_sum(), 2);
7810                insta::assert_json_snapshot!(metric.abc);
7811            },
7812        );
7813    }
7814
7815    #[cfg(feature = "typescript")]
7816    #[test]
7817    fn typescript_ternary_and_nullish() {
7818        check_metrics::<TypescriptParser>(
7819            "class C {
7820                m(x: number | null): number {
7821                    return x !== null           // +1 (!==)
7822                        ? x                     // +1 (ternary ?)
7823                        : 0;
7824                }
7825                n(x: number | null): number {
7826                    return x ?? 0;              // +1 (??)
7827                }
7828            }",
7829            "foo.ts",
7830            |metric| {
7831                assert_eq!(metric.abc.conditions_sum(), 3);
7832                insta::assert_json_snapshot!(metric.abc);
7833            },
7834        );
7835    }
7836
7837    #[cfg(feature = "typescript")]
7838    #[test]
7839    fn typescript_instanceof_counts_as_condition() {
7840        check_metrics::<TypescriptParser>(
7841            "class C {
7842                m(o: unknown): boolean {
7843                    return o instanceof C;      // +1
7844                }
7845            }",
7846            "foo.ts",
7847            |metric| {
7848                assert_eq!(metric.abc.conditions_sum(), 1);
7849                insta::assert_json_snapshot!(metric.abc);
7850            },
7851        );
7852    }
7853
7854    #[cfg(feature = "typescript")]
7855    #[test]
7856    fn typescript_generic_lt_gt_not_a_condition() {
7857        // `<T>` in `class C<T>` and `Array<number>` should not contribute
7858        // to conditions even though the tokens are `<` and `>`.
7859        check_metrics::<TypescriptParser>(
7860            "class C<T> {
7861                xs: Array<number> = [];
7862                m(): void {
7863                    const arr: Array<string> = [];   // suppressed const
7864                    void arr;
7865                }
7866            }",
7867            "foo.ts",
7868            |metric| {
7869                assert_eq!(metric.abc.conditions_sum(), 0);
7870                insta::assert_json_snapshot!(metric.abc);
7871            },
7872        );
7873    }
7874
7875    // #1297, TypeScript half: the `<` / `>` arm flipped from denying
7876    // `type_arguments` / `type_parameters` to allowing
7877    // `binary_expression`, and this pins the direction that flip could
7878    // have broken. `Array<string>` keeps a generic in scope so the
7879    // fixture proves both sides at once, and the assertion is the
7880    // grammar-dispatch §8 pin: on the function's own space the ABC
7881    // condition count equals the cyclomatic decision count
7882    // (`cyclomatic()` minus the per-space base of 1). Both are 2, one
7883    // per `if`.
7884    //
7885    // As with the Lua fixture, `Array<string>` leaves no ABC trace once
7886    // excluded, so nothing here fails if a later edit drops the
7887    // generic; that the generic reaches the arm is established by the
7888    // revert test, which fails this test when the gate is removed.
7889    //
7890    // TypeScript gets no JSX fixture because the `.ts` dialect has no
7891    // JSX to exercise: it lexes `return <div>…</div>` as a type
7892    // assertion and a comparison chain, sometimes with `ERROR` nodes and
7893    // sometimes without, depending on what follows the tag. Either way
7894    // the tokens land under `type_arguments` and `binary_expression`
7895    // rather than the `jsx_*` productions, so the construct that
7896    // motivated #1297 is only reachable through
7897    // `tsx_jsx_elements_are_not_conditions` below.
7898    //
7899    // That mis-parse still over-counts — a `.ts` file containing JSX
7900    // scores conditions this gate cannot exclude, because the grammar
7901    // genuinely reports a `binary_expression`. It is a wrong-dialect
7902    // input rather than a defect in this arm, and no allowlist can
7903    // distinguish it; see the same caveat on Kotlin's arm.
7904    #[cfg(feature = "typescript")]
7905    #[test]
7906    fn typescript_comparison_operators_still_count_alongside_generics() {
7907        check_func_space::<TypescriptParser, _>(
7908            "function m(xs: Array<string>, a: number, b: number): number {
7909                if (a < b) { return 1; }
7910                if (a > b) { return 2; }
7911                void xs;
7912                return 0;
7913            }",
7914            "foo.ts",
7915            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
7916        );
7917    }
7918
7919    // #1275, TypeScript half. Eleven grammar productions emit a bare
7920    // `?` and only `ternary_expression` is a decision; the other ten are
7921    // type syntax. This fixture exercises five of them — `optional_
7922    // parameter`, `property_signature`, `method_signature`,
7923    // `abstract_method_signature` and `public_field_definition`, plus an
7924    // `optional_type` in a tuple — against one real ternary over one
7925    // real `>`.
7926    //
7927    // Six type-syntax `?` means the numbers separate cleanly: 8 pre-fix,
7928    // 2 once the allowlist is aimed at `TernaryExpression`, 1 if it is
7929    // aimed at anything else (the ternary stops counting too), 0 if the
7930    // fixture stops parsing. Any partial gate — one that named some of
7931    // the type-syntax parents in a denylist instead — lands between 3
7932    // and 7 and is equally visible.
7933    #[cfg(feature = "typescript")]
7934    #[test]
7935    fn typescript_optional_type_syntax_is_not_a_condition() {
7936        check_metrics::<TypescriptParser>(
7937            "interface I { a?: string; m?(x: number): void; }
7938            abstract class K { f?: number; abstract g?(): void; }
7939            type Tup = [number, string?];
7940            function h(x?: number, y: number = 0): number { return y > 1 ? 1 : 2; }",
7941            "foo.ts",
7942            |metric| {
7943                assert_eq!(metric.abc.conditions_sum(), 2);
7944            },
7945        );
7946    }
7947
7948    // The explicit half of #1275's TypeScript decision: a conditional
7949    // type (`T extends U ? X : Y`) is resolved by the type checker and
7950    // erased before runtime, so its `?` is not a condition. That falls
7951    // out of the `TernaryExpression` allowlist rather than being named,
7952    // which is exactly why it needs its own test — the choice reads as
7953    // an omission otherwise, and nothing else here would notice if a
7954    // later edit added `ConditionalType` to the allowlist "for
7955    // symmetry".
7956    //
7957    // The real ternary below keeps the expectation off zero: 3 pre-fix,
7958    // 2 with the conditional type excluded, 1 if the ternary is
7959    // swallowed too.
7960    #[cfg(feature = "typescript")]
7961    #[test]
7962    fn typescript_conditional_type_is_not_a_condition() {
7963        check_metrics::<TypescriptParser>(
7964            "type Cond<T> = T extends string ? number : boolean;
7965            function pick(a: number, b: number): number { return a > b ? 1 : 2; }",
7966            "foo.ts",
7967            |metric| {
7968                assert_eq!(metric.abc.conditions_sum(), 2);
7969            },
7970        );
7971    }
7972
7973    // grammar-dispatch §2. `Typescript::QMARK2` is the enum entry for
7974    // the external scanner's `_ternary_qmark` token, and
7975    // tree-sitter-typescript's public symbol map folds it back onto
7976    // `anon_sym_QMARK`, so `kind_id()` never reports it — every ternary
7977    // `?` in the fixture below arrives as plain `QMARK`. Listing
7978    // `QMARK2` in the gated arm would therefore be dead code, and
7979    // leaving it out is safe only for as long as that mapping holds.
7980    // This pins both halves so a grammar bump that starts exposing the
7981    // alias fails here rather than silently zeroing every TypeScript
7982    // ternary.
7983    //
7984    // The fixture's only `?` is the ternary's, deliberately. An optional
7985    // parameter (`b?: number`) would emit a `QMARK` of its own and
7986    // satisfy the positive assertion on its own, leaving it true no
7987    // matter what id the ternary's `?` came back as — decoration rather
7988    // than the non-vacuity guard it is here for.
7989    #[cfg(feature = "typescript")]
7990    #[test]
7991    fn typescript_ternary_qmark_alias_stays_unreachable() {
7992        let parser = TypescriptParser::new(
7993            "function f(a: boolean): number { return a ? 1 : 2; }\n"
7994                .as_bytes()
7995                .to_vec(),
7996            std::path::Path::new("foo.ts"),
7997            None,
7998        );
7999        assert!(ast_has_kind_id(&parser, Typescript::QMARK as u16));
8000        assert!(!ast_has_kind_id(&parser, Typescript::QMARK2 as u16));
8001    }
8002
8003    #[cfg(feature = "typescript")]
8004    #[test]
8005    fn typescript_abstract_class_abc() {
8006        // Abstract methods have no body — they contribute nothing.
8007        check_metrics::<TypescriptParser>(
8008            "abstract class C {
8009                abstract a(): void;
8010                m(x: number): number {
8011                    if (x > 0) return 1;        // +1 condition
8012                    return 0;
8013                }
8014            }",
8015            "foo.ts",
8016            |metric| {
8017                assert_eq!(metric.abc.conditions_sum(), 1);
8018                assert_eq!(metric.abc.branches_sum(), 0);
8019                insta::assert_json_snapshot!(metric.abc);
8020            },
8021        );
8022    }
8023
8024    #[cfg(feature = "typescript")]
8025    #[test]
8026    fn typescript_interface_abc_zero() {
8027        check_metrics::<TypescriptParser>(
8028            "interface I {
8029                a(): void;
8030                b(): number;
8031                p: string;
8032            }",
8033            "foo.ts",
8034            |metric| {
8035                assert_eq!(metric.abc.assignments_sum(), 0);
8036                assert_eq!(metric.abc.branches_sum(), 0);
8037                assert_eq!(metric.abc.conditions_sum(), 0);
8038                insta::assert_json_snapshot!(metric.abc);
8039            },
8040        );
8041    }
8042
8043    #[cfg(feature = "typescript")]
8044    #[test]
8045    fn typescript_arrow_field_contributes_abc() {
8046        // Arrow function class members are function spaces; their
8047        // assignments/branches/conditions are counted.
8048        check_metrics::<TypescriptParser>(
8049            "class C {
8050                arrow = (x: number) => {
8051                    if (x > 0) {                // +1 condition
8052                        return foo();           // +1 branch
8053                    }
8054                    return 0;
8055                };
8056            }",
8057            "foo.ts",
8058            |metric| {
8059                assert_eq!(metric.abc.conditions_sum(), 1);
8060                assert_eq!(metric.abc.branches_sum(), 1);
8061                insta::assert_json_snapshot!(metric.abc);
8062            },
8063        );
8064    }
8065
8066    #[cfg(feature = "typescript")]
8067    #[test]
8068    fn typescript_parameter_property_init_not_assignment() {
8069        // Parameter properties don't introduce a `=` token themselves;
8070        // only the explicit `let z = 0` body assignment is counted.
8071        // The class field initializer `f: number = 0` likewise has a `=`
8072        // that DOES count (matches `typescript_assignments_basic`).
8073        check_metrics::<TypescriptParser>(
8074            "class C {
8075                f: number = 0;
8076                constructor(public x: number, private y: string) {
8077                    let z = 0;
8078                }
8079            }",
8080            "foo.ts",
8081            |metric| {
8082                // f's initializer + `let z = 0` = 2 assignments; the
8083                // parameter properties contribute zero.
8084                assert_eq!(metric.abc.assignments_sum(), 2);
8085                insta::assert_json_snapshot!(metric.abc);
8086            },
8087        );
8088    }
8089
8090    // TSX parity
8091
8092    #[cfg(feature = "typescript")]
8093    #[test]
8094    fn tsx_assignments_basic() {
8095        check_metrics::<TsxParser>(
8096            "class C {
8097                m(): void {
8098                    let x = 0;
8099                    x = 1;
8100                    x += 2;
8101                    x++;
8102                }
8103            }",
8104            "foo.tsx",
8105            |metric| {
8106                assert_eq!(metric.abc.assignments_sum(), 4);
8107                insta::assert_json_snapshot!(metric.abc);
8108            },
8109        );
8110    }
8111
8112    #[cfg(feature = "typescript")]
8113    #[test]
8114    fn tsx_const_excluded_from_assignments() {
8115        check_metrics::<TsxParser>(
8116            "class C {
8117                m(): void {
8118                    const a = 1;
8119                    let b = 2;
8120                }
8121            }",
8122            "foo.tsx",
8123            |metric| {
8124                assert_eq!(metric.abc.assignments_sum(), 1);
8125                insta::assert_json_snapshot!(metric.abc);
8126            },
8127        );
8128    }
8129
8130    #[cfg(feature = "typescript")]
8131    #[test]
8132    fn tsx_branches_function_calls() {
8133        check_metrics::<TsxParser>(
8134            "class C {
8135                m(): void {
8136                    foo();
8137                    new Date();
8138                }
8139            }",
8140            "foo.tsx",
8141            |metric| {
8142                assert_eq!(metric.abc.branches_sum(), 2);
8143                insta::assert_json_snapshot!(metric.abc);
8144            },
8145        );
8146    }
8147
8148    #[cfg(feature = "typescript")]
8149    #[test]
8150    fn tsx_conditions_comparison_operators() {
8151        check_metrics::<TsxParser>(
8152            "class C {
8153                m(x: number, y: number): boolean {
8154                    return x == y || x < y || x >= y;
8155                }
8156            }",
8157            "foo.tsx",
8158            |metric| {
8159                assert_eq!(metric.abc.conditions_sum(), 3);
8160                insta::assert_json_snapshot!(metric.abc);
8161            },
8162        );
8163    }
8164
8165    #[cfg(feature = "typescript")]
8166    #[test]
8167    fn tsx_conditions_control_flow_arms() {
8168        check_metrics::<TsxParser>(
8169            "class C {
8170                m(x: number): number {
8171                    try {
8172                        if (x > 0) return 1;
8173                        else return -1;
8174                    } catch (e) {
8175                        return 0;
8176                    }
8177                }
8178            }",
8179            "foo.tsx",
8180            |metric| {
8181                assert_eq!(metric.abc.conditions_sum(), 4);
8182                insta::assert_json_snapshot!(metric.abc);
8183            },
8184        );
8185    }
8186
8187    #[cfg(feature = "typescript")]
8188    #[test]
8189    fn tsx_conditions_switch_case() {
8190        check_metrics::<TsxParser>(
8191            "class C {
8192                m(x: number): number {
8193                    switch (x) {
8194                        case 1: return 1;       // +1
8195                        case 2: return 2;       // +1
8196                        default: return 0;      // +0 (fallthrough, #469)
8197                    }
8198                }
8199            }",
8200            "foo.tsx",
8201            |metric| {
8202                assert_eq!(metric.abc.conditions_sum(), 2);
8203                insta::assert_json_snapshot!(metric.abc);
8204            },
8205        );
8206    }
8207
8208    #[cfg(feature = "typescript")]
8209    #[test]
8210    fn tsx_ternary_and_nullish() {
8211        check_metrics::<TsxParser>(
8212            "class C {
8213                m(x: number | null): number {
8214                    return x !== null ? x : 0;
8215                }
8216                n(x: number | null): number { return x ?? 0; }
8217            }",
8218            "foo.tsx",
8219            |metric| {
8220                assert_eq!(metric.abc.conditions_sum(), 3);
8221                insta::assert_json_snapshot!(metric.abc);
8222            },
8223        );
8224    }
8225
8226    #[cfg(feature = "typescript")]
8227    #[test]
8228    fn tsx_instanceof_counts_as_condition() {
8229        check_metrics::<TsxParser>(
8230            "class C { m(o: unknown): boolean { return o instanceof C; } }",
8231            "foo.tsx",
8232            |metric| {
8233                assert_eq!(metric.abc.conditions_sum(), 1);
8234                insta::assert_json_snapshot!(metric.abc);
8235            },
8236        );
8237    }
8238
8239    #[cfg(feature = "typescript")]
8240    #[test]
8241    fn tsx_generic_lt_gt_not_a_condition() {
8242        check_metrics::<TsxParser>(
8243            "class C<T> { xs: Array<number> = []; }",
8244            "foo.tsx",
8245            |metric| {
8246                assert_eq!(metric.abc.conditions_sum(), 0);
8247                insta::assert_json_snapshot!(metric.abc);
8248            },
8249        );
8250    }
8251
8252    // #1297: a JSX tag delimiter is markup punctuation, not a
8253    // comparison. The `<` / `>` arm denied only `type_arguments` and
8254    // `type_parameters`, so every tag bracket scored a condition —
8255    // `jsx_opening_element` contributes both tokens, `jsx_closing_element`
8256    // a `>` (its `</` is one token) and `jsx_self_closing_element` a `<`
8257    // (its `/>` is one token).
8258    //
8259    // The fixture pairs seven such brackets — two elements' opening and
8260    // closing tags, plus one self-closing tag — against one genuine
8261    // `a < b`, so every way of mis-aiming the gate lands on its own
8262    // number: 8 pre-fix, 1 once it allows `BinaryExpression`, 0 if it
8263    // allows the wrong parent or the fixture stops parsing. Asserting 0
8264    // on a JSX-only body would not have separated those last two.
8265    #[cfg(feature = "typescript")]
8266    #[test]
8267    fn tsx_jsx_elements_are_not_conditions() {
8268        check_metrics::<TsxParser>(
8269            "function f(a: number, b: number) {
8270                if (a < b) { return <div className=\"x\"><span>hi</span></div>; }
8271                return <br />;
8272            }",
8273            "foo.tsx",
8274            |metric| {
8275                assert_eq!(metric.abc.conditions_sum(), 1);
8276                // Fixture-presence anchor: the `className="x"` attribute
8277                // `=` is this fixture's only assignment, so the test
8278                // fails if a later edit drops the JSX. Without it the
8279                // surviving `a < b` alone also scores 1, and the
8280                // conditions assertion would pass while proving nothing.
8281                assert_eq!(metric.abc.assignments_sum(), 1);
8282            },
8283        );
8284    }
8285
8286    // #1275 in the second expansion of `ts_abc_compute!`. TSX shares
8287    // TypeScript's `?` productions and its own `TernaryExpression` /
8288    // `QMARK` ids, so the gate is a distinct instantiation and needs its
8289    // own fixture — a passing TypeScript test says nothing about the
8290    // macro's other expansion. Four type-syntax `?` plus one `>` and one
8291    // ternary: 6 pre-fix, 2 after, 1 if the allowlist is misaimed.
8292    #[cfg(feature = "typescript")]
8293    #[test]
8294    fn tsx_optional_type_syntax_is_not_a_condition() {
8295        check_metrics::<TsxParser>(
8296            "interface I { a?: string; m?(x: number): void; }
8297            class K { f?: number; }
8298            function h(x?: number, y: number = 0): number { return y > 1 ? 1 : 2; }",
8299            "foo.tsx",
8300            |metric| {
8301                assert_eq!(metric.abc.conditions_sum(), 2);
8302            },
8303        );
8304    }
8305
8306    // The TSX half of `typescript_conditional_type_is_not_a_condition`.
8307    // `ts_abc_compute!` expands twice and the two expansions are
8308    // independent code (grammar-dispatch §11): adding `ConditionalType`
8309    // to the allowlist "for symmetry" fails only the TypeScript test
8310    // without this one, which reads as the TSX expansion being fine
8311    // rather than untested. `conditional_type` is in the tsx grammar's
8312    // `?` set exactly as it is in typescript's.
8313    #[cfg(feature = "typescript")]
8314    #[test]
8315    fn tsx_conditional_type_is_not_a_condition() {
8316        check_metrics::<TsxParser>(
8317            "type Cond<T> = T extends string ? number : boolean;
8318            function pick(a: number, b: number): number { return a > b ? 1 : 2; }",
8319            "foo.tsx",
8320            |metric| {
8321                assert_eq!(metric.abc.conditions_sum(), 2);
8322            },
8323        );
8324    }
8325
8326    // The TSX half of `typescript_ternary_qmark_alias_stays_unreachable`
8327    // — the tsx grammar declares the same `_ternary_qmark` external and
8328    // maps it back onto `anon_sym_QMARK` at its own id. Same
8329    // single-`?` fixture rule; see that test for why.
8330    #[cfg(feature = "typescript")]
8331    #[test]
8332    fn tsx_ternary_qmark_alias_stays_unreachable() {
8333        let parser = TsxParser::new(
8334            "function f(a: boolean): number { return a ? 1 : 2; }\n"
8335                .as_bytes()
8336                .to_vec(),
8337            std::path::Path::new("foo.tsx"),
8338            None,
8339        );
8340        assert!(ast_has_kind_id(&parser, Tsx::QMARK as u16));
8341        assert!(!ast_has_kind_id(&parser, Tsx::QMARK2 as u16));
8342    }
8343
8344    #[cfg(feature = "typescript")]
8345    #[test]
8346    fn tsx_abstract_class_abc() {
8347        check_metrics::<TsxParser>(
8348            "abstract class C {
8349                abstract a(): void;
8350                m(x: number): number {
8351                    if (x > 0) return 1;
8352                    return 0;
8353                }
8354            }",
8355            "foo.tsx",
8356            |metric| {
8357                assert_eq!(metric.abc.conditions_sum(), 1);
8358                assert_eq!(metric.abc.branches_sum(), 0);
8359                insta::assert_json_snapshot!(metric.abc);
8360            },
8361        );
8362    }
8363
8364    #[cfg(feature = "typescript")]
8365    #[test]
8366    fn tsx_interface_abc_zero() {
8367        check_metrics::<TsxParser>(
8368            "interface I { a(): void; p: string; }",
8369            "foo.tsx",
8370            |metric| {
8371                assert_eq!(metric.abc.assignments_sum(), 0);
8372                assert_eq!(metric.abc.branches_sum(), 0);
8373                assert_eq!(metric.abc.conditions_sum(), 0);
8374                insta::assert_json_snapshot!(metric.abc);
8375            },
8376        );
8377    }
8378
8379    #[cfg(feature = "typescript")]
8380    #[test]
8381    fn tsx_arrow_field_contributes_abc() {
8382        check_metrics::<TsxParser>(
8383            "class C {
8384                arrow = (x: number) => {
8385                    if (x > 0) return foo();
8386                    return 0;
8387                };
8388            }",
8389            "foo.tsx",
8390            |metric| {
8391                assert_eq!(metric.abc.conditions_sum(), 1);
8392                assert_eq!(metric.abc.branches_sum(), 1);
8393                insta::assert_json_snapshot!(metric.abc);
8394            },
8395        );
8396    }
8397
8398    #[cfg(feature = "typescript")]
8399    #[test]
8400    fn tsx_asi_const_does_not_suppress_later_assignments() {
8401        // TSX half of the #1277 cluster; see
8402        // `typescript_asi_const_does_not_suppress_later_assignments`.
8403        // TSX has its own `Const` / `VariableDeclarator` /
8404        // `LexicalDeclaration` kind ids, so the predicate is generated
8405        // separately and needs its own fixture.
8406        check_metrics::<TsxParser>(
8407            "function f() {
8408                const a = 1
8409                x = 2
8410                y = 3
8411            }",
8412            "foo.tsx",
8413            |metric| {
8414                assert_eq!(metric.abc.assignments_sum(), 2);
8415            },
8416        );
8417    }
8418
8419    #[cfg(feature = "typescript")]
8420    #[test]
8421    fn tsx_const_declarator_shapes_stay_suppressed() {
8422        // TSX half of `typescript_const_declarator_shapes_stay_suppressed`.
8423        check_metrics::<TsxParser>(
8424            "function f(o: any, xs: number[]) {
8425                const a = 1, b = 2
8426                const {c = 5, d: {e = 6} = {}} = o
8427                const [g = 7, ...[h = 8]] = xs
8428                let i = 3
8429                var j = 4
8430                for (const x of xs) { k(x) }
8431            }",
8432            "foo.tsx",
8433            |metric| {
8434                assert_eq!(metric.abc.assignments_sum(), 2);
8435            },
8436        );
8437    }
8438
8439    #[cfg(feature = "typescript")]
8440    #[test]
8441    fn tsx_parameter_property_init_not_assignment() {
8442        // Parameter properties contribute no `=`; the body's `let z = 0`
8443        // and the field initializer do.
8444        check_metrics::<TsxParser>(
8445            "class C {
8446                f: number = 0;
8447                constructor(public x: number) { let z = 0; }
8448            }",
8449            "foo.tsx",
8450            |metric| {
8451                assert_eq!(metric.abc.assignments_sum(), 2);
8452                insta::assert_json_snapshot!(metric.abc);
8453            },
8454        );
8455    }
8456
8457    // --- Ruby ABC tests ---------------------------------------------------
8458    //
8459    // Each Ruby `assignment` / `operator_assignment` is one assignment
8460    // regardless of whether the LHS is a local, instance, or class
8461    // variable. Every `call` / `super` / `yield` is one branch. Every
8462    // comparison-operator token inside a `binary` node plus each
8463    // `else` / `elsif` / `when` / `then` / `?` / `rescue` clause is
8464    // one condition.
8465
8466    #[cfg(feature = "ruby")]
8467    #[test]
8468    fn ruby_zero_abc() {
8469        check_metrics::<RubyParser>("\n", "foo.rb", |metric| {
8470            assert_eq!(metric.abc.assignments_sum(), 0);
8471            assert_eq!(metric.abc.branches_sum(), 0);
8472            assert_eq!(metric.abc.conditions_sum(), 0);
8473            insta::assert_json_snapshot!(metric.abc);
8474        });
8475    }
8476
8477    #[cfg(feature = "ruby")]
8478    #[test]
8479    fn ruby_simple_assignment() {
8480        check_metrics::<RubyParser>("def f\n  a = 1\n  b = 2\nend\n", "foo.rb", |metric| {
8481            assert_eq!(metric.abc.assignments_sum(), 2);
8482            assert_eq!(metric.abc.branches_sum(), 0);
8483            assert_eq!(metric.abc.conditions_sum(), 0);
8484            insta::assert_json_snapshot!(metric.abc);
8485        });
8486    }
8487
8488    #[cfg(feature = "ruby")]
8489    #[test]
8490    fn ruby_augmented_assignment() {
8491        // `+=`, `-=`, `*=` are `operator_assignment` nodes — each is
8492        // one assignment. Plain `=` to set the initial value adds one
8493        // more.
8494        check_metrics::<RubyParser>(
8495            "def f(x)\n  a = 0\n  a += x\n  a -= 1\n  a *= 2\nend\n",
8496            "foo.rb",
8497            |metric| {
8498                assert_eq!(metric.abc.assignments_sum(), 4);
8499                insta::assert_json_snapshot!(metric.abc);
8500            },
8501        );
8502    }
8503
8504    #[cfg(feature = "ruby")]
8505    #[test]
8506    fn ruby_logical_augmented_assignment() {
8507        // `||=` and `&&=` are also `operator_assignment` nodes.
8508        check_metrics::<RubyParser>("def f\n  @x ||= 0\n  @x &&= 1\nend\n", "foo.rb", |metric| {
8509            assert_eq!(metric.abc.assignments_sum(), 2);
8510            insta::assert_json_snapshot!(metric.abc);
8511        });
8512    }
8513
8514    #[cfg(feature = "ruby")]
8515    #[test]
8516    fn ruby_method_call_branch() {
8517        // Each method invocation is one branch.
8518        check_metrics::<RubyParser>(
8519            "def f(obj)\n  foo()\n  obj.bar(1)\nend\n",
8520            "foo.rb",
8521            |metric| {
8522                assert_eq!(metric.abc.branches_sum(), 2);
8523                insta::assert_json_snapshot!(metric.abc);
8524            },
8525        );
8526    }
8527
8528    #[cfg(feature = "ruby")]
8529    #[test]
8530    fn ruby_super_and_yield_branches() {
8531        // `super` and `yield` both count as branches (control-pass).
8532        check_metrics::<RubyParser>("def f\n  super\n  yield\nend\n", "foo.rb", |metric| {
8533            assert_eq!(metric.abc.branches_sum(), 2);
8534            assert_eq!(metric.abc.assignments_sum(), 0);
8535            insta::assert_json_snapshot!(metric.abc);
8536        });
8537    }
8538
8539    #[cfg(feature = "ruby")]
8540    #[test]
8541    fn ruby_attr_macro_is_branch() {
8542        // `attr_accessor` is a `Call3` node and registers as a branch
8543        // like any method invocation.
8544        check_metrics::<RubyParser>("class A\n  attr_accessor :x\nend\n", "foo.rb", |metric| {
8545            assert_eq!(metric.abc.branches_sum(), 1);
8546            insta::assert_json_snapshot!(metric.abc);
8547        });
8548    }
8549
8550    #[cfg(feature = "ruby")]
8551    #[test]
8552    fn ruby_comparison_conditions() {
8553        // Each comparison operator is one condition.
8554        check_metrics::<RubyParser>(
8555            "def f(a, b)\n  a == b\n  a != b\n  a < b\n  a > b\n  a <= b\n  a >= b\nend\n",
8556            "foo.rb",
8557            |metric| {
8558                assert_eq!(metric.abc.conditions_sum(), 6);
8559                insta::assert_json_snapshot!(metric.abc);
8560            },
8561        );
8562    }
8563
8564    #[cfg(feature = "ruby")]
8565    #[test]
8566    fn ruby_superclass_clause_is_not_a_condition() {
8567        // Regression for #1280: a superclass clause spells its `<` with the
8568        // same `LT` token as a comparison, but parents it under
8569        // `superclass` rather than `binary`, so the parent gate excludes
8570        // it. Before the gate every subclass declaration scored a phantom
8571        // condition.
8572        // expected: 0 conditions — the file contains no conditional at all;
8573        // the single assignment is `x = 1`.
8574        check_metrics::<RubyParser>(
8575            "class Foo < Bar\n  def plain\n    x = 1\n  end\nend\n",
8576            "foo.rb",
8577            |metric| {
8578                assert_eq!(metric.abc.conditions_sum(), 0);
8579                assert_eq!(metric.abc.assignments_sum(), 1);
8580            },
8581        );
8582    }
8583
8584    #[cfg(feature = "ruby")]
8585    #[test]
8586    fn ruby_operator_method_name_is_not_a_condition() {
8587        // The `<` naming an operator method parents under `operator`, which
8588        // the `binary` gate likewise excludes (#1280). The body carries a
8589        // real comparison so the expected value is not the all-zero default:
8590        // 1 discriminates "only the name token is excluded" from both "the
8591        // gate excludes everything" (0) and "nothing is gated" (2).
8592        // expected: 1 condition — the `@v < other` comparison, not the `def <`.
8593        check_metrics::<RubyParser>("def <(other)\n  @v < other\nend\n", "foo.rb", |metric| {
8594            assert_eq!(metric.abc.conditions_sum(), 1);
8595        });
8596    }
8597
8598    #[cfg(feature = "ruby")]
8599    #[test]
8600    fn ruby_every_comparison_operator_method_name_is_not_a_condition() {
8601        // The sibling half of #1280. `<` is not special: every comparison
8602        // and equality token Ruby lets you `def` parents under `operator`
8603        // in that position, so gating only `LT` / `GT` left `def ==`,
8604        // `def <=`, `def >=`, `def <=>`, `def !=` and `def =~` each
8605        // scoring a phantom condition — measured at 1 apiece for a body
8606        // containing no conditional at all.
8607        // expected: 0 conditions per definition; only the name token is on
8608        // the line, so a single non-zero total localises the regression.
8609        check_metrics::<RubyParser>(
8610            "def ==(o)\n  1\nend\n\
8611             def !=(o)\n  1\nend\n\
8612             def <=(o)\n  1\nend\n\
8613             def >=(o)\n  1\nend\n\
8614             def <=>(o)\n  1\nend\n\
8615             def =~(o)\n  1\nend\n\
8616             def <(o)\n  1\nend\n\
8617             def >(o)\n  1\nend\n",
8618            "foo.rb",
8619            |metric| {
8620                assert_eq!(metric.abc.conditions_sum(), 0);
8621            },
8622        );
8623        // The positive control: the same tokens inside a `binary` are real
8624        // comparisons and still count, so the gate is not blanket
8625        // suppression.
8626        check_metrics::<RubyParser>(
8627            "def cmp(a, b)\n  a == b || a <= b || a <=> b\nend\n",
8628            "foo.rb",
8629            |metric| {
8630                assert_eq!(metric.abc.conditions_sum(), 3);
8631            },
8632        );
8633    }
8634
8635    #[cfg(feature = "ruby")]
8636    #[test]
8637    fn ruby_case_match_in_arms_are_conditions() {
8638        // Regression for #977: each non-wildcard `case … in` arm is one
8639        // ABC condition, matching Python's `case_clause` handling. Using
8640        // literal patterns (no comparison operators) isolates the
8641        // `in_clause` contribution from any operand tokens.
8642        // expected: 2 conditions — one per `in 1` / `in 2` arm.
8643        check_metrics::<RubyParser>(
8644            "def f(x)\n  case x\n  in 1 then :one\n  in 2 then :two\n  end\nend\n",
8645            "foo.rb",
8646            |metric| {
8647                assert_eq!(metric.abc.conditions_sum(), 2);
8648            },
8649        );
8650    }
8651
8652    #[cfg(feature = "ruby")]
8653    #[test]
8654    fn ruby_case_match_guarded_wildcard_is_a_condition() {
8655        // Regression for #977: a guarded wildcard arm `in _ if x` is not a
8656        // bare default and counts as one ABC condition, while the trailing
8657        // bare `in _` adds none.
8658        // expected: 2 conditions — the guarded `in _ if x` arm, plus its
8659        // guard slot. The guard predicate is a bare identifier, so before
8660        // #1454 it contributed nothing and the total was 1; the slot is
8661        // what makes `if x`, `if x.even?` and `if x > 0` agree.
8662        check_metrics::<RubyParser>(
8663            "def f(x)\n  case x\n  in _ if x then :y\n  in _ then :default\n  end\nend\n",
8664            "foo.rb",
8665            |metric| {
8666                assert_eq!(metric.abc.conditions_sum(), 2);
8667            },
8668        );
8669    }
8670
8671    #[cfg(feature = "ruby")]
8672    #[test]
8673    fn ruby_case_match_bare_wildcard_is_not_a_condition() {
8674        // Regression for #977: a `case … in` whose only arm is the bare
8675        // wildcard `in _` (no guard) is the default arm and contributes no
8676        // ABC condition, keeping ABC and cyclomatic in lockstep on the
8677        // same construct.
8678        // expected: 0 conditions.
8679        check_metrics::<RubyParser>(
8680            "def f(x)\n  case x\n  in _ then :default\n  end\nend\n",
8681            "foo.rb",
8682            |metric| {
8683                assert_eq!(metric.abc.conditions_sum(), 0);
8684            },
8685        );
8686    }
8687
8688    #[cfg(feature = "ruby")]
8689    #[test]
8690    fn ruby_bare_predicate_control_flow_counts_one_condition() {
8691        // Regression for #696: idiomatic Ruby bare predicates
8692        // (`if flag` / `while flag` / `unless flag` / `until flag`) each
8693        // count one unary condition, matching Rust / C# / PHP / Python. The
8694        // condition field is read for both block and modifier forms.
8695        //
8696        // expected: 8 conditions — four block forms (`if`/`unless`/`while`/
8697        // `until`) plus the same four as modifiers, one each.
8698        check_metrics::<RubyParser>(
8699            "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",
8700            "foo.rb",
8701            |metric| {
8702                assert_eq!(metric.abc.conditions_sum(), 8);
8703            },
8704        );
8705    }
8706
8707    #[cfg(feature = "ruby")]
8708    #[test]
8709    fn ruby_bare_predicate_does_not_double_count_comparison_or_chain() {
8710        // `if a == b` counts only the `==` comparison (the condition field
8711        // is a `binary` node, adding nothing). `if a && b` counts the two
8712        // chain operands via the `&&` walker, again with the condition-field
8713        // arm adding nothing — so neither shape is double-counted (#696).
8714        //
8715        // expected: 3 — `==` (1) + the `a`,`b` operands of `&&` (2).
8716        check_metrics::<RubyParser>(
8717            "def f(a, b)\n  if a == b\n    x\n  end\n  if a && b\n    y\n  end\nend\n",
8718            "foo.rb",
8719            |metric| {
8720                assert_eq!(metric.abc.conditions_sum(), 3);
8721            },
8722        );
8723    }
8724
8725    #[cfg(feature = "ruby")]
8726    #[test]
8727    fn ruby_spaceship_and_case_equality() {
8728        // `<=>` and `===` are comparison operators (conditions).
8729        check_metrics::<RubyParser>(
8730            "def f(a, b)\n  a <=> b\n  a === b\nend\n",
8731            "foo.rb",
8732            |metric| {
8733                assert_eq!(metric.abc.conditions_sum(), 2);
8734                insta::assert_json_snapshot!(metric.abc);
8735            },
8736        );
8737    }
8738
8739    #[cfg(feature = "ruby")]
8740    #[test]
8741    fn ruby_ternary_condition() {
8742        // The `?` ternary marker is one condition; the inner `==` is
8743        // another.
8744        check_metrics::<RubyParser>("def f(x)\n  x == 0 ? :z : :nz\nend\n", "foo.rb", |metric| {
8745            assert_eq!(metric.abc.conditions_sum(), 2);
8746            insta::assert_json_snapshot!(metric.abc);
8747        });
8748    }
8749
8750    // Issue #1161. Ruby's ternary carried only the `?` token arm, so
8751    // `a ? !b : !c` scored 1 against the 4 that Java, C#, Groovy, the C
8752    // family, the JS family, PHP and Perl all report for the same
8753    // expression (#1102).
8754    //
8755    // Every expectation below is the value its C++ sibling
8756    // (`cpp_ternary_operand_slots_count_as_unary_conditions`) already
8757    // asserts for the same expression, so the two read as one table.
8758    #[cfg(feature = "ruby")]
8759    #[test]
8760    fn ruby_ternary_operand_slots_count_as_unary_conditions() {
8761        // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4.
8762        check_metrics::<RubyParser>("def f\n  x = a ? !b : !c\nend\n", "foo.rb", |metric| {
8763            assert_eq!(metric.abc.conditions_sum(), 4);
8764        });
8765        // No-double-count pin, and the assertion that catches the trap
8766        // this grammar sets: `-b` and `!b` are the SAME node kind
8767        // (`unary:284`), separated only by their operator. Scoring a
8768        // branch only when the peel proves it boolean — a `!` / `not`
8769        // layer, not the `unary` kind — is what keeps this at 2. An
8770        // implementation keying on `Unary` reads 3 here.
8771        // `?` (1) + `>` (1) = 2, unchanged by the fix: the parenthesised
8772        // condition unwraps to a comparison its own token already
8773        // counts, and neither branch is negated.
8774        check_metrics::<RubyParser>("def f\n  x = (a > 0) ? b : -b\nend\n", "foo.rb", |metric| {
8775            assert_eq!(metric.abc.conditions_sum(), 2);
8776        });
8777        // Nested — Ruby needs the inner ternary parenthesised. Outer `?`
8778        // (1) + outer condition `a` (1) + inner `?` (1) + inner
8779        // condition `b` (1) = 4. The outer consequence unwraps to the
8780        // inner `conditional`, which proves nothing boolean and is no
8781        // further paren / `!` layer, so it adds nothing on its own; the
8782        // inner ternary is reached by the walk, not by descent.
8783        check_metrics::<RubyParser>(
8784            "def f\n  x = a ? (b ? c : d) : e\nend\n",
8785            "foo.rb",
8786            |metric| {
8787                assert_eq!(metric.abc.conditions_sum(), 4);
8788            },
8789        );
8790        // A parenthesised condition, pinning the paren unwrap on the
8791        // condition slot: `(a)` is `parenthesized_statements`, which the
8792        // peel must look through.
8793        // `?` (1) + `(a)` (1) + `!b` (1) + `!c` (1) = 4.
8794        check_metrics::<RubyParser>("def f\n  x = (a) ? !b : !c\nend\n", "foo.rb", |metric| {
8795            assert_eq!(metric.abc.conditions_sum(), 4);
8796        });
8797        // A negated condition takes the same fallback through the `!`
8798        // unwrap rather than the paren one. `?` (1) + `!a` (1) = 2.
8799        check_metrics::<RubyParser>("def f\n  x = !a ? b : c\nend\n", "foo.rb", |metric| {
8800            assert_eq!(metric.abc.conditions_sum(), 2);
8801        });
8802    }
8803
8804    // The boolean-context seed must discriminate between the condition
8805    // slot and the two branch slots — not merely exist. None of the
8806    // fixtures above can tell the difference: their branches are either
8807    // `!`-unaries (which set the flag inside the unwrap loop regardless
8808    // of the seed) or kinds the loop breaks on before any terminal test.
8809    // A seed that returned `true` for every slot of a `Conditional`
8810    // leaves all five at their asserted values and fails nothing.
8811    //
8812    // A parenthesised *branch* is the input that separates them: the
8813    // unwrap reaches a bare terminal, so only the seed decides whether
8814    // it counts. `?` (1) + condition `a` (1) = 2 in both directions.
8815    #[cfg(feature = "ruby")]
8816    #[test]
8817    fn ruby_ternary_branch_operands_are_not_double_counted() {
8818        check_metrics::<RubyParser>("def f\n  x = a ? (b) : c\nend\n", "foo.rb", |metric| {
8819            assert_eq!(metric.abc.conditions_sum(), 2);
8820        });
8821        check_metrics::<RubyParser>("def f\n  x = a ? b : (c)\nend\n", "foo.rb", |metric| {
8822            assert_eq!(metric.abc.conditions_sum(), 2);
8823        });
8824        // The same pair with a comment before the operand. Comments are
8825        // tree-sitter `extras`, so they become the branch's previous
8826        // sibling — which is why the seed asks the grammar which child
8827        // is the `condition` field rather than testing that sibling for
8828        // `?` / `:` as the C family does. Under the token form both of
8829        // these read 3.
8830        check_metrics::<RubyParser>(
8831            "def f\n  x = a ?\n    # note\n    (b) : c\nend\n",
8832            "foo.rb",
8833            |metric| {
8834                assert_eq!(metric.abc.conditions_sum(), 2);
8835            },
8836        );
8837        check_metrics::<RubyParser>(
8838            "def f\n  x = a ? b :\n    # note\n    (c)\nend\n",
8839            "foo.rb",
8840            |metric| {
8841                assert_eq!(metric.abc.conditions_sum(), 2);
8842            },
8843        );
8844    }
8845
8846    #[cfg(feature = "ruby")]
8847    #[test]
8848    fn ruby_case_when_arms() {
8849        // Each `when` named clause and the `else` clause count as one
8850        // condition each; the `case` head and the implicit `then`
8851        // wrappers do not.
8852        check_metrics::<RubyParser>(
8853            "def f(x)\n  case x\n  when 1 then 'one'\n  when 2 then 'two'\n  else 'other'\n  end\nend\n",
8854            "foo.rb",
8855            |metric| {
8856                // 2 `when` + 1 `else` = 3 conditions.
8857                assert_eq!(metric.abc.conditions_sum(), 3);
8858                insta::assert_json_snapshot!(metric.abc);
8859            },
8860        );
8861    }
8862
8863    #[cfg(feature = "ruby")]
8864    #[test]
8865    fn ruby_elsif_and_else() {
8866        // `elsif` and `else` named clauses are conditions; their inner
8867        // `then` wrappers are not.
8868        check_metrics::<RubyParser>(
8869            "def f(x)\n  if x > 0\n    1\n  elsif x < 0\n    -1\n  else\n    0\n  end\nend\n",
8870            "foo.rb",
8871            |metric| {
8872                // `>`(1) + `elsif`(1) + `<`(1) + `else`(1) = 4.
8873                assert_eq!(metric.abc.conditions_sum(), 4);
8874                insta::assert_json_snapshot!(metric.abc);
8875            },
8876        );
8877    }
8878
8879    #[cfg(feature = "ruby")]
8880    #[test]
8881    fn ruby_rescue_clause_condition() {
8882        // The `rescue` named clause is one condition; the `rescue`
8883        // keyword token (`Rescue2`) is not counted on its own.
8884        // `do_it` without parens is an `identifier`, not a `call`, so
8885        // it contributes no branch. `handle(e)` is a `call` (1 branch).
8886        check_metrics::<RubyParser>(
8887            "def f\n  begin\n    do_it\n  rescue StandardError => e\n    handle(e)\n  end\nend\n",
8888            "foo.rb",
8889            |metric| {
8890                assert_eq!(metric.abc.conditions_sum(), 1);
8891                assert_eq!(metric.abc.branches_sum(), 1);
8892                insta::assert_json_snapshot!(metric.abc);
8893            },
8894        );
8895    }
8896
8897    #[cfg(feature = "ruby")]
8898    #[test]
8899    fn ruby_class_complex_function() {
8900        // Mixed: assignment(=), branch(call), conditions(`>` and `==`).
8901        check_metrics::<RubyParser>(
8902            "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",
8903            "foo.rb",
8904            |metric| {
8905                assert_eq!(metric.abc.assignments_sum(), 1);
8906                assert_eq!(metric.abc.branches_sum(), 1);
8907                // `>`(1) + `==`(1) = 2 conditions. `if` is not a
8908                // token; `&&` is `AMPAMP` and is not counted (see
8909                // the module-level `Stats` doc-comment for the
8910                // cross-language policy; #395, walker tracked in
8911                // #403).
8912                assert_eq!(metric.abc.conditions_sum(), 2);
8913                insta::assert_json_snapshot!(metric.abc);
8914            },
8915        );
8916    }
8917
8918    #[cfg(feature = "ruby")]
8919    #[test]
8920    fn ruby_unary_conditions_in_chain() {
8921        // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
8922        // `&&` / `||` chain is one condition. `a && b || c` → a, b, c each
8923        // contribute one. Ruby's `if` keyword is not a condition token.
8924        // expected: 3 unary conditions (matches the Java byte-equivalent).
8925        check_metrics::<RubyParser>(
8926            "def f(a, b, c)\n  if a && b || c\n    puts \"x\"\n  end\nend\n",
8927            "foo.rb",
8928            |metric| {
8929                assert_eq!(metric.abc.conditions_sum(), 3);
8930            },
8931        );
8932    }
8933
8934    #[cfg(feature = "ruby")]
8935    #[test]
8936    fn ruby_keyword_and_or_chain_counts_operands() {
8937        // The keyword forms `and` / `or` get the same Rule 9 treatment as
8938        // `&&` / `||`. expected: 3 unary conditions (a, b, c).
8939        check_metrics::<RubyParser>(
8940            "def f(a, b, c)\n  if a and b or c\n    puts \"x\"\n  end\nend\n",
8941            "foo.rb",
8942            |metric| {
8943                assert_eq!(metric.abc.conditions_sum(), 3);
8944            },
8945        );
8946    }
8947
8948    #[cfg(feature = "ruby")]
8949    #[test]
8950    fn ruby_negated_operand_is_unary_condition() {
8951        // A `!`-negated operand unwraps the `unary` node to the inner
8952        // identifier. expected: 2 (`a` and the `!b` operand).
8953        check_metrics::<RubyParser>(
8954            "def f(a, b)\n  if a && !b\n    puts \"x\"\n  end\nend\n",
8955            "foo.rb",
8956            |metric| {
8957                assert_eq!(metric.abc.conditions_sum(), 2);
8958            },
8959        );
8960    }
8961
8962    #[cfg(feature = "ruby")]
8963    #[test]
8964    fn ruby_comparison_operands_add_nothing() {
8965        // Isolation for Rule 9 (issue #557): when the `&&` operands are
8966        // themselves comparisons, the unary-condition walker must add
8967        // nothing on top of the two `>` comparisons already counted as
8968        // conditions — distinguishing the gap (bare boolean operands)
8969        // from ordinary relational conditions. Mirrors the Kotlin and
8970        // Elixir isolation tests. expected: 2 (the two `>` comparisons).
8971        check_metrics::<RubyParser>(
8972            "def f(x, y)\n  if x > 0 && y > 0\n    puts \"x\"\n  end\nend\n",
8973            "foo.rb",
8974            |metric| {
8975                assert_eq!(metric.abc.conditions_sum(), 2);
8976            },
8977        );
8978    }
8979
8980    // The two anchors every #1453 fixture needs, coupled on purpose as
8981    // `assert_kotlin_class_members` couples them for #1421. `subjects` is
8982    // the load-bearing one: the whole distinction is whether a `case`
8983    // carries a `value` field, no node kind spells that, and a fixture
8984    // that grew a subject would score its clauses the pre-#1453 way with
8985    // every other row still satisfied. Each fixture is top-level `def`s,
8986    // so the members are the unit space's.
8987    #[cfg(feature = "ruby")]
8988    fn assert_ruby_case_members(
8989        src: &str,
8990        subjects: usize,
8991        kinds: &[(u16, usize, &str)],
8992        expected: &[(&str, u64, u64)],
8993    ) {
8994        let parser = RubyParser::new(
8995            src.as_bytes().to_vec(),
8996            std::path::Path::new("foo.rb"),
8997            None,
8998        );
8999        let found = parser
9000            .root()
9001            .preorder()
9002            .filter(|n| n.kind_id() == Ruby::Case && n.child_by_field_name("value").is_some())
9003            .count();
9004        assert_eq!(found, subjects, "`case` subjects — the fixture moved");
9005        assert_fixture_spells::<RubyParser>(src, "foo.rb", kinds);
9006        check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
9007            assert_members_score(&space, expected);
9008        });
9009    }
9010
9011    // #1453's headline. A subject-less `when` clause's condition is an
9012    // ordinary boolean expression, so a comparison in it is already an
9013    // ABC condition through the token arm; the clause added a blanket one
9014    // on top, and `gt` scored 3 against its `if` analogue `iff`'s 2.
9015    //
9016    // `bare` and `call` are the controls that isolate the defect: same
9017    // clause shape, no operator for any other arm to count, so the clause
9018    // must still pay for its decision — 2 and 1 before the fix and after.
9019    // A fix that moved them would be attacking the clause count, which
9020    // was never wrong.
9021    //
9022    // `gt`, `bare` and `iff` sit one above cyclomatic's decision count
9023    // because ABC counts the `else` (Fitzpatrick Rule 5) and cyclomatic
9024    // does not; every other row is at `conditions == cyclomatic - 1`.
9025    // `cmt` puts a comment where the subject would go: an extra is not the
9026    // `value` field, so the `case` stays subject-less.
9027    #[cfg(feature = "ruby")]
9028    #[test]
9029    fn ruby_subjectless_case_when_counts_its_condition_once() {
9030        let src = "def gt(x)
9031  case
9032  when x > 5 then 1
9033  else 0
9034  end
9035end
9036def iff(x)
9037  if x > 5 then 1 else 0 end
9038end
9039def two(x)
9040  case
9041  when x > 5 then 1
9042  when x < 0 then 2
9043  end
9044end
9045def ge(x)
9046  case
9047  when x >= 5 then 1
9048  end
9049end
9050def andd(a, b)
9051  case
9052  when a > 1 && b < 2 then 1
9053  end
9054end
9055def orr(a, b)
9056  case
9057  when a || b then 1
9058  end
9059end
9060def kw_or(a, b)
9061  case
9062  when a or b then 1
9063  end
9064end
9065def bare(b)
9066  case
9067  when b then 1
9068  else 0
9069  end
9070end
9071def call(x)
9072  case
9073  when x.even? then 1
9074  end
9075end
9076def cmt(x)
9077  case # no subject
9078  when x > 5 then 1
9079  end
9080end
9081";
9082        assert_ruby_case_members(
9083            src,
9084            0,
9085            &[
9086                (Ruby::When as u16, 10, "`when` clauses"),
9087                (Ruby::GT as u16, 5, "`>` comparisons"),
9088                (Ruby::AMPAMP as u16, 1, "`andd`'s `&&`"),
9089                (Ruby::PIPEPIPE as u16, 1, "`orr`'s `||`"),
9090                (Ruby::Or as u16, 1, "`kw_or`'s `or`"),
9091                (Ruby::Comment as u16, 1, "`cmt`'s comment"),
9092            ],
9093            &[
9094                // (member, abc.conditions, cyclomatic)
9095                // Was 3: the `>` and the clause, for one decision.
9096                ("gt", 2, 2),
9097                ("iff", 2, 2),
9098                // Was 4.
9099                ("two", 2, 3),
9100                // Was 2: `>=` reaches the same token arm.
9101                ("ge", 1, 2),
9102                // Was 3. The compound condition keeps both comparisons —
9103                // suppressing the operators instead would collapse it.
9104                ("andd", 2, 3),
9105                // The Rule 9 walker scores both operands of a bare chain,
9106                // in both spellings; the clause adds nothing on top.
9107                ("orr", 2, 3),
9108                ("kw_or", 2, 3),
9109                ("bare", 2, 2),
9110                ("call", 1, 2),
9111                // Was 2.
9112                ("cmt", 1, 2),
9113            ],
9114        );
9115    }
9116
9117    // Wrappers and the shapes no other arm owns. `not_cmp`, `paren_cmp`,
9118    // `tern` and `test_pat` peel to an expression another arm already
9119    // scored, and each read one above parity before the fix. `paren` and
9120    // `notted` peel to a bare operand nothing else counts, so the clause
9121    // pays exactly as an `if` predicate would.
9122    //
9123    // `scope`, `neg`, `splat` and `arith` are why the clause asks "is this
9124    // already paid for" rather than "is this a known terminal": the `if`
9125    // slot asked the second question and scored each of these 0 until
9126    // #1520 moved it onto the clause's question — see
9127    // `ruby_condition_slot_pays_what_no_other_arm_counted`.
9128    #[cfg(feature = "ruby")]
9129    #[test]
9130    fn ruby_subjectless_case_when_condition_wrappers_count_once() {
9131        let src = "def paren(b)
9132  case
9133  when (b) then 1
9134  end
9135end
9136def notted(b)
9137  case
9138  when !b then 1
9139  end
9140end
9141def not_cmp(x)
9142  case
9143  when not(x > 1) then 1
9144  end
9145end
9146def paren_cmp(x)
9147  case
9148  when (x > 1) then 1
9149  end
9150end
9151def tern(x, a, b)
9152  case
9153  when x ? a : b then 1
9154  end
9155end
9156def test_pat(x)
9157  case
9158  when (x in Integer) then 1
9159  end
9160end
9161def scope
9162  case
9163  when Foo::Bar then 1
9164  end
9165end
9166def neg(x)
9167  case
9168  when -x then 1
9169  end
9170end
9171def splat(xs)
9172  case
9173  when *xs then 1
9174  end
9175end
9176def arith(x)
9177  case
9178  when x + 1 then 1
9179  end
9180end
9181";
9182        assert_ruby_case_members(
9183            src,
9184            0,
9185            &[
9186                (Ruby::When as u16, 10, "`when` clauses"),
9187                (
9188                    Ruby::ParenthesizedStatements as u16,
9189                    4,
9190                    "parenthesised operands",
9191                ),
9192                (Ruby::Conditional as u16, 1, "`tern`'s ternary"),
9193                (Ruby::TestPattern as u16, 1, "`test_pat`'s pattern test"),
9194                (Ruby::ScopeResolution2 as u16, 1, "`scope`'s `Foo::Bar`"),
9195                (Ruby::SplatArgument as u16, 1, "`splat`'s `*xs`"),
9196            ],
9197            &[
9198                ("paren", 1, 2),
9199                ("notted", 1, 2),
9200                // Each was 2.
9201                ("not_cmp", 1, 2),
9202                ("paren_cmp", 1, 2),
9203                ("test_pat", 1, 2),
9204                // Was 3. The ternary's own `?` and its condition `x` are
9205                // its two conditions, as in `if x ? a : b`.
9206                ("tern", 2, 3),
9207                ("scope", 1, 2),
9208                ("neg", 1, 2),
9209                ("splat", 1, 2),
9210                ("arith", 1, 2),
9211            ],
9212        );
9213    }
9214
9215    // A clause may list several patterns: an implicit `||`, so each is
9216    // one operand of that chain and scores once unless its own arm already
9217    // counted it — every row here must equal its `if … || …` twin in the
9218    // `or_*` defs. `bare_first` and `cmp_first` differ only in pattern
9219    // order and must agree; reading the first pattern alone, as Kotlin's
9220    // `kotlin_count_when_entry` does, would score them 2 and 1. None can
9221    // reach `conditions == cyclomatic - 1`: cyclomatic scores the clause
9222    // as one decision, and the `||` twins sit just as far above it.
9223    #[cfg(feature = "ruby")]
9224    #[test]
9225    fn ruby_subjectless_case_when_multiple_patterns() {
9226        let src = "def bare_alts(a, b)
9227  case
9228  when a, b then 1
9229  end
9230end
9231def alts(x)
9232  case
9233  when x > 1, x < -1 then 1
9234  end
9235end
9236def bare_first(a, x)
9237  case
9238  when a, x > 1 then 1
9239  end
9240end
9241def cmp_first(a, x)
9242  case
9243  when x > 1, a then 1
9244  end
9245end
9246def or_bare(a, b)
9247  if a || b then 1 end
9248end
9249def or_cmp(x)
9250  if x > 1 || x < -1 then 1 end
9251end
9252def or_mixed(a, x)
9253  if a || x > 1 then 1 end
9254end
9255";
9256        assert_ruby_case_members(
9257            src,
9258            0,
9259            &[
9260                (Ruby::When as u16, 4, "`when` clauses"),
9261                (Ruby::Pattern as u16, 8, "two patterns per clause"),
9262            ],
9263            &[
9264                // Was 1: one pattern went unscored.
9265                ("bare_alts", 2, 2),
9266                ("or_bare", 2, 3),
9267                // Was 3.
9268                ("alts", 2, 2),
9269                ("or_cmp", 2, 3),
9270                ("bare_first", 2, 2),
9271                ("cmp_first", 2, 2),
9272                ("or_mixed", 2, 3),
9273            ],
9274        );
9275    }
9276
9277    // An `elsif` is Java's `else if`: one condition for the `else`
9278    // (Rule 5) plus an `if` predicate slot. It paid only the `else`, so
9279    // `eb`, `eself`, `eneg` and `eparen` scored 2 where
9280    // `if (a) {} else if (b) {}` scores 3, while `ecmp` matched only
9281    // because its `>` arm paid the predicate. Each row now equals its Java
9282    // twin (`eelse` 4 / 3: Rule 5 counts the `else` arm, cyclomatic
9283    // does not), and `ecmp` / `eand` must not move, or the slot is paying
9284    // on top of the operator arms (#1520).
9285    #[cfg(feature = "ruby")]
9286    #[test]
9287    fn ruby_elsif_predicate_is_a_condition_slot() {
9288        let src = "def eb(a, b)
9289  if a then 1 elsif b then 2 end
9290end
9291def ecmp(a, x)
9292  if a then 1 elsif x > 0 then 2 end
9293end
9294def eself(a)
9295  if a then 1 elsif self then 2 end
9296end
9297def eneg(a, b)
9298  if a then 1 elsif !b then 2 end
9299end
9300def eparen(a, b)
9301  if a then 1 elsif (b) then 2 end
9302end
9303def eand(a, b, c)
9304  if a then 1 elsif b && c then 2 end
9305end
9306def eelse(a, b)
9307  if a then 1 elsif b then 2 else 3 end
9308end
9309";
9310        assert_fixture_spells::<RubyParser>(
9311            src,
9312            "foo.rb",
9313            &[
9314                (Ruby::Elsif as u16, 7, "one `elsif` per member"),
9315                (Ruby::Else as u16, 1, "`eelse`'s `else`"),
9316            ],
9317        );
9318        check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
9319            assert_members_score(
9320                &space,
9321                &[
9322                    // (member, abc.conditions, cyclomatic)
9323                    // Was 2.
9324                    ("eb", 3, 3),
9325                    ("ecmp", 3, 3),
9326                    // Was 2.
9327                    ("eself", 3, 3),
9328                    // Was 2.
9329                    ("eneg", 3, 3),
9330                    // Was 2.
9331                    ("eparen", 3, 3),
9332                    ("eand", 4, 4),
9333                    // Was 3.
9334                    ("eelse", 4, 3),
9335                ],
9336            );
9337        });
9338    }
9339
9340    // #1520: every Ruby condition slot — block and modifier `if` /
9341    // `unless` / `while` / `until`, a `case … in` guard, a ternary
9342    // condition — pays one condition unless another arm already charged
9343    // the same decision. The slot used to pay only for a predicate that
9344    // peeled down to a known boolean terminal, so the first block below
9345    // scored 0 for one decision each, while the same expression as a
9346    // subject-less `when` scored 1 (`ruby_subjectless_case_when_condition_
9347    // wrappers_count_once`). Every row sits at `conditions == cyclomatic
9348    // - 1` except `tern` and `ifelse`, which agree with each other one
9349    // above it: ABC counts the `?` / `else` arm and cyclomatic does not.
9350    // `chain` and `chain_neg` sit level with each other, as a Rule 9
9351    // chain scores one per operand.
9352    //
9353    // The second block must not move, or the slot is paying twice: each
9354    // predicate's comparison or chain operand is already counted by its
9355    // own arm. `begin_cmp` and `assign_cmp` are why the peel looks through
9356    // `begin … end` and an assignment's value — without that the slot
9357    // would see a non-terminal and pay on top of the `>`. `chain_neg`'s
9358    // operands are slots of their own: the Rule 9 walker pays for `-a`
9359    // and `-b` as it does for `a` and `b`, and the `if` adds nothing
9360    // (#1529; before it, the walker skipped both and the `if` paid one).
9361    #[cfg(feature = "ruby")]
9362    #[test]
9363    fn ruby_condition_slot_pays_what_no_other_arm_counted() {
9364        let src = "def scope
9365  if Foo::Bar then 1 end
9366end
9367def zelf
9368  if self then 1 end
9369end
9370def neg(x)
9371  if -x then 1 end
9372end
9373def defd(x)
9374  if defined?(x) then 1 end
9375end
9376def assign(x)
9377  if (y = x) then y end
9378end
9379def arith(x)
9380  if x + 1 then 1 end
9381end
9382def blk(b)
9383  g if begin b end
9384end
9385def unl
9386  g unless Foo::Bar
9387end
9388def til(x)
9389  g until -x
9390end
9391def whl
9392  while self do g end
9393end
9394def guard(x)
9395  case x
9396  in [y] if -y then 1
9397  end
9398end
9399def tern(x)
9400  -x ? 1 : 2
9401end
9402def ifelse(x)
9403  if -x then 1 else 2 end
9404end
9405def cmp(x)
9406  if x > 1 then 1 end
9407end
9408def begin_cmp(x)
9409  if begin x > 1 end then 1 end
9410end
9411def assign_cmp(x)
9412  if (y = x > 1) then y end
9413end
9414def notted(x)
9415  if !x then 1 end
9416end
9417def chain(a, b)
9418  if a && b then 1 end
9419end
9420def chain_neg(a, b)
9421  if -a && -b then 1 end
9422end
9423";
9424        assert_fixture_spells::<RubyParser>(
9425            src,
9426            "foo.rb",
9427            &[
9428                (Ruby::ScopeResolution2 as u16, 2, "`Foo::Bar`"),
9429                (Ruby::Zelf as u16, 2, "`self`"),
9430                (Ruby::Begin as u16, 2, "`begin … end`"),
9431                (Ruby::Assignment as u16, 2, "parenthesised assignments"),
9432                (Ruby::Conditional as u16, 1, "`tern`'s ternary"),
9433                (Ruby::IfGuard as u16, 1, "`guard`'s guard"),
9434            ],
9435        );
9436        check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
9437            assert_members_score(
9438                &space,
9439                &[
9440                    // (member, abc.conditions, cyclomatic); each of the
9441                    // first thirteen was one lower.
9442                    ("scope", 1, 2),
9443                    ("zelf", 1, 2),
9444                    ("neg", 1, 2),
9445                    ("defd", 1, 2),
9446                    ("assign", 1, 2),
9447                    ("arith", 1, 2),
9448                    ("blk", 1, 2),
9449                    ("unl", 1, 2),
9450                    ("til", 1, 2),
9451                    ("whl", 1, 2),
9452                    ("guard", 2, 3),
9453                    ("tern", 2, 2),
9454                    ("ifelse", 2, 2),
9455                    ("cmp", 1, 2),
9456                    ("begin_cmp", 1, 2),
9457                    ("assign_cmp", 1, 2),
9458                    ("notted", 1, 2),
9459                    ("chain", 2, 3),
9460                    // Was 0, then 1 until #1529.
9461                    ("chain_neg", 2, 3),
9462                ],
9463            );
9464        });
9465    }
9466
9467    // The other half of #1453: a subject-ful clause must not move. It
9468    // lists a pattern matched with `pattern === subject`, a comparison
9469    // written nowhere, so the clause itself pays for the decision.
9470    // `subj_cmp` is the subject-ful counterpart of `gt` above and keeps
9471    // the clause's count *and* its `>`: `y > 5` is a value compared
9472    // against `x`, two comparisons for one branch. `in_arms` (the
9473    // `InClause` arm), `unl` and `elsif_` share no code with `When` and
9474    // are here to pin that the fix left them alone.
9475    #[cfg(feature = "ruby")]
9476    #[test]
9477    fn ruby_subjectful_case_when_keeps_the_per_clause_count() {
9478        let src = "def konst(x)
9479  case x
9480  when 1 then 1
9481  when 2 then 2
9482  else 0
9483  end
9484end
9485def subj_alts(x)
9486  case x
9487  when 1, 2 then 1
9488  else 0
9489  end
9490end
9491def subj_cmp(x, y)
9492  case x
9493  when y > 5 then 1
9494  end
9495end
9496def in_arms(x)
9497  case x
9498  in Integer then 1
9499  in String then 2
9500  else 0
9501  end
9502end
9503def unl(x)
9504  unless x > 5 then 1 else 0 end
9505end
9506def elsif_(x)
9507  if x > 5 then 1 elsif x < 0 then 2 else 0 end
9508end
9509";
9510        // Three: `in_arms` is a `case_match`, a separate production.
9511        assert_ruby_case_members(
9512            src,
9513            3,
9514            &[
9515                (Ruby::CaseMatch as u16, 1, "`in_arms`'s `case`"),
9516                (Ruby::When as u16, 4, "`when` clauses"),
9517                (Ruby::InClause as u16, 2, "`in_arms`'s clauses"),
9518                (Ruby::GT as u16, 3, "`>` comparisons"),
9519            ],
9520            &[
9521                ("konst", 3, 3),
9522                ("subj_alts", 2, 2),
9523                ("subj_cmp", 2, 2),
9524                ("in_arms", 3, 3),
9525                ("unl", 2, 2),
9526                ("elsif_", 4, 3),
9527            ],
9528        );
9529    }
9530
9531    // ---------------------------------------------------------------
9532    // Default-impl placeholder smoke tests (audited in #188).
9533    //
9534    // These tests assert that the *current* default-impl languages
9535    // return ABC = 0/0/0 for source that DOES contain branches,
9536    // conditions, and assignments. When the real impl lands for any
9537    // of these languages, the corresponding assertion below will fire
9538    // — the implementer must update the expected values, which is the
9539    // gate. Tag the follow-up issue in each test.
9540    // ---------------------------------------------------------------
9541
9542    // --- Python ABC ---------------------------------------------------
9543
9544    #[cfg(feature = "python")]
9545    #[test]
9546    fn python_empty_module_zero() {
9547        check_metrics::<PythonParser>("", "empty.py", |metric| {
9548            assert_eq!(metric.abc.assignments_sum(), 0);
9549            assert_eq!(metric.abc.branches_sum(), 0);
9550            assert_eq!(metric.abc.conditions_sum(), 0);
9551            insta::assert_json_snapshot!(metric.abc);
9552        });
9553    }
9554
9555    #[cfg(feature = "python")]
9556    #[test]
9557    fn python_plain_assignments_count() {
9558        // Three plain `=` assignments → A=3. No branches, no conditions.
9559        check_metrics::<PythonParser>("x = 1\ny = 2\nz = x\n", "foo.py", |metric| {
9560            assert_eq!(metric.abc.assignments_sum(), 3);
9561            assert_eq!(metric.abc.branches_sum(), 0);
9562            assert_eq!(metric.abc.conditions_sum(), 0);
9563            insta::assert_json_snapshot!(metric.abc);
9564        });
9565    }
9566
9567    #[cfg(feature = "python")]
9568    #[test]
9569    fn python_typed_assignment_counts_bare_annotation_does_not() {
9570        // `x: int = 1` carries an `=`, so it counts.
9571        // `y: int` is a bare annotation (no `=`) — declares a type but
9572        // binds nothing; it must NOT inflate the assignment count.
9573        check_metrics::<PythonParser>("x: int = 1\ny: int\n", "foo.py", |metric| {
9574            assert_eq!(metric.abc.assignments_sum(), 1);
9575            insta::assert_json_snapshot!(metric.abc);
9576        });
9577    }
9578
9579    #[cfg(feature = "python")]
9580    #[test]
9581    fn python_augmented_assignments_count() {
9582        // Each augmented op counts once.
9583        check_metrics::<PythonParser>("x = 0\nx += 1\nx -= 1\nx *= 2\n", "foo.py", |metric| {
9584            // 1 plain `=` + 3 augmented = 4 assignments.
9585            assert_eq!(metric.abc.assignments_sum(), 4);
9586            insta::assert_json_snapshot!(metric.abc);
9587        });
9588    }
9589
9590    #[cfg(feature = "python")]
9591    #[test]
9592    fn python_walrus_counts_as_assignment() {
9593        // `x := 10` is a `NamedExpression` (PEP 572). It binds a value
9594        // → one assignment under Fitzpatrick's rule.
9595        check_metrics::<PythonParser>("if (n := 10) > 5:\n    pass\n", "foo.py", |metric| {
9596            // 1 assignment (walrus) + 1 condition (`> 5` is a
9597            // ComparisonOperator).
9598            assert_eq!(metric.abc.assignments_sum(), 1);
9599            assert_eq!(metric.abc.conditions_sum(), 1);
9600            insta::assert_json_snapshot!(metric.abc);
9601        });
9602    }
9603
9604    #[cfg(feature = "python")]
9605    #[test]
9606    fn python_calls_are_branches() {
9607        // `foo()`, `bar()`, `Baz()` (constructor) all parse as `Call`
9608        // → three branches.
9609        check_metrics::<PythonParser>(
9610            "def foo():\n    pass\ndef bar():\n    pass\nclass Baz:\n    pass\nfoo()\nbar()\nBaz()\n",
9611            "foo.py",
9612            |metric| {
9613                assert_eq!(metric.abc.branches_sum(), 3);
9614                assert_eq!(metric.abc.assignments_sum(), 0);
9615                insta::assert_json_snapshot!(metric.abc);
9616            },
9617        );
9618    }
9619
9620    #[cfg(feature = "python")]
9621    #[test]
9622    fn python_comparisons_count_conditions() {
9623        // `x > 0`, `x == y`, `x is None` are each a single
9624        // `ComparisonOperator` node — three conditions.
9625        check_metrics::<PythonParser>(
9626            "def f(x, y):\n    a = x > 0\n    b = x == y\n    c = x is None\n",
9627            "foo.py",
9628            |metric| {
9629                assert_eq!(metric.abc.conditions_sum(), 3);
9630                // 3 plain assignments; the comparisons are operands.
9631                assert_eq!(metric.abc.assignments_sum(), 3);
9632                insta::assert_json_snapshot!(metric.abc);
9633            },
9634        );
9635    }
9636
9637    #[cfg(feature = "python")]
9638    #[test]
9639    fn python_chained_comparison_counts_once() {
9640        // tree-sitter-python collapses `0 < x < 10` into a single
9641        // `ComparisonOperator` — one condition, not two.
9642        check_metrics::<PythonParser>("def f(x):\n    return 0 < x < 10\n", "foo.py", |metric| {
9643            assert_eq!(metric.abc.conditions_sum(), 1);
9644            insta::assert_json_snapshot!(metric.abc);
9645        });
9646    }
9647
9648    #[cfg(feature = "python")]
9649    #[test]
9650    fn python_number_truthy_condition_counts() {
9651        // Regression for #772: Python treats every non-zero number as
9652        // truthy, so `if 5:` and `x and 5` should each count their
9653        // numeric literal as a Fitzpatrick unary condition. Pre-fix
9654        // the former `python_bool_terminal_kinds!()` listed `True` /
9655        // `False` but omitted `Integer` / `Float`, so the walker dropped every
9656        // numeric-truthy operand (mirrors the Lua `Number` fix).
9657        check_metrics::<PythonParser>(
9658            "def f(a):\n    if 5:\n        pass\n    return a and 2\n",
9659            "foo.py",
9660            |metric| {
9661                // `if 5:` → walker counts the Integer literal (+1).
9662                // `a and 2` → `and` walker counts both operands:
9663                //   identifier `a` (+1), Integer `2` (+1).
9664                // Total: 3.
9665                assert_eq!(metric.abc.conditions_sum(), 3);
9666                insta::assert_json_snapshot!(metric.abc);
9667            },
9668        );
9669    }
9670
9671    #[cfg(feature = "python")]
9672    #[test]
9673    fn python_boolean_operators_not_counted_directly() {
9674        // Python's `and` / `or` are not counted as conditions on
9675        // their own (Fitzpatrick Rule 5; #395). Each operand is
9676        // instead counted as a unary conditional by the walker
9677        // (Rule 9; #403). `if a and b or c:` parses left-to-right
9678        // with `or` lower precedence: `(a and b) or c`. Walker
9679        // tallies: inner `and` counts `a`, `b` (+2); outer `or`
9680        // counts only the new outer operand `c` (+1; the inner
9681        // `(a and b)` BooleanOperator is not a terminal). Total
9682        // C = 3.
9683        check_metrics::<PythonParser>(
9684            "def f(a, b, c):\n    if a and b or c:\n        pass\n",
9685            "foo.py",
9686            |metric| {
9687                assert_eq!(metric.abc.conditions_sum(), 3);
9688                insta::assert_json_snapshot!(metric.abc);
9689            },
9690        );
9691    }
9692
9693    /// Python's unary `not` operator parses as `NotOperator` and now
9694    /// counts as one condition, matching Java's `!x` rule. Closes
9695    /// the parity gap noted in #214: without this, `if not flag:`
9696    /// reported 0 conditions while the Java equivalent reports 1.
9697    #[cfg(feature = "python")]
9698    #[test]
9699    fn python_unary_not_counts_as_condition() {
9700        check_metrics::<PythonParser>(
9701            "def f(flag):\n    if not flag:\n        return 1\n    return 0\n",
9702            "foo.py",
9703            |metric| {
9704                // One `NotOperator` -> 1 condition. The `if` itself
9705                // is structural and doesn't add an Abc condition.
9706                assert_eq!(metric.abc.conditions_sum(), 1);
9707                insta::assert_json_snapshot!(metric.abc);
9708            },
9709        );
9710    }
9711
9712    /// `return not flag` — the unary `not` is the entire return
9713    /// expression. Without `NotOperator` counted, this reports zero
9714    /// conditions; with it, one. Java's `return !flag;` is one.
9715    #[cfg(feature = "python")]
9716    #[test]
9717    fn python_return_unary_not_counts() {
9718        check_metrics::<PythonParser>("def f(flag):\n    return not flag\n", "foo.py", |metric| {
9719            assert_eq!(metric.abc.conditions_sum(), 1);
9720            insta::assert_json_snapshot!(metric.abc);
9721        });
9722    }
9723
9724    /// `foo(not ready, value)` — the unary `not` inside an argument
9725    /// list still contributes. Mirrors Java's `java_count_negated`
9726    /// walk over argument lists.
9727    #[cfg(feature = "python")]
9728    #[test]
9729    fn python_unary_not_in_argument_list_counts() {
9730        check_metrics::<PythonParser>(
9731            "def f(ready, value):\n    log(not ready, value)\n",
9732            "foo.py",
9733            |metric| {
9734                // 1 Call (log) -> 1 branch.
9735                // 1 NotOperator (not ready) -> 1 condition.
9736                assert_eq!(metric.abc.branches_sum(), 1);
9737                assert_eq!(metric.abc.conditions_sum(), 1);
9738                insta::assert_json_snapshot!(metric.abc);
9739            },
9740        );
9741    }
9742
9743    /// A negated comparison is one condition. `not (x > 0)` parses as
9744    /// `NotOperator(ParenthesizedExpression(ComparisonOperator))`; the
9745    /// comparison's arm pays and the `not` adds nothing, as Java's
9746    /// `!(x > 0)` scores 1 (#1526). This test asserted 2 on the false
9747    /// premise that Java scores 2.
9748    #[cfg(feature = "python")]
9749    #[test]
9750    fn python_unary_not_with_comparison_counts_each_once() {
9751        check_metrics::<PythonParser>(
9752            "def f(x):\n    if not (x > 0):\n        return 1\n    return 0\n",
9753            "foo.py",
9754            |metric| {
9755                // ComparisonOperator (1); the `not` negates a counted
9756                // comparison and adds nothing.
9757                assert_eq!(metric.abc.conditions_sum(), 1);
9758                insta::assert_json_snapshot!(metric.abc);
9759            },
9760        );
9761    }
9762
9763    /// `not x and y` parses as `BooleanOperator(NotOperator(x), and,
9764    /// y)`. The `and` itself is NOT counted (Fitzpatrick Rule 5
9765    /// lists only comparison operators); the `NotOperator` is
9766    /// counted at the top level (Rule 7); and the `y` operand is
9767    /// counted by the Rule 9 walker (issue #403). Total: 2.
9768    /// `NotOperator` is intentionally not walked-into a second
9769    /// time — the walker skips it to avoid double-counting.
9770    #[cfg(feature = "python")]
9771    #[test]
9772    fn python_unary_not_with_boolean_combinator_counts_each() {
9773        check_metrics::<PythonParser>(
9774            "def f(x, y):\n    if not x and y:\n        return 1\n    return 0\n",
9775            "foo.py",
9776            |metric| {
9777                // NotOperator (1) + walker on `and` finds `y` (1) = 2.
9778                assert_eq!(metric.abc.conditions_sum(), 2);
9779                insta::assert_json_snapshot!(metric.abc);
9780            },
9781        );
9782    }
9783
9784    #[cfg(feature = "python")]
9785    #[test]
9786    fn python_control_flow_arms_count_conditions() {
9787        // `elif`, `else`, `except`, `finally`, `case` each contribute
9788        // one condition. The comparisons in the `if`/`elif`/`while`
9789        // headers contribute their own ComparisonOperator counts.
9790        check_metrics::<PythonParser>(
9791            "def f(x):\n    if x > 0:\n        a = 1\n    elif x > -1:\n        a = 2\n    else:\n        a = 3\n",
9792            "foo.py",
9793            |metric| {
9794                // 2 ComparisonOperator (`x > 0`, `x > -1`) + 1
9795                // ElifClause + 1 ElseClause = 4 conditions.
9796                assert_eq!(metric.abc.conditions_sum(), 4);
9797                insta::assert_json_snapshot!(metric.abc);
9798            },
9799        );
9800    }
9801
9802    #[cfg(feature = "python")]
9803    #[test]
9804    fn python_ternary_counts_as_condition() {
9805        // `a if c else b` is `ConditionalExpression` → 1 condition.
9806        // `c > 0` adds 1 more (ComparisonOperator).
9807        check_metrics::<PythonParser>(
9808            "def f(c):\n    return 1 if c > 0 else 0\n",
9809            "foo.py",
9810            |metric| {
9811                assert_eq!(metric.abc.conditions_sum(), 2);
9812                insta::assert_json_snapshot!(metric.abc);
9813            },
9814        );
9815    }
9816
9817    // Issue #1161. Python counted the `conditional_expression` node but
9818    // never its condition slot, so `a if c() else b` reported 1 where
9819    // the equivalent `c() ? a : b` reports 2 everywhere else — and
9820    // the former `python_inspect_container`'s `ConditionalExpression`
9821    // boolean-context seed was unreachable, no call site having passed
9822    // that parent.
9823    #[cfg(feature = "python")]
9824    #[test]
9825    fn python_ternary_condition_slot_counts_as_a_unary_condition() {
9826        // ternary (1) + condition `c()` (1) = 2. `c()` is a `Call`, a
9827        // boolean terminal; it also adds one *branch*, not a condition.
9828        check_metrics::<PythonParser>(
9829            "def f(a, b, c):\n    return a if c() else b\n",
9830            "foo.py",
9831            |metric| {
9832                assert_eq!(metric.abc.conditions_sum(), 2);
9833            },
9834        );
9835        // A parenthesised condition: `(c)` is a `parenthesized_expression`,
9836        // so only the peel (`python_wrapper_operand`) can resolve it to
9837        // `c`, which the slot then pays for.
9838        // ternary (1) + `(c)` (1) = 2.
9839        check_metrics::<PythonParser>(
9840            "def f(a, b, c):\n    return a if (c) else b\n",
9841            "foo.py",
9842            |metric| {
9843                assert_eq!(metric.abc.conditions_sum(), 2);
9844            },
9845        );
9846        // A negated condition is *not* counted by the new slot: it is a
9847        // `NotOperator`, which already has its own top-level dispatcher
9848        // arm. ternary (1) + `not c` (1) = 2, not 3.
9849        check_metrics::<PythonParser>(
9850            "def f(a, b, c):\n    return a if not c else b\n",
9851            "foo.py",
9852            |metric| {
9853                assert_eq!(metric.abc.conditions_sum(), 2);
9854            },
9855        );
9856        // The cross-language reference case. `(not b) if a else (not c)`
9857        // is the exact semantic equivalent of `a ? !b : !c`, which every
9858        // other language reports as 4: ternary (1) + condition `a` (1) +
9859        // two `NotOperator`s (2). The negated operands come from
9860        // Python's own arm, the condition from the slot added here.
9861        //
9862        // #1161's resolution plan predicted this would stay 3, having
9863        // measured the condition slot's contribution against the pre-fix
9864        // total. 3 would have left Python disagreeing with every other
9865        // language on the reference expression — the gap the issue was
9866        // filed about.
9867        check_metrics::<PythonParser>(
9868            "def f(a, b, c):\n    return (not b) if a else (not c)\n",
9869            "foo.py",
9870            |metric| {
9871                assert_eq!(metric.abc.conditions_sum(), 4);
9872            },
9873        );
9874    }
9875
9876    // The double-count pin, and the reason Python gets a condition-slot
9877    // helper rather than a copy of `cpp_walk_ternary`: Python's branch
9878    // operands are counted by the top-level `NotOperator` /
9879    // `ComparisonOperator` arms, a different mechanism from every other
9880    // language's walker. Routing the branch slots through a negated-
9881    // operand walk as the C family does (`python_inspect_container`
9882    // before #1526) would have counted a parenthesised operand that
9883    // the identical unparenthesised expression scores at zero.
9884    //
9885    // Both fixtures below are 2 today and were 4 under such a copy, so a
9886    // later "make Python consistent with the others" change cannot land
9887    // silently.
9888    #[cfg(feature = "python")]
9889    #[test]
9890    fn python_ternary_branch_operands_are_not_double_counted() {
9891        // ternary (1) + condition `a` (1) = 2. The two parenthesised
9892        // operands add nothing — an unnegated branch is type-free.
9893        check_metrics::<PythonParser>(
9894            "def f(a, b, c):\n    return (b) if a else (c)\n",
9895            "foo.py",
9896            |metric| {
9897                assert_eq!(metric.abc.conditions_sum(), 2);
9898            },
9899        );
9900        // The unparenthesised form must agree: nothing about `(b)`
9901        // versus `b` is a condition.
9902        check_metrics::<PythonParser>(
9903            "def f(a, b, c):\n    return b if a else c\n",
9904            "foo.py",
9905            |metric| {
9906                assert_eq!(metric.abc.conditions_sum(), 2);
9907            },
9908        );
9909    }
9910
9911    // Comments are tree-sitter `extras`, so they arrive as direct
9912    // children of `conditional_expression` and shift every positional
9913    // index after them. `python_count_ternary_condition` therefore
9914    // anchors on the `if` keyword and skips comments after it; both
9915    // halves are needed and each fixture below fails without one.
9916    #[cfg(feature = "python")]
9917    #[test]
9918    fn python_ternary_condition_survives_an_interposed_comment() {
9919        // Comment before the keyword: `child(2)` is the `if` token here,
9920        // so a positional lookup reads 1. ternary (1) + `f()` (1) = 2.
9921        check_metrics::<PythonParser>(
9922            "def f(b, c):\n    return (b\n            # why\n            if f() else c)\n",
9923            "foo.py",
9924            |metric| {
9925                assert_eq!(metric.abc.conditions_sum(), 2);
9926            },
9927        );
9928        // Comment after the keyword: the child immediately following
9929        // `if` is the comment, so taking the first rather than the first
9930        // non-comment reads 1. ternary (1) + `f()` (1) = 2.
9931        check_metrics::<PythonParser>(
9932            "def f(b, c):\n    return (b if\n            # why\n            f() else c)\n",
9933            "foo.py",
9934            |metric| {
9935                assert_eq!(metric.abc.conditions_sum(), 2);
9936            },
9937        );
9938    }
9939
9940    #[cfg(feature = "python")]
9941    #[test]
9942    fn python_try_except_finally_count_conditions() {
9943        // ExceptClause + FinallyClause → 2 conditions.
9944        check_metrics::<PythonParser>(
9945            "def f():\n    try:\n        pass\n    except ValueError:\n        pass\n    finally:\n        pass\n",
9946            "foo.py",
9947            |metric| {
9948                assert_eq!(metric.abc.conditions_sum(), 2);
9949                insta::assert_json_snapshot!(metric.abc);
9950            },
9951        );
9952    }
9953
9954    #[cfg(feature = "python")]
9955    #[test]
9956    fn python_match_case_counts_conditions() {
9957        // Each non-wildcard `CaseClause` → 1 condition. The bare
9958        // `case _:` arm is the language-neutral `default:` equivalent
9959        // and is excluded (matches Rust's bare-`_` MatchArm filter and
9960        // Java/C#'s `default:` rule). Source has `case 1:` (counts) +
9961        // `case _:` (excluded) → C = 1.
9962        check_metrics::<PythonParser>(
9963            "def f(x):\n    match x:\n        case 1:\n            pass\n        case _:\n            pass\n",
9964            "foo.py",
9965            |metric| {
9966                assert_eq!(metric.abc.conditions_sum(), 1);
9967                insta::assert_json_snapshot!(metric.abc);
9968            },
9969        );
9970    }
9971
9972    #[cfg(feature = "python")]
9973    #[test]
9974    fn python_match_case_guarded_wildcard_counts() {
9975        // `case _ if g:` is NOT a bare wildcard — the guard
9976        // contributes real branching, so the arm counts as a
9977        // condition. Mirrors Rust's `_ if g => ...` behavior.
9978        // Source: `case 1:` (counts) + `case _ if x > 0:` (guarded
9979        // wildcard, counts) + `case _:` (bare wildcard, excluded) →
9980        // C from CaseClause = 2; the guard's `x > 0` adds one
9981        // ComparisonOperator → total C = 3.
9982        check_metrics::<PythonParser>(
9983            "def f(x):\n    match x:\n        case 1:\n            pass\n        case _ if x > 0:\n            pass\n        case _:\n            pass\n",
9984            "foo.py",
9985            |metric| {
9986                assert_eq!(metric.abc.conditions_sum(), 3);
9987                insta::assert_json_snapshot!(metric.abc);
9988            },
9989        );
9990    }
9991
9992    #[cfg(feature = "python")]
9993    #[test]
9994    fn python_complex_function_abc() {
9995        // Mixed-shape regression: assignments, calls, conditions all in
9996        // a single function.
9997        check_metrics::<PythonParser>(
9998            "def f(items, threshold):\n\
9999             \x20   result = []\n\
10000             \x20   for item in items:\n\
10001             \x20       if item > threshold:\n\
10002             \x20           result.append(item)\n\
10003             \x20   return result\n",
10004            "foo.py",
10005            |metric| {
10006                // assignments: `result = []` → 1
10007                // branches: `result.append(item)` is one call → 1
10008                // conditions: `item > threshold` is one
10009                // ComparisonOperator → 1
10010                assert_eq!(metric.abc.assignments_sum(), 1);
10011                assert_eq!(metric.abc.branches_sum(), 1);
10012                assert_eq!(metric.abc.conditions_sum(), 1);
10013                insta::assert_json_snapshot!(metric.abc);
10014            },
10015        );
10016    }
10017
10018    #[cfg(feature = "python")]
10019    #[test]
10020    fn python_if_multiple_conditions() {
10021        // Fitzpatrick Rule 9 walker on `and` / `or` (issue #403).
10022        //   - `if a or b or c or d:` → 4 (each operand counted once)
10023        //   - `if a and b and c:`    → 3
10024        //   - `if not a and not b:`  → 2 (two `NotOperator`s counted
10025        //     by the top-level dispatcher arm; the walker SKIPS
10026        //     `NotOperator` children to avoid double-counting)
10027        // Total: 4 + 3 + 2 = 9.
10028        check_metrics::<PythonParser>(
10029            "def f(a, b, c, d):\n\
10030             \x20   if a or b or c or d:           # +4c\n\
10031             \x20       pass\n\
10032             \x20   if a and b and c:              # +3c\n\
10033             \x20       pass\n\
10034             \x20   if not a and not b:            # +2c (NotOperator x2)\n\
10035             \x20       pass\n",
10036            "foo.py",
10037            |metric| {
10038                assert_eq!(metric.abc.conditions_sum(), 9);
10039                insta::assert_json_snapshot!(metric.abc);
10040            },
10041        );
10042    }
10043
10044    #[cfg(feature = "python")]
10045    #[test]
10046    fn python_while_conditions() {
10047        // Python has no `do { ... } while(cond);` construct, so this
10048        // mirrors only the `while` half of the Java suite. The
10049        // walker fires on each `and` / `or` token inside the loop
10050        // header.
10051        check_metrics::<PythonParser>(
10052            "def f(a, b):\n\
10053             \x20   while a or b:                  # +2c\n\
10054             \x20       break\n\
10055             \x20   while a and not b:             # +2c (a + NotOperator)\n\
10056             \x20       break\n",
10057            "foo.py",
10058            |metric| {
10059                assert_eq!(metric.abc.conditions_sum(), 4);
10060                insta::assert_json_snapshot!(metric.abc);
10061            },
10062        );
10063    }
10064
10065    #[cfg(feature = "python")]
10066    #[test]
10067    fn python_short_circuit_with_boolean_literal_operand() {
10068        // `a and True` reports 2 conditions: one identifier, one
10069        // True literal. Confirms `True` / `False` are in the walker
10070        // terminal set.
10071        check_metrics::<PythonParser>("def f(a):\n    return a and True\n", "foo.py", |metric| {
10072            assert_eq!(metric.abc.conditions_sum(), 2);
10073            insta::assert_json_snapshot!(metric.abc);
10074        });
10075    }
10076
10077    #[cfg(feature = "python")]
10078    #[test]
10079    fn python_await_expression_condition_counts() {
10080        // Regression for findings.md round-2 #2 (Python):
10081        // `if await ready(): pass` parses with `await` as the
10082        // condition node. Adding `Python::Await` to the
10083        // terminal-bool set mirrors the C# reference (lesson 19).
10084        check_metrics::<PythonParser>(
10085            "async def ready(): return True\n\
10086             async def f():\n    if await ready(): pass\n",
10087            "foo.py",
10088            |metric| {
10089                // ready() is a call (1 branch); await is the
10090                // condition (1).
10091                assert_eq!(metric.abc.branches_sum(), 1);
10092                assert_eq!(metric.abc.conditions_sum(), 1);
10093                insta::assert_json_snapshot!(metric.abc);
10094            },
10095        );
10096    }
10097
10098    #[cfg(feature = "python")]
10099    #[test]
10100    fn python_if_call_terminal_condition_counts_once() {
10101        // Pins the Phase-2B behaviour for Python's `Call` terminal-bool
10102        // kind: `if foo():` is a Fitzpatrick Rule 6 unary conditional
10103        // (a bare boolean-evaluating call as the if-condition). The
10104        // walker's terminal-at-top check fires once per call-condition;
10105        // the call itself separately contributes 1 branch. Surfaced
10106        // (and verified intentional) by the code-review pass on
10107        // Phase 2B.
10108        check_metrics::<PythonParser>("def f():\n    if foo(): pass\n", "foo.py", |metric| {
10109            assert_eq!(metric.abc.branches_sum(), 1);
10110            assert_eq!(metric.abc.conditions_sum(), 1);
10111            insta::assert_json_snapshot!(metric.abc);
10112        });
10113    }
10114
10115    #[cfg(feature = "python")]
10116    #[test]
10117    fn python_if_boolean_literal_condition() {
10118        // Phase 2B (issue #403): bare-boolean conditions count once.
10119        // Python has no paren wrap around if-conditions, so the
10120        // condition node is checked directly. The existing
10121        // NotOperator / ComparisonOperator arms continue to fire
10122        // for those shapes; only the bare-terminal cases (Identifier,
10123        // True, False, etc.) are added by the new arm.
10124        check_metrics::<PythonParser>(
10125            "def f(a):\n\
10126             \x20   if True: pass        # +1c\n\
10127             \x20   if False: pass       # +1c\n\
10128             \x20   while True: break    # +1c\n\
10129             \x20   if a: pass           # +1c (Rule 6 — bare identifier as condition)\n",
10130            "foo.py",
10131            |metric| {
10132                assert_eq!(metric.abc.conditions_sum(), 4);
10133                insta::assert_json_snapshot!(metric.abc);
10134            },
10135        );
10136    }
10137
10138    #[cfg(feature = "python")]
10139    #[test]
10140    fn python_methods_arguments_with_conditions() {
10141        // `m(not a, not b)` reports 2 conditions — both `NotOperator`
10142        // nodes are counted by Python's pre-existing top-level
10143        // NotOperator dispatcher arm. The argument-list walker does
10144        // not need a separate Python arm.
10145        check_metrics::<PythonParser>(
10146            "def f(a, b):\n\
10147             \x20   m(a, b)             # +1b\n\
10148             \x20   m(not a, not b)     # +1b +2c\n",
10149            "foo.py",
10150            |metric| {
10151                assert_eq!(metric.abc.branches_sum(), 2);
10152                assert_eq!(metric.abc.conditions_sum(), 2);
10153                insta::assert_json_snapshot!(metric.abc);
10154            },
10155        );
10156    }
10157
10158    #[cfg(feature = "python")]
10159    #[test]
10160    fn python_return_with_conditions() {
10161        // Phase 2B (issue #403). Python uses the pre-existing top-
10162        // level NotOperator / ComparisonOperator arms for return
10163        // expressions; no dedicated ReturnStatement walker arm is
10164        // needed.
10165        check_metrics::<PythonParser>(
10166            "def m1(z): return not (z >= 0)\n\
10167             def m2(x): return (((not x)))\n\
10168             def m3(x, y): return x and y\n",
10169            "foo.py",
10170            |metric| {
10171                // m1: ComparisonOperator (1); the `not` adds nothing,
10172                //     as Java's `return !(z >= 0)` scores 1 (#1526).
10173                // m2: NotOperator (1).
10174                // m3: walker on `and` counts both operands = 2.
10175                // Sum: 4.
10176                assert_eq!(metric.abc.conditions_sum(), 4);
10177                insta::assert_json_snapshot!(metric.abc);
10178            },
10179        );
10180    }
10181
10182    #[cfg(feature = "rust")]
10183    #[test]
10184    fn rust_empty_unit_zero() {
10185        // No code at all → A=B=C=0. Establishes the trait is wired up
10186        // and the per-language compute is reachable.
10187        check_metrics::<RustParser>("", "empty.rs", |metric| {
10188            assert_eq!(metric.abc.assignments_sum(), 0);
10189            assert_eq!(metric.abc.branches_sum(), 0);
10190            assert_eq!(metric.abc.conditions_sum(), 0);
10191            insta::assert_json_snapshot!(metric.abc);
10192        });
10193    }
10194
10195    #[cfg(feature = "rust")]
10196    #[test]
10197    fn rust_assignments_let_init_plain_and_compound() {
10198        // `let mut x = 0` is a `let_declaration` carrying an `=`
10199        // initializer → counts as 1 (matches Fitzpatrick's literal
10200        // "every `=` is an assignment" rule and the JS impl's
10201        // treatment of `let x = 5`). `x = 5` and `x = 7` are plain
10202        // `=` assignments → 2. `x += 2` is a compound assignment → 1.
10203        // Total A = 4.
10204        check_metrics::<RustParser>(
10205            "fn f() { let mut x = 0; x = 5; x += 2; x = 7; }",
10206            "foo.rs",
10207            |metric| {
10208                assert_eq!(metric.abc.assignments_sum(), 4);
10209                assert_eq!(metric.abc.branches_sum(), 0);
10210                assert_eq!(metric.abc.conditions_sum(), 0);
10211                insta::assert_json_snapshot!(metric.abc);
10212            },
10213        );
10214    }
10215
10216    #[cfg(feature = "rust")]
10217    #[test]
10218    fn rust_let_without_initializer_does_not_count() {
10219        // `let a;` is a `let_declaration` with NO `=` and no `value`
10220        // field — the binding is uninitialised. The arm only fires
10221        // when `value` is present, so this contributes zero to A.
10222        // `let _b;` is the same shape (the `_` pattern is still a
10223        // pattern, not a wildcard suppression of the binding).
10224        // Regression test for issue #393: only `=` counts, not the
10225        // bare declaration.
10226        check_metrics::<RustParser>(
10227            "fn f() { let a: i32; let _b: i32; a = 5; }",
10228            "foo.rs",
10229            |metric| {
10230                // Only `a = 5` (assignment_expression) → A = 1.
10231                assert_eq!(metric.abc.assignments_sum(), 1);
10232                insta::assert_json_snapshot!(metric.abc);
10233            },
10234        );
10235    }
10236
10237    #[cfg(feature = "rust")]
10238    #[test]
10239    fn rust_let_initializers_immutable_and_mutable_count() {
10240        // Issue #393: `let a = 1;`, `let b = 2;`, `let c = a + b;`,
10241        // `let mut d = 0;` are all `let_declaration` nodes carrying
10242        // an `=` initializer — each counts as 1 (Option B in the
10243        // issue body: literal Fitzpatrick, both `let` and `let mut`
10244        // count). `d = 5;` is one plain assignment_expression, `d
10245        // += 1;` is one compound. Total A = 4 + 1 + 1 = 6.
10246        check_metrics::<RustParser>(
10247            "fn f() { let a=1; let b=2; let c=a+b; let mut d=0; d=5; d+=1; }",
10248            "foo.rs",
10249            |metric| {
10250                assert_eq!(metric.abc.assignments_sum(), 6);
10251                insta::assert_json_snapshot!(metric.abc);
10252            },
10253        );
10254    }
10255
10256    #[cfg(feature = "rust")]
10257    #[test]
10258    fn rust_calls_are_branches() {
10259        // Free function call + method call (parses as call_expression
10260        // with a field_expression callee) + associated-fn call. All
10261        // three are `call_expression` → B = 3. Macro invocations like
10262        // `println!` parse as `macro_invocation`, NOT `call_expression`,
10263        // so they are not branches.
10264        check_metrics::<RustParser>(
10265            "fn f() { g(); 1.to_string(); String::new(); }\nfn g() {}\n",
10266            "foo.rs",
10267            |metric| {
10268                assert_eq!(metric.abc.branches_sum(), 3);
10269                assert_eq!(metric.abc.assignments_sum(), 0);
10270                assert_eq!(metric.abc.conditions_sum(), 0);
10271                insta::assert_json_snapshot!(metric.abc);
10272            },
10273        );
10274    }
10275
10276    #[cfg(feature = "rust")]
10277    #[test]
10278    fn rust_try_operator_is_branch() {
10279        // `?` parses as `try_expression` and counts as one branch
10280        // (short-circuit return on Err / None). The `Err(())` call
10281        // contributes one branch in addition (call_expression).
10282        check_metrics::<RustParser>(
10283            "fn f() -> Result<i32, ()> { let r: Result<i32, ()> = Err(()); Ok(r?) }",
10284            "foo.rs",
10285            |metric| {
10286                // Err(()) + Ok(...) + r? → 2 calls + 1 try = 3 branches.
10287                assert_eq!(metric.abc.branches_sum(), 3);
10288                insta::assert_json_snapshot!(metric.abc);
10289            },
10290        );
10291    }
10292
10293    #[cfg(feature = "rust")]
10294    #[test]
10295    fn rust_comparisons_count_conditions() {
10296        // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six
10297        // comparisons → C = 6.
10298        check_metrics::<RustParser>(
10299            "fn f(a: i32, b: i32) -> bool { a < b || a > b || a <= b || a >= b || a == b || a != b }",
10300            "foo.rs",
10301            |metric| {
10302                assert_eq!(metric.abc.conditions_sum(), 6);
10303                insta::assert_json_snapshot!(metric.abc);
10304            },
10305        );
10306    }
10307
10308    #[cfg(feature = "rust")]
10309    #[test]
10310    fn rust_generic_brackets_not_conditions() {
10311        // `<` / `>` in `Vec<i32>` are TypeArguments delimiters, not
10312        // comparison operators. The parent-check in the LT/GT arms
10313        // must filter them out. Expected C = 0.
10314        check_metrics::<RustParser>(
10315            "fn f() -> Vec<i32> { Vec::<i32>::new() }",
10316            "foo.rs",
10317            |metric| {
10318                assert_eq!(metric.abc.conditions_sum(), 0);
10319                insta::assert_json_snapshot!(metric.abc);
10320            },
10321        );
10322    }
10323
10324    #[cfg(feature = "rust")]
10325    #[test]
10326    fn rust_if_let_counts_as_condition() {
10327        // `if let Some(v) = opt { ... }` introduces a `let_condition`
10328        // → 1 condition. The `if` keyword itself does not add another
10329        // count — Fitzpatrick counts conditions, not branch keywords.
10330        check_metrics::<RustParser>(
10331            "fn f(opt: Option<i32>) { if let Some(_v) = opt { } }",
10332            "foo.rs",
10333            |metric| {
10334                assert_eq!(metric.abc.conditions_sum(), 1);
10335                insta::assert_json_snapshot!(metric.abc);
10336            },
10337        );
10338    }
10339
10340    #[cfg(feature = "rust")]
10341    #[test]
10342    fn rust_while_let_counts_as_condition() {
10343        // `while let Some(y) = it.next() { ... }` is also a
10344        // `let_condition` (the `while` form). One condition; the
10345        // `it.next()` call adds one branch.
10346        check_metrics::<RustParser>(
10347            "fn f(mut it: std::vec::IntoIter<i32>) { while let Some(_y) = it.next() { } }",
10348            "foo.rs",
10349            |metric| {
10350                assert_eq!(metric.abc.conditions_sum(), 1);
10351                assert_eq!(metric.abc.branches_sum(), 1);
10352                insta::assert_json_snapshot!(metric.abc);
10353            },
10354        );
10355    }
10356
10357    #[cfg(feature = "rust")]
10358    #[test]
10359    fn rust_match_arms_count_conditions_wildcard_excluded() {
10360        // Three arms: `0 => 1`, `n if n > 0 => n`, `_ => -1`. The
10361        // bare wildcard is the `default:` equivalent and is skipped.
10362        // The guarded arm has a `n if n > 0` pattern (more than one
10363        // child in the match_pattern) and still counts. Two non-wildcard
10364        // arms → C = 2 from MatchArm. Plus the comparison `n > 0`
10365        // adds one more → C = 3.
10366        check_metrics::<RustParser>(
10367            "fn f(x: i32) -> i32 { match x { 0 => 1, n if n > 0 => n, _ => -1, } }",
10368            "foo.rs",
10369            |metric| {
10370                assert_eq!(metric.abc.conditions_sum(), 3);
10371                insta::assert_json_snapshot!(metric.abc);
10372            },
10373        );
10374    }
10375
10376    #[cfg(feature = "rust")]
10377    #[test]
10378    fn rust_else_counts_as_condition() {
10379        // `if a > b { ... } else { ... }` → `a > b` is one condition,
10380        // `else` is one condition → C = 2.
10381        check_metrics::<RustParser>(
10382            "fn f(a: i32, b: i32) -> i32 { if a > b { a } else { b } }",
10383            "foo.rs",
10384            |metric| {
10385                assert_eq!(metric.abc.conditions_sum(), 2);
10386                insta::assert_json_snapshot!(metric.abc);
10387            },
10388        );
10389    }
10390
10391    #[cfg(feature = "rust")]
10392    #[test]
10393    fn rust_let_chain2_hidden_rule_drift_marker() {
10394        // Drift marker (findings.md round-2 #3): `Rust::LetChain2`
10395        // maps to the hidden grammar rule `_let_chain`. At the
10396        // pinned tree-sitter-rust version it is never emitted as a
10397        // concrete node — the visible `LetChain` (= 352) carries
10398        // every let-chain. We list `LetChain2` defensively in
10399        // `rust_condition_scores_itself` and `rust_count_chain_operands`
10400        // (lesson 34); if a future grammar bump promotes
10401        // `_let_chain` to a visible rule, this assertion fails
10402        // loudly so the maintainer knows to verify the walker still
10403        // counts correctly for the new shape.
10404        let src = "fn f(a: bool, b: Option<i32>) {\n\
10405                   \x20   if a && let Some(_) = b { }\n\
10406                   }\n";
10407        let parser = RustParser::new(
10408            src.as_bytes().to_vec(),
10409            &std::path::PathBuf::from("foo.rs"),
10410            None,
10411        );
10412        assert!(!ast_has_kind_id(&parser, Rust::LetChain2 as u16));
10413    }
10414
10415    #[cfg(feature = "rust")]
10416    #[test]
10417    fn rust_scoped_identifier_condition_counts() {
10418        // Regression for findings.md round-2 #1 (Rust):
10419        // `if crate::FLAG {}` parses with `scoped_identifier` as the
10420        // condition node. Pre-fix, the former `rust_bool_terminal_kinds!()`
10421        // listed only `Identifier` so the walker reached the
10422        // `scoped_identifier` child, found it non-terminal /
10423        // non-paren / non-unary, and broke without counting.
10424        // Mirrors the C# fix in #372 (lesson 19) for
10425        // `MemberAccessExpression`.
10426        check_metrics::<RustParser>("fn f() { if crate::FLAG { } }\n", "foo.rs", |metric| {
10427            assert_eq!(metric.abc.conditions_sum(), 1);
10428            insta::assert_json_snapshot!(metric.abc);
10429        });
10430    }
10431
10432    #[cfg(feature = "rust")]
10433    #[test]
10434    fn rust_await_expression_condition_counts() {
10435        // Regression for findings.md round-2 #2 (Rust):
10436        // `if ready().await {}` parses with `await_expression` as
10437        // the condition node. Adding `Rust::AwaitExpression` to the
10438        // former terminal-bool set closed the parity gap with the C#
10439        // reference (the former `csharp_bool_terminal_kinds!()`).
10440        check_metrics::<RustParser>(
10441            "async fn ready() -> bool { true }\n\
10442             async fn f() { if ready().await { } }\n",
10443            "foo.rs",
10444            |metric| {
10445                // ready() is a call (1 branch); `ready().await` is
10446                // the unary boolean condition (1).
10447                assert_eq!(metric.abc.branches_sum(), 1);
10448                assert_eq!(metric.abc.conditions_sum(), 1);
10449                insta::assert_json_snapshot!(metric.abc);
10450            },
10451        );
10452    }
10453
10454    #[cfg(feature = "rust")]
10455    #[test]
10456    fn rust_complex_function_abc() {
10457        // Mixed-shape regression: assignments, calls, conditions, `?`,
10458        // `if let`, `match` in one body. Verified by hand:
10459        // - assignments: `let mut x = 0` (let init), `x = 5`, `x += 2`,
10460        //   `let _ = ...` (let init), `let r: ... = Err(())` (let init),
10461        //   `let _v = r?` (let init) → A = 6 (post-#393: every `=`
10462        //   initializer in a `let_declaration` is one assignment, in
10463        //   line with the literal Fitzpatrick reading).
10464        // - branches: `xs.iter()`, `.next()`, `Err(())`, `r?` → B = 4
10465        //   (3 calls + 1 try).
10466        // - conditions: `if let Some(v) = opt` → 1, `match x` arms
10467        //   `0`, `n if n>0` (wildcard excluded) → 2, `n > 0` → 1.
10468        //   Total C = 4.
10469        check_metrics::<RustParser>(
10470            "fn f(opt: Option<i32>, xs: Vec<i32>) -> Result<i32, ()> {\n\
10471             \x20   let mut x = 0;\n\
10472             \x20   x = 5;\n\
10473             \x20   x += 2;\n\
10474             \x20   if let Some(_v) = opt { }\n\
10475             \x20   let _ = xs.iter().next();\n\
10476             \x20   let r: Result<i32, ()> = Err(());\n\
10477             \x20   let _v = r?;\n\
10478             \x20   Ok(match x {\n\
10479             \x20       0 => 1,\n\
10480             \x20       n if n > 0 => n,\n\
10481             \x20       _ => -1,\n\
10482             \x20   })\n\
10483             }\n",
10484            "foo.rs",
10485            |metric| {
10486                assert_eq!(metric.abc.assignments_sum(), 6);
10487                // calls: xs.iter(), .next(), Err(()), Ok(...) → 4 calls
10488                // plus 1 try (`r?`) → 5 branches.
10489                assert_eq!(metric.abc.branches_sum(), 5);
10490                // 1 let_condition + 2 non-wildcard match_arms + 1
10491                // comparison (`n > 0`) → 4.
10492                assert_eq!(metric.abc.conditions_sum(), 4);
10493                insta::assert_json_snapshot!(metric.abc);
10494            },
10495        );
10496    }
10497
10498    #[cfg(feature = "rust")]
10499    #[test]
10500    fn rust_let_chain_bare_identifier_operand_counts() {
10501        // Regression: pre-fix, `if a && let Some(_z) = y { }` reported
10502        // 1 condition (only the LetCondition). The bare-identifier
10503        // `a` operand was lost because Rust 2024 wraps let-chain
10504        // `&&` operands in a `LetChain` node (not `BinaryExpression`)
10505        // and the former `rust_count_unary_conditions` only counted
10506        // terminals under a `BinaryExpression` parent. Allowing `LetChain` /
10507        // `LetChain2` as known-bool list parents fixed the loss.
10508        // Expected: LetCondition (1) + walker on `a` (1) = 2.
10509        check_metrics::<RustParser>(
10510            "fn f(a: bool, y: Option<i32>) {\n\
10511             \x20   if a && let Some(_z) = y { }\n\
10512             }\n",
10513            "foo.rs",
10514            |metric| {
10515                assert_eq!(metric.abc.conditions_sum(), 2);
10516                insta::assert_json_snapshot!(metric.abc);
10517            },
10518        );
10519    }
10520
10521    #[cfg(feature = "rust")]
10522    #[test]
10523    fn rust_if_multiple_conditions() {
10524        // Fitzpatrick Rule 7 / Listing 2 (issue #403): every operand of
10525        // a `&&` / `||` chain is one condition. Mirrors
10526        // `java_if_multiple_conditions`. Rust's `if` head has no
10527        // parentheses, but the walker fires on each `&&` / `||` token
10528        // and walks the parent `binary_expression` regardless.
10529        check_metrics::<RustParser>(
10530            "fn f(a: bool, b: bool, c: bool, d: bool) -> i32 {\n\
10531             \x20   if a || b || c || d { return 1; }    // +4c\n\
10532             \x20   if a && b && c { return 2; }         // +3c\n\
10533             \x20   if !a && !b { return 3; }            // +2c\n\
10534             \x20   0\n\
10535             }\n",
10536            "foo.rs",
10537            |metric| {
10538                // 4 + 3 + 2 = 9
10539                assert_eq!(metric.abc.conditions_sum(), 9);
10540                insta::assert_json_snapshot!(metric.abc);
10541            },
10542        );
10543    }
10544
10545    #[cfg(feature = "rust")]
10546    #[test]
10547    fn rust_while_conditions() {
10548        // Rust has no `do { ... } while(cond);` construct, so this
10549        // mirrors only the `while` half of `java_while_and_do_while_conditions`.
10550        // Each operand of the `&&` / `||` chain in the loop condition
10551        // counts as one Fitzpatrick condition (Rule 7).
10552        check_metrics::<RustParser>(
10553            "fn f(a: bool, b: bool) {\n\
10554             \x20   while a || b { break; }       // +2c\n\
10555             \x20   while a && !b { break; }      // +2c\n\
10556             }\n",
10557            "foo.rs",
10558            |metric| {
10559                assert_eq!(metric.abc.conditions_sum(), 4);
10560                insta::assert_json_snapshot!(metric.abc);
10561            },
10562        );
10563    }
10564
10565    #[cfg(feature = "rust")]
10566    #[test]
10567    fn rust_if_boolean_literal_condition() {
10568        // Phase 2B (issue #403): a condition whose entire body is a
10569        // boolean literal counts as one Fitzpatrick condition.
10570        // `if true {}` → 1, `if !false {}` → 1 (unary unwrap), and
10571        // `while true { break }` → 1.
10572        check_metrics::<RustParser>(
10573            "fn f() {\n\
10574             \x20   if true { }                  // +1c\n\
10575             \x20   if !false { }                // +1c\n\
10576             \x20   while true { break; }        // +1c\n\
10577             }\n",
10578            "foo.rs",
10579            |metric| {
10580                assert_eq!(metric.abc.conditions_sum(), 3);
10581                insta::assert_json_snapshot!(metric.abc);
10582            },
10583        );
10584    }
10585
10586    #[cfg(feature = "rust")]
10587    #[test]
10588    fn rust_methods_arguments_with_conditions() {
10589        // Phase 2B (issue #403): unary-conditional arguments to a
10590        // call each count once. `m(!a, !b)` → 2 conditions + 1
10591        // branch (the call itself). Bare identifier arguments do
10592        // NOT count (they reach the count_unary_conditions list with
10593        // list_kind = Arguments, not BinaryExpression).
10594        check_metrics::<RustParser>(
10595            "fn f(a: bool, b: bool) {\n\
10596             \x20   m(a, b);                     // +1b\n\
10597             \x20   m(!a, !b);                   // +1b +2c\n\
10598             \x20   m(!a, b, !a);                // +1b +2c\n\
10599             }\n",
10600            "foo.rs",
10601            |metric| {
10602                assert_eq!(metric.abc.branches_sum(), 3);
10603                assert_eq!(metric.abc.conditions_sum(), 4);
10604                insta::assert_json_snapshot!(metric.abc);
10605            },
10606        );
10607    }
10608
10609    #[cfg(feature = "rust")]
10610    #[test]
10611    fn rust_return_with_conditions() {
10612        // Phase 2B (issue #403). Mirrors `java_return_with_conditions`
10613        // — `return !a` / `return x && y` count their unary
10614        // conditional operands. Per Fitzpatrick Rule 7, a `!`-wrapped
10615        // relational expression contributes ONE condition (the
10616        // relational op itself) — the `!` does not add a second
10617        // count when its operand is already a comparison.
10618        check_metrics::<RustParser>(
10619            "fn m1(z: i32) -> bool { return !(z >= 0); }\n\
10620             fn m2(x: bool) -> bool { return (((!x))); }\n\
10621             fn m3(x: bool, y: bool) -> bool { return x && y; }\n\
10622             fn m4(y: bool, z: i32) -> bool { return y || (z < 0); }\n",
10623            "foo.rs",
10624            |metric| {
10625                // m1: !(z >= 0) → the `>=` contributes 1; the unary
10626                //     `!` wraps a paren'd BinaryExpression, which
10627                //     inspect_container does not unwrap further →
10628                //     no walker count. Total: 1.
10629                // m2: (((!x))) → ReturnExpression arm walks (((!x))).
10630                //     inspect_container unwraps three parens and one
10631                //     unary, reaches Identifier `x`, has_boolean_content
10632                //     was seeded true by the unary-not flip. +1.
10633                // m3: x && y → `&&` walker counts both terminals → 2.
10634                // m4: y || (z < 0) → `||` walker counts `y` (terminal,
10635                //     +1); the `<` contributes 1 via its own arm; the
10636                //     paren'd BinaryExpression `(z < 0)` is not
10637                //     terminal under the walker → no extra count.
10638                //     Total: 2.
10639                // Sum: 1 + 1 + 2 + 2 = 6.
10640                assert_eq!(metric.abc.conditions_sum(), 6);
10641                insta::assert_json_snapshot!(metric.abc);
10642            },
10643        );
10644    }
10645
10646    #[cfg(feature = "rust")]
10647    #[test]
10648    fn rust_short_circuit_with_boolean_literal_operand() {
10649        // `if a && true` reports 2 conditions: one for the identifier
10650        // operand, one for the boolean-literal operand. Confirms the
10651        // walker terminal set includes `BooleanLiteral`.
10652        check_metrics::<RustParser>(
10653            "fn f(a: bool) -> bool { a && true }\n",
10654            "foo.rs",
10655            |metric| {
10656                assert_eq!(metric.abc.conditions_sum(), 2);
10657                insta::assert_json_snapshot!(metric.abc);
10658            },
10659        );
10660    }
10661
10662    // ----- Go -----
10663
10664    #[cfg(feature = "go")]
10665    #[test]
10666    fn go_empty_unit_zero() {
10667        // Package declaration only — no Fitzpatrick events. Confirms the
10668        // GoCode Abc trait is wired up and emits zero counts.
10669        check_metrics::<GoParser>("package main\n", "empty.go", |metric| {
10670            assert_eq!(metric.abc.assignments_sum(), 0);
10671            assert_eq!(metric.abc.branches_sum(), 0);
10672            assert_eq!(metric.abc.conditions_sum(), 0);
10673            insta::assert_json_snapshot!(metric.abc);
10674        });
10675    }
10676
10677    #[cfg(feature = "go")]
10678    #[test]
10679    fn go_assignments_count_plain_compound_short_var_and_incdec() {
10680        // `x := 0` (short var decl), `x = 5` and `x = 7` (plain `=`),
10681        // `x += 2` (compound), `x++` (inc), and the initialized
10682        // declaration `var y = 1` — which is counted, matching the Rust
10683        // and Java rules for `let y = 1` / `int y = 1` (both measured at
10684        // one assignment each). The comment here previously claimed the
10685        // opposite and pinned Go at 6 (#1278).
10686        check_metrics::<GoParser>(
10687            "package main\nfunc f() { var y = 1; _ = y; x := 0; x = 5; x += 2; x = 7; x++ }\n",
10688            "foo.go",
10689            |metric| {
10690                // `_ = y` is itself an assignment_statement → +1.
10691                // var y=1 + _=y + x:= + x=5 + x+=2 + x=7 + x++ → 7
10692                assert_eq!(metric.abc.assignments_sum(), 7);
10693                assert_eq!(metric.abc.branches_sum(), 0);
10694                assert_eq!(metric.abc.conditions_sum(), 0);
10695                insta::assert_json_snapshot!(metric.abc);
10696            },
10697        );
10698    }
10699
10700    #[cfg(feature = "go")]
10701    #[test]
10702    fn go_var_declarations_count_only_when_initialized() {
10703        // Regression for #1278: a `var` declaration with an initializer is
10704        // a `var_spec` carrying a `value` field, not an
10705        // `assignment_statement` or `short_var_declaration`, so it scored
10706        // zero — `var x = 5` and `x := 5` are the same binding spelled two
10707        // ways. Both typed and untyped initializers count; an
10708        // uninitialized `var z int` and a `const` do not.
10709        // expected: 3 assignments — `var x = 5`, `var y int = 6`, `z := 7`;
10710        // `var w int` and `const c = 1` contribute nothing.
10711        check_metrics::<GoParser>(
10712            "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",
10713            "foo.go",
10714            |metric| {
10715                assert_eq!(metric.abc.assignments_sum(), 3);
10716                assert_eq!(metric.abc.branches_sum(), 0);
10717            },
10718        );
10719    }
10720
10721    #[cfg(feature = "go")]
10722    #[test]
10723    fn go_grouped_var_block_counts_each_initialized_spec() {
10724        // A grouped `var ( … )` block is one `var_declaration` holding one
10725        // `var_spec` per line, so matching the spec counts each initialized
10726        // line on its own. A multi-name spec is still one binding
10727        // statement, matching `p, q := 1, 2` (#1278).
10728        // expected: 3 assignments — `a = 1`, `p, q = 1, 2`, and `r := 0`;
10729        // the uninitialized `b int` contributes nothing.
10730        check_metrics::<GoParser>(
10731            "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",
10732            "foo.go",
10733            |metric| {
10734                // The trailing `_ = …` is itself an assignment_statement.
10735                assert_eq!(metric.abc.assignments_sum(), 4);
10736            },
10737        );
10738    }
10739
10740    #[cfg(feature = "go")]
10741    #[test]
10742    fn go_calls_are_branches() {
10743        // Three calls: free function `g()`, method call `r.Inc()`, and
10744        // builtin call `len(s)`. All parse as `call_expression` → B = 3.
10745        // Composite literal `Foo{}` is NOT a call.
10746        check_metrics::<GoParser>(
10747            "package main\n\
10748             type R struct{}\n\
10749             func (r R) Inc() {}\n\
10750             func g() {}\n\
10751             func f(s string) { g(); var r R = R{}; r.Inc(); _ = len(s) }\n",
10752            "foo.go",
10753            |metric| {
10754                assert_eq!(metric.abc.branches_sum(), 3);
10755                insta::assert_json_snapshot!(metric.abc);
10756            },
10757        );
10758    }
10759
10760    #[cfg(feature = "go")]
10761    #[test]
10762    fn go_comparisons_count_conditions() {
10763        // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six
10764        // comparisons → C = 6.
10765        check_metrics::<GoParser>(
10766            "package main\nfunc f(a, b int) bool { return a < b || a > b || a <= b || a >= b || a == b || a != b }\n",
10767            "foo.go",
10768            |metric| {
10769                assert_eq!(metric.abc.conditions_sum(), 6);
10770                insta::assert_json_snapshot!(metric.abc);
10771            },
10772        );
10773    }
10774
10775    #[cfg(feature = "go")]
10776    #[test]
10777    fn go_generic_brackets_not_conditions() {
10778        // Generic instantiation `Min[int](a, b)` puts `int` inside
10779        // `TypeArguments`, not `BinaryExpression`. The parent guard on
10780        // `<` / `>` must not count these. Expected C = 0; B = 1 (one call).
10781        check_metrics::<GoParser>(
10782            "package main\nfunc Min[T int | float64](a, b T) T { return a }\nfunc f() { _ = Min[int](1, 2) }\n",
10783            "foo.go",
10784            |metric| {
10785                assert_eq!(metric.abc.conditions_sum(), 0);
10786                assert_eq!(metric.abc.branches_sum(), 1);
10787                insta::assert_json_snapshot!(metric.abc);
10788            },
10789        );
10790    }
10791
10792    #[cfg(feature = "go")]
10793    #[test]
10794    fn go_switch_arms_count_conditions_default_excluded() {
10795        // Four arms: `case 1:`, `case 2:`, `case 3:`, `default:`. The
10796        // bare `default` is the C/Java `default:` equivalent and is
10797        // excluded — 3 conditions from ExpressionCase. The switch
10798        // expression `x` is bare (no comparison), so no extra
10799        // condition from `==`-style operators.
10800        check_metrics::<GoParser>(
10801            "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",
10802            "foo.go",
10803            |metric| {
10804                assert_eq!(metric.abc.conditions_sum(), 3);
10805                insta::assert_json_snapshot!(metric.abc);
10806            },
10807        );
10808    }
10809
10810    #[cfg(feature = "go")]
10811    #[test]
10812    fn go_type_switch_arms_count_conditions() {
10813        // Type switch: `case int:`, `case string:`, `default:`. Two
10814        // non-default type-case arms → C = 2.
10815        check_metrics::<GoParser>(
10816            "package main\nfunc f(v interface{}) { switch v.(type) { case int: return; case string: return; default: return } }\n",
10817            "foo.go",
10818            |metric| {
10819                assert_eq!(metric.abc.conditions_sum(), 2);
10820                insta::assert_json_snapshot!(metric.abc);
10821            },
10822        );
10823    }
10824
10825    #[cfg(feature = "go")]
10826    #[test]
10827    fn go_select_arms_count_conditions() {
10828        // `select { case <-ch: ...; case ch <- 1: ...; default: ... }`.
10829        // Two non-default communication cases → C = 2.
10830        check_metrics::<GoParser>(
10831            "package main\nfunc f(ch chan int) { select { case <-ch: return; case ch <- 1: return; default: return } }\n",
10832            "foo.go",
10833            |metric| {
10834                assert_eq!(metric.abc.conditions_sum(), 2);
10835                insta::assert_json_snapshot!(metric.abc);
10836            },
10837        );
10838    }
10839
10840    // Anchors the tag shape of a Go fixture before scoring its members:
10841    // `tagged` is how many expression switches carry a `value` field, so
10842    // a fixture edit that turns a tagless switch tagged (or back) fails
10843    // here rather than as a moved score.
10844    #[cfg(feature = "go")]
10845    fn assert_go_switch_members(
10846        src: &str,
10847        tagged: usize,
10848        kinds: &[(u16, usize, &str)],
10849        expected: &[(&str, u64, u64)],
10850    ) {
10851        let parser = GoParser::new(
10852            src.as_bytes().to_vec(),
10853            std::path::Path::new("foo.go"),
10854            None,
10855        );
10856        let found = parser
10857            .root()
10858            .preorder()
10859            .filter(|n| {
10860                n.kind_id() == Go::ExpressionSwitchStatement
10861                    && n.child_by_field_name("value").is_some()
10862            })
10863            .count();
10864        assert_eq!(found, tagged, "tagged switches — the fixture moved");
10865        assert_fixture_spells::<GoParser>(src, "foo.go", kinds);
10866        check_func_space::<GoParser, _>(src, "foo.go", |space| {
10867            assert_members_score(&space, expected);
10868        });
10869    }
10870
10871    // #1523, transferring #1453's Ruby rule. A tagless switch is
10872    // `switch true`: each case expression is an ordinary boolean, so a
10873    // comparison in it is already paid by the token arm, and the case's
10874    // former blanket +1 counted `gt` twice against its `if` twin `iff`.
10875    // Every row now equals the `if` spelling of the same predicate.
10876    //
10877    // `bare`, `paren` and `neg` are the controls: no operator for any
10878    // other arm to count, so the case still pays through the `if` slot
10879    // (1 before the fix and after). `negcmp` peels to a comparison the
10880    // token arm owns. `init1` carries an `initializer` and no tag, and
10881    // `cmt` puts a comment where the tag would go: neither is the
10882    // `value` field, so both stay tagless. `nest2` is a tagless switch
10883    // inside a tagged one — each switch is judged by its own tag.
10884    #[cfg(feature = "go")]
10885    #[test]
10886    fn go_tagless_switch_case_counts_its_condition_once() {
10887        let src = "package main
10888func gt(x int) int { switch { case x > 5: return 1 }; return 0 }
10889func iff(x int) int { if x > 5 { return 1 }; return 0 }
10890func bare(b bool) int { switch { case b: return 1 }; return 0 }
10891func paren(b bool) int { switch { case (b): return 1 }; return 0 }
10892func neg(b bool) int { switch { case !b: return 1 }; return 0 }
10893func negcmp(x int) int { switch { case !(x > 5): return 1 }; return 0 }
10894func andd(a, b bool) int { switch { case a && b: return 1 }; return 0 }
10895func two(x int) int { switch { case x > 1: fallthrough; case x < 0: return 1; default: return 2 } }
10896func init1(s string) int { switch n := len(s); { case n > 1: return 1 }; return 0 }
10897func cmt(x int) int { switch /* no tag */ { case /* c */ x > 5: return 1 }; return 0 }
10898func nest(x, y int) int { switch { case x > 1: switch { case y > 1: return 1 } }; return 0 }
10899func nest2(x, y int) int { switch x { case 1: switch { case y > 1: return 1 } }; return 0 }
10900";
10901        assert_go_switch_members(
10902            src,
10903            1,
10904            &[
10905                (Go::ExpressionCase as u16, 14, "expression cases"),
10906                (Go::DefaultCase as u16, 1, "`two`'s `default`"),
10907                (Go::GT as u16, 9, "`>` comparisons"),
10908                (
10909                    Go::ParenthesizedExpression as u16,
10910                    2,
10911                    "`paren` / `negcmp` parens",
10912                ),
10913                (Go::Comment as u16, 2, "`cmt`'s comments"),
10914            ],
10915            &[
10916                // (member, abc.conditions, cyclomatic)
10917                // Was 2: the `>` and the case, for one decision.
10918                ("gt", 1, 2),
10919                ("iff", 1, 2),
10920                ("bare", 1, 2),
10921                ("paren", 1, 2),
10922                ("neg", 1, 2),
10923                // Was 2.
10924                ("negcmp", 1, 2),
10925                // Was 3: the Rule 9 walker owns both operands.
10926                ("andd", 2, 3),
10927                // Was 4. `default` stays +0 and `fallthrough` adds nothing.
10928                ("two", 2, 3),
10929                // Was 2.
10930                ("init1", 1, 2),
10931                // Was 2. A comparison, not a bare operand, so a misread
10932                // tag (+1 per case on top of the `>`) shows as 2.
10933                ("cmt", 1, 2),
10934                // Was 4.
10935                ("nest", 2, 3),
10936                // Was 3: the tagged outer case keeps its 1.
10937                ("nest2", 2, 3),
10938            ],
10939        );
10940    }
10941
10942    // A pointer dereference or channel receive is a boolean operand the
10943    // way an identifier is: `*p` of a `*bool`, `<-ch` of a `chan bool`.
10944    // No operator arm counts either, so each slot pays for it — the `if`
10945    // slot, a tagless case (which #1523 routed through that slot, so
10946    // `swp` / `swr` fell from 1 to 0 until this), the `!` / paren peel,
10947    // and an `&&` operand. `neg` and `asg` are the controls: `-x`, `*&y`
10948    // and `b := *p` sit in no boolean slot and stay 0 (#1526).
10949    #[cfg(feature = "go")]
10950    #[test]
10951    fn go_deref_and_receive_are_boolean_operands() {
10952        let src = "package main
10953func ifp(p *bool) int { if *p { return 1 }; return 0 }
10954func ifr(ch chan bool) int { if <-ch { return 1 }; return 0 }
10955func swp(p *bool) int { switch { case *p: return 1 }; return 0 }
10956func swr(ch chan bool) int { switch { case <-ch: return 1 }; return 0 }
10957func andp(a bool, p *bool) int { if a && *p { return 1 }; return 0 }
10958func notp(p *bool) int { if !*p { return 1 }; return 0 }
10959func parp(p *bool) int { if (*p) { return 1 }; return 0 }
10960func neg(x int) int { y := -x; z := *&y; return z }
10961func asg(p *bool) bool { b := *p; return b }
10962";
10963        assert_go_switch_members(
10964            src,
10965            0,
10966            &[
10967                (Go::ExpressionCase as u16, 2, "`swp` / `swr` cases"),
10968                (Go::LTDASH as u16, 2, "`ifr` / `swr` receives"),
10969                (Go::ParenthesizedExpression as u16, 1, "`parp`'s parens"),
10970            ],
10971            &[
10972                // (member, abc.conditions, cyclomatic)
10973                ("ifp", 1, 2),
10974                ("ifr", 1, 2),
10975                ("swp", 1, 2),
10976                ("swr", 1, 2),
10977                ("andp", 2, 3),
10978                ("notp", 1, 2),
10979                ("parp", 1, 2),
10980                ("neg", 0, 1),
10981                ("asg", 0, 1),
10982            ],
10983        );
10984    }
10985
10986    // A case listing several expressions is an implicit `||` — Go's spec
10987    // defines a tagless switch as `switch true`, so `case a, b:` holds
10988    // when `a == true || b == true` — and each expression is scored as an
10989    // operand of that chain: once, unless its own arm already counted it.
10990    // `ab` and `mix` therefore read 2, exactly their `if a || b` /
10991    // `if a || x > 1` twins, and `xim` shows the order cannot matter.
10992    // This is #1453's Ruby rule (`when a, b` reads 2) rather than
10993    // Kotlin's first-alternative-only rule, which scores the two orders
10994    // differently.
10995    //
10996    // Parity consequence: cyclomatic scores the case as one decision, so
10997    // these rows sit one above `conditions == cyclomatic - 1`, while the
10998    // `||` twins sit at it — the same trade Ruby's `when a, b` makes.
10999    #[cfg(feature = "go")]
11000    #[test]
11001    fn go_tagless_switch_case_lists_are_an_implicit_or() {
11002        let src = "package main
11003func ab(a, b bool) int { switch { case a, b: return 1 }; return 0 }
11004func ifab(a, b bool) int { if a || b { return 1 }; return 0 }
11005func mix(a bool, x int) int { switch { case a, x > 1: return 1 }; return 0 }
11006func xim(a bool, x int) int { switch { case x > 1, a: return 1 }; return 0 }
11007func ifmix(a bool, x int) int { if a || x > 1 { return 1 }; return 0 }
11008func cmts(a, b bool) int { switch { case a, /* c */ b: return 1 }; return 0 }
11009";
11010        assert_go_switch_members(
11011            src,
11012            0,
11013            &[
11014                (Go::ExpressionCase as u16, 4, "expression cases"),
11015                (Go::PIPEPIPE as u16, 2, "the `||` twins"),
11016                (Go::Comment as u16, 1, "`cmts`'s comment"),
11017            ],
11018            &[
11019                // Was 1: only the case, for two operands.
11020                ("ab", 2, 2),
11021                ("ifab", 2, 3),
11022                ("mix", 2, 2),
11023                ("xim", 2, 2),
11024                ("ifmix", 2, 3),
11025                // Was 1: the comment is no operand.
11026                ("cmts", 2, 2),
11027            ],
11028        );
11029    }
11030
11031    // The controls that must not move. A tagged case lists values
11032    // compared with the tag, a comparison the source never spells, so the
11033    // case itself is the condition — `init2` carries an `initializer`
11034    // beside its tag, and `tagcmp`'s `x > 1` is a value compared with `b`
11035    // (`if b == (x > 1)` also scores 2). Type-switch and `select` cases
11036    // are not expression cases at all.
11037    #[cfg(feature = "go")]
11038    #[test]
11039    fn go_tagged_switch_keeps_the_per_case_count() {
11040        let src = "package main
11041func tag(x int) int { switch x { case 1, 2: return 1 }; return 0 }
11042func init2(s string) int { switch n := len(s); n { case 1: return 1 }; return 0 }
11043func tagcmp(b bool, x int) int { switch b { case x > 1: return 1 }; return 0 }
11044func ts(v any) int { switch v.(type) { case int: return 1 }; return 0 }
11045func sel(c chan int) int { select { case <-c: return 1 }; return 0 }
11046";
11047        assert_go_switch_members(
11048            src,
11049            3,
11050            &[
11051                (Go::ExpressionCase as u16, 3, "expression cases"),
11052                (Go::TypeCase as u16, 1, "`ts`'s type case"),
11053                (
11054                    Go::CommunicationCase as u16,
11055                    1,
11056                    "`sel`'s communication case",
11057                ),
11058            ],
11059            &[
11060                ("tag", 1, 2),
11061                ("init2", 1, 2),
11062                ("tagcmp", 2, 2),
11063                ("ts", 1, 2),
11064                ("sel", 1, 2),
11065            ],
11066        );
11067    }
11068
11069    #[cfg(feature = "go")]
11070    #[test]
11071    fn go_else_counts_as_condition() {
11072        // `if a > b { ... } else { ... }` → `a > b` is one condition,
11073        // `else` is one condition → C = 2.
11074        check_metrics::<GoParser>(
11075            "package main\nfunc f(a, b int) int { if a > b { return a } else { return b } }\n",
11076            "foo.go",
11077            |metric| {
11078                assert_eq!(metric.abc.conditions_sum(), 2);
11079                insta::assert_json_snapshot!(metric.abc);
11080            },
11081        );
11082    }
11083
11084    #[cfg(feature = "go")]
11085    #[test]
11086    fn go_complex_function_abc() {
11087        // Mixed shape, verified by hand:
11088        // - Assignments: `var x = 10` (an initialized declaration, #1278),
11089        //   `_ = x`, `n := 0`, `n = n + 1`, `n += 2`, `n++`,
11090        //   `_ = len(s)` → A = 7. Every `_ = ...` IS counted as an
11091        //   assignment_statement.
11092        // - Branches: `len(s)` → B = 1.
11093        // - Conditions: `n < 10` → 1, `else` → 1, switch arms `case 0:`
11094        //   and `case 1:` (default excluded) → 2 → total C = 4.
11095        check_metrics::<GoParser>(
11096            "package main\nfunc f(s string) int {\n\
11097             \x20   var x = 10\n\
11098             \x20   _ = x\n\
11099             \x20   n := 0\n\
11100             \x20   if n < 10 { n = n + 1 } else { n += 2 }\n\
11101             \x20   n++\n\
11102             \x20   _ = len(s)\n\
11103             \x20   switch n {\n\
11104             \x20   case 0: return 0\n\
11105             \x20   case 1: return 1\n\
11106             \x20   default: return n\n\
11107             \x20   }\n\
11108             }\n",
11109            "foo.go",
11110            |metric| {
11111                assert_eq!(metric.abc.assignments_sum(), 7);
11112                assert_eq!(metric.abc.branches_sum(), 1);
11113                assert_eq!(metric.abc.conditions_sum(), 4);
11114                insta::assert_json_snapshot!(metric.abc);
11115            },
11116        );
11117    }
11118
11119    #[cfg(feature = "go")]
11120    #[test]
11121    fn go_if_multiple_conditions() {
11122        // Fitzpatrick Rule 7 walker fan-out (issue #403). Mirrors
11123        // `rust_if_multiple_conditions`.
11124        check_metrics::<GoParser>(
11125            "package p\n\
11126             func F(a, b, c, d bool) int {\n\
11127             \x20   if a || b || c || d { return 1 }    // +4c\n\
11128             \x20   if a && b && c { return 2 }         // +3c\n\
11129             \x20   if !a && !b { return 3 }            // +2c\n\
11130             \x20   return 0\n\
11131             }\n",
11132            "foo.go",
11133            |metric| {
11134                assert_eq!(metric.abc.conditions_sum(), 9);
11135                insta::assert_json_snapshot!(metric.abc);
11136            },
11137        );
11138    }
11139
11140    #[cfg(feature = "go")]
11141    #[test]
11142    fn go_for_with_conditions() {
11143        // Go has no `while` or `do { … } while(…);` — the `for` loop
11144        // header is the sole condition slot. Each operand of the
11145        // `&&` / `||` chain in the for-condition counts as one
11146        // Fitzpatrick condition.
11147        check_metrics::<GoParser>(
11148            "package p\n\
11149             func F(a, b bool) {\n\
11150             \x20   for a || b { break }       // +2c\n\
11151             \x20   for a && !b { break }      // +2c\n\
11152             }\n",
11153            "foo.go",
11154            |metric| {
11155                assert_eq!(metric.abc.conditions_sum(), 4);
11156                insta::assert_json_snapshot!(metric.abc);
11157            },
11158        );
11159    }
11160
11161    #[cfg(feature = "go")]
11162    #[test]
11163    fn go_for_bare_condition_counts() {
11164        // Regression for findings.md #1: `for true {}` / `for !ready {}`
11165        // are Go's only loop-condition slot. Pre-fix, the Phase-2B
11166        // dispatcher had no `G::ForStatement` arm, so bare-boolean
11167        // and `!`-wrapped `for` conditions silently reported zero.
11168        // `go_count_condition`'s terminal-bool / paren / unary filter
11169        // makes the walker safe across all three for-statement shapes:
11170        // bare condition, `for_clause` (init; cond; post) — whose own
11171        // `condition` field #1276 taught the walker to read — and
11172        // `range_clause`, which has no such field and contributes
11173        // nothing.
11174        check_metrics::<GoParser>(
11175            "package p\n\
11176             func F(ready bool) {\n\
11177             \x20   for true { break }      // +1c\n\
11178             \x20   for !ready { break }    // +1c\n\
11179             \x20   for i := 0; i < 3; i++ { _ = i }    // +1c (the `<`)\n\
11180             }\n",
11181            "foo.go",
11182            |metric| {
11183                // `for true`: walker counts True (+1).
11184                // `for !ready`: walker on unary unwraps to `ready`
11185                //   (+1).
11186                // `for_clause`'s condition is `i < 3`, a
11187                //   `binary_expression` that `go_count_condition`
11188                //   filters out; the `<` itself contributes 1 via the
11189                //   pre-existing LT/GT arm.
11190                // Total: 3.
11191                assert_eq!(metric.abc.conditions_sum(), 3);
11192                insta::assert_json_snapshot!(metric.abc);
11193            },
11194        );
11195    }
11196
11197    // Issue #1276, Go's share. `for_statement`'s child(1) is the
11198    // condition only in the `for cond {}` spelling; the three-clause
11199    // form puts it one level down, in the `for_clause`'s `condition`
11200    // field. Letting the `for_clause` fall through — which the arm's
11201    // own comment used to call harmless — scored a bare three-clause
11202    // condition zero while `for a {}` scored one.
11203    #[cfg(feature = "go")]
11204    #[test]
11205    fn go_three_clause_for_condition_counts() {
11206        // Bare identifier in the three-clause header: no comparison
11207        // token, so only the `for_clause` lookup can count it.
11208        check_metrics::<GoParser>(
11209            "package p\n\
11210             func F(a bool) {\n\
11211             \x20   for i := 0; a; i++ { _ = i }\n\
11212             }\n",
11213            "foo.go",
11214            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
11215        );
11216        // Negation, through `go_inspect_container`'s `!` unwrap.
11217        check_metrics::<GoParser>(
11218            "package p\n\
11219             func F(a bool) {\n\
11220             \x20   for i := 0; !a; i++ { _ = i }\n\
11221             }\n",
11222            "foo.go",
11223            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
11224        );
11225        // Empty condition: the `for_clause` exposes no `condition`
11226        // field, so zero — the same answer as Go's bare `for {}` and
11227        // as every other language since #1276.
11228        check_metrics::<GoParser>(
11229            "package p\n\
11230             func F() {\n\
11231             \x20   for i := 0; ; i++ { break }\n\
11232             }\n",
11233            "foo.go",
11234            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
11235        );
11236        // A `range_clause` carries no condition either, and the walker
11237        // must not mistake the clause itself for one.
11238        check_metrics::<GoParser>(
11239            "package p\n\
11240             func F(xs []int) {\n\
11241             \x20   for _, v := range xs { _ = v }\n\
11242             }\n",
11243            "foo.go",
11244            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
11245        );
11246    }
11247
11248    // Go's `for_statement` is the one grammar here that exposes no
11249    // `condition` field, so its header slot is located structurally and
11250    // has to skip what the field-addressed siblings get for free. Two
11251    // things it must skip, each of which `node.child(1)` got wrong:
11252    // a leading comment (tree-sitter counts comments among a node's
11253    // children — the #1181 failure), and the body of a bare `for {}`,
11254    // which IS child(1) and would otherwise be offered to
11255    // `go_count_condition` as though it were a condition.
11256    #[cfg(feature = "go")]
11257    #[test]
11258    fn go_for_header_slot_skips_comments_and_the_body() {
11259        // Each pair is (source, expected conditions). The commented
11260        // spelling must agree with its bare twin.
11261        let cases = [
11262            ("for a { break }", 1),
11263            ("for /* n */ a { break }", 1),
11264            ("for i := 0; a; i++ { _ = i }", 1),
11265            ("for /* n */ i := 0; a; i++ { _ = i }", 1),
11266            ("for i := 0; /* n */ a; i++ { _ = i }", 1),
11267            // Bare infinite loop: the body is child(1) and must not be
11268            // read as the condition.
11269            ("for { break }", 0),
11270            ("for /* n */ { break }", 0),
11271            ("for _, v := range xs { _ = v }", 0),
11272            ("for /* n */ _, v := range xs { _ = v }", 0),
11273        ];
11274        let mut ran = 0;
11275        for (body, expected) in cases {
11276            let src = format!("package p\nfunc F(a bool, xs []int) {{\n\t{body}\n}}\n");
11277            assert_eq!(abc_conditions(LANG::Go, &src), expected, "`{body}`");
11278            ran += 1;
11279        }
11280        // Non-vacuity, both halves: the loop must actually have run
11281        // every row, and the rows must carry both answers — a walker
11282        // stuck at 0 or at 1 would otherwise pass half the table
11283        // silently.
11284        assert_eq!(ran, cases.len());
11285        assert!(cases.iter().any(|&(_, n)| n == 1));
11286        assert!(cases.iter().any(|&(_, n)| n == 0));
11287    }
11288
11289    #[cfg(feature = "go")]
11290    #[test]
11291    fn go_if_init_statement_condition_counts() {
11292        // Regression for the code-review finding: Go's
11293        // `if x := f(); x { ... }` init-statement form puts the
11294        // short-var declaration at child(1) and the condition at
11295        // child(2). Pre-fix, the dispatcher used child(1) and
11296        // counted zero conditions for this idiomatic Go shape.
11297        // The fix uses `child_by_field_name("condition")` which
11298        // returns the condition regardless of init presence.
11299        check_metrics::<GoParser>(
11300            "package p\nfunc F() { if x := g(); x { } }\n",
11301            "foo.go",
11302            |metric| {
11303                // `x` bare-identifier condition contributes 1
11304                // (Rule 6 — bare boolean identifier in if-condition).
11305                // `g()` call contributes 1 branch but no condition.
11306                assert_eq!(metric.abc.branches_sum(), 1);
11307                assert_eq!(metric.abc.conditions_sum(), 1);
11308                insta::assert_json_snapshot!(metric.abc);
11309            },
11310        );
11311    }
11312
11313    #[cfg(feature = "go")]
11314    #[test]
11315    fn go_if_boolean_literal_condition() {
11316        check_metrics::<GoParser>(
11317            "package p\n\
11318             func F() {\n\
11319             \x20   if true {}                  // +1c\n\
11320             \x20   if !false {}                // +1c\n\
11321             }\n",
11322            "foo.go",
11323            |metric| {
11324                assert_eq!(metric.abc.conditions_sum(), 2);
11325                insta::assert_json_snapshot!(metric.abc);
11326            },
11327        );
11328    }
11329
11330    #[cfg(feature = "go")]
11331    #[test]
11332    fn go_methods_arguments_with_conditions() {
11333        check_metrics::<GoParser>(
11334            "package p\n\
11335             func F(a, b bool) {\n\
11336             \x20   m(a, b)                     // +1b\n\
11337             \x20   m(!a, !b)                   // +1b +2c\n\
11338             }\n",
11339            "foo.go",
11340            |metric| {
11341                assert_eq!(metric.abc.branches_sum(), 2);
11342                assert_eq!(metric.abc.conditions_sum(), 2);
11343                insta::assert_json_snapshot!(metric.abc);
11344            },
11345        );
11346    }
11347
11348    #[cfg(feature = "go")]
11349    #[test]
11350    fn go_return_with_conditions() {
11351        check_metrics::<GoParser>(
11352            "package p\n\
11353             func M1(z int) bool { return !(z >= 0) }\n\
11354             func M2(x bool) bool { return !x }\n\
11355             func M3(x, y bool) bool { return x && y }\n",
11356            "foo.go",
11357            |metric| {
11358                // M1: `>=` (1). `!(z >= 0)` walker on the unary
11359                //     doesn't reach a terminal — stops at the
11360                //     BinaryExpression z>=0 inside the parens. +1.
11361                // M2: walker on `!x` → 1.
11362                // M3: `&&` walker counts both → 2.
11363                // Sum: 1 + 1 + 2 = 4.
11364                assert_eq!(metric.abc.conditions_sum(), 4);
11365                insta::assert_json_snapshot!(metric.abc);
11366            },
11367        );
11368    }
11369
11370    #[cfg(feature = "go")]
11371    #[test]
11372    fn go_short_circuit_with_boolean_literal_operand() {
11373        // `a && true` reports 2 conditions: one identifier, one
11374        // boolean literal. Confirms the terminal set includes
11375        // `True` / `False`.
11376        check_metrics::<GoParser>(
11377            "package p\nfunc F(a bool) bool { return a && true }\n",
11378            "foo.go",
11379            |metric| {
11380                assert_eq!(metric.abc.conditions_sum(), 2);
11381                insta::assert_json_snapshot!(metric.abc);
11382            },
11383        );
11384    }
11385
11386    // ----- Elixir -----
11387
11388    // No top-level Calls and no operators → all three vectors are
11389    // zero. Uses a bare expression rather than a `defmodule` wrapper
11390    // (which would itself be a Call → 1 branch). Confirms the
11391    // ElixirCode Abc trait is wired up and the metric emits.
11392    #[cfg(feature = "elixir")]
11393    #[test]
11394    fn elixir_empty_unit_zero() {
11395        check_metrics::<ElixirParser>(":ok\n", "foo.ex", |metric| {
11396            assert_eq!(metric.abc.assignments_sum(), 0);
11397            assert_eq!(metric.abc.branches_sum(), 0);
11398            assert_eq!(metric.abc.conditions_sum(), 0);
11399            insta::assert_json_snapshot!(metric.abc);
11400        });
11401    }
11402
11403    // An empty `defmodule Foo do ... end` is itself ONE `Call` →
11404    // Documents that module-/function-defining macros (`defmodule`,
11405    // `def`, `defp`, `defmacro`, `defmacrop`) and declarative
11406    // directives (`alias`, `import`, `require`, `use`) are NOT
11407    // runtime dispatch and therefore do NOT inflate `branches`,
11408    // matching Cognitive's treatment.
11409    #[cfg(feature = "elixir")]
11410    #[test]
11411    fn elixir_defmodule_is_zero_branches() {
11412        check_metrics::<ElixirParser>("defmodule Foo do\nend\n", "foo.ex", |metric| {
11413            assert_eq!(metric.abc.branches_sum(), 0);
11414            assert_eq!(metric.abc.assignments_sum(), 0);
11415            assert_eq!(metric.abc.conditions_sum(), 0);
11416            insta::assert_json_snapshot!(metric.abc);
11417        });
11418    }
11419
11420    // Pattern-match `=` counts as an assignment. Two bindings → A = 2.
11421    // `defmodule` and `def` are declarative-Call wrappers and are
11422    // filtered out of branches; the assertion focuses on assignments
11423    // so we only pin that vector.
11424    #[cfg(feature = "elixir")]
11425    #[test]
11426    fn elixir_pattern_match_is_assignment() {
11427        check_metrics::<ElixirParser>(
11428            "defmodule Foo do\n  def f do\n    x = 1\n    y = x + 1\n    y\n  end\nend\n",
11429            "foo.ex",
11430            |metric| {
11431                assert_eq!(metric.abc.assignments_sum(), 2);
11432                insta::assert_json_snapshot!(metric.abc);
11433            },
11434        );
11435    }
11436
11437    // `|>` pipeline operator: each `|>` token contributes one branch.
11438    // Two `|>` ops → +2 from the pipe operator itself. Each pipeline
11439    // step also dispatches a Call (`String.upcase(...)`,
11440    // `String.trim(...)`) — these are wrapped inside the outer
11441    // pipeline Call tree, contributing additional Call branches.
11442    // The headline assertion confirms (a) `|>` is detected and (b)
11443    // pipeline steps are not silently dropped.
11444    #[cfg(feature = "elixir")]
11445    #[test]
11446    fn elixir_pipeline_each_step_is_branch() {
11447        check_metrics::<ElixirParser>(
11448            "defmodule Foo do\n  def normalize(s) do\n    s |> String.trim() |> String.upcase()\n  end\nend\n",
11449            "foo.ex",
11450            |metric| {
11451                // Pipeline yields 2 `|>` branches plus Calls for
11452                // String.trim, String.upcase, and the outer pipeline
11453                // (which surfaces as a Call wrapping the binary
11454                // operator). `def` and `defmodule` are declarative
11455                // and excluded. Empirical total: B = 5.
11456                assert_eq!(metric.abc.branches_sum(), 5);
11457                assert_eq!(metric.abc.assignments_sum(), 0);
11458                insta::assert_json_snapshot!(metric.abc);
11459            },
11460        );
11461    }
11462
11463    // Comparison operators all count as conditions. Six comparisons
11464    // (`==`, `!=`, `<`, `>`, `<=`, `>=`) → C = 6.
11465    #[cfg(feature = "elixir")]
11466    #[test]
11467    fn elixir_comparisons_are_conditions() {
11468        check_metrics::<ElixirParser>(
11469            "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",
11470            "foo.ex",
11471            |metric| {
11472                assert_eq!(metric.abc.conditions_sum(), 6);
11473                insta::assert_json_snapshot!(metric.abc);
11474            },
11475        );
11476    }
11477
11478    // Strict-equality operators `===` / `!==` count as conditions too.
11479    #[cfg(feature = "elixir")]
11480    #[test]
11481    fn elixir_strict_equality_is_condition() {
11482        check_metrics::<ElixirParser>(
11483            "defmodule Foo do\n  def f(a, b) do\n    a === b or a !== b\n  end\nend\n",
11484            "foo.ex",
11485            |metric| {
11486                assert_eq!(metric.abc.conditions_sum(), 2);
11487                insta::assert_json_snapshot!(metric.abc);
11488            },
11489        );
11490    }
11491
11492    // #1461 moved every relational construct a grammar spells as its own
11493    // production onto an unconditional arm — scored by use, Fitzpatrick
11494    // Rule 5 — in C#, Java, Groovy, Kotlin and Ruby. Elixir's membership
11495    // and type tests were the same class and were not in that sweep, so
11496    // `a in b` scored zero where `a == b` scored one. The gap stayed
11497    // invisible while the `when` arm paid a flat one for every guard,
11498    // and surfaced the moment the guard became a slot: an
11499    // operator-spelled guard is owned by its operator's arm, and `in`
11500    // had none.
11501    //
11502    // `bare` / `negbare` are the by-use claim; `slot` is the §5 check
11503    // that a boolean slot does not now score the same operator twice;
11504    // `cmp` is the control the two membership rows are levelled against.
11505    // `in` rides the `<` / `>` `binary_operator` gate rather than
11506    // standing alone — it has the same three grammar positions — and
11507    // `elixir_operator_identifier_is_not_a_condition` pins that gate.
11508    #[cfg(feature = "elixir")]
11509    #[test]
11510    fn elixir_membership_is_a_condition_by_use() {
11511        check_func_space::<ElixirParser, _>(
11512            "defmodule Foo do\n\
11513               def bare(a, b), do: a in b\n\
11514               def negbare(a, b), do: a not in b\n\
11515               def slot(a, b) do\n\
11516                 if a in b do\n\
11517                   IO.puts(\"x\")\n\
11518                 end\n\
11519               end\n\
11520               def cmp(a, b), do: a == b\n\
11521             end\n",
11522            "foo.ex",
11523            |space| {
11524                assert_members_score(
11525                    &space.spaces[0],
11526                    &[
11527                        // Each was 0 before the arm.
11528                        ("bare", 1, 1),
11529                        ("negbare", 1, 1),
11530                        // `in` (1) only — the `if` predicate slot is
11531                        // already paid by the operator (#1527), exactly
11532                        // as `if a == b` scores.
11533                        ("slot", 1, 2),
11534                        ("cmp", 1, 1),
11535                    ],
11536                );
11537            },
11538        );
11539    }
11540
11541    // Guard `when` clause counts as a condition — exactly one, and here
11542    // it is the `>` that supplies it. The guard is a condition *slot*
11543    // (#1454), so an operator-spelled guard scores through the operator
11544    // arm that owns it and the slot itself adds nothing; see
11545    // `elixir_guard_is_a_decision_however_spelled` for the four-spelling
11546    // agreement this is one row of.
11547    //
11548    // Was 2, on a flat `+1` for the `when` token laid on top of the `>`:
11549    // the §5 double count a whole-branch review of the #1454 batch
11550    // found.
11551    #[cfg(feature = "elixir")]
11552    #[test]
11553    fn elixir_guard_when_is_condition() {
11554        check_metrics::<ElixirParser>(
11555            "defmodule Foo do\n  def f(x) when x > 0 do\n    :pos\n  end\nend\n",
11556            "foo.ex",
11557            |metric| {
11558                // the guard slot (+0, the guard is an operator
11559                // application) + `>` (+1) = 1
11560                assert_eq!(metric.abc.conditions_sum(), 1);
11561                insta::assert_json_snapshot!(metric.abc);
11562            },
11563        );
11564    }
11565
11566    // A `case` pays one condition per clause (#1531) AND one branch for
11567    // the Call itself. The `_ ->` catch-all is the default arm and pays
11568    // nothing, so this two-clause `case` scores 1; the per-clause rule
11569    // is pinned by `elixir_clause_constructs_pay_per_clause`.
11570    #[cfg(feature = "elixir")]
11571    #[test]
11572    fn elixir_case_is_condition_and_branch() {
11573        check_metrics::<ElixirParser>(
11574            "defmodule Foo do\n  def f(x) do\n    case x do\n      1 -> :one\n      _ -> :other\n    end\n  end\nend\n",
11575            "foo.ex",
11576            |metric| {
11577                // conditions: `1 ->` (+1) + `_ ->` default (+0) = 1
11578                assert_eq!(metric.abc.conditions_sum(), 1);
11579                insta::assert_json_snapshot!(metric.abc);
11580            },
11581        );
11582    }
11583
11584    // `cond` scores per clause, as its nested-`if` twin does (#1527): the
11585    // `x > 0` clause is a predicate slot its `>` already pays, and the
11586    // `true ->` catch-all is the default arm and pays nothing.
11587    #[cfg(feature = "elixir")]
11588    #[test]
11589    fn elixir_cond_is_condition() {
11590        check_metrics::<ElixirParser>(
11591            "defmodule Foo do\n  def f(x) do\n    cond do\n      x > 0 -> :pos\n      true -> :other\n    end\n  end\nend\n",
11592            "foo.ex",
11593            |metric| {
11594                // conditions: `x > 0` clause slot (+0) + `>` (+1) +
11595                // `true ->` default (+0) = 1, against cyclomatic 2
11596                assert_eq!(metric.abc.conditions_sum(), 1);
11597                insta::assert_json_snapshot!(metric.abc);
11598            },
11599        );
11600    }
11601
11602    // An `if` / `unless` predicate and each `cond` clause are condition
11603    // slots (#1527): one condition, unless an operator in the slot
11604    // already paid. Before, the keyword Call paid a flat one on top of
11605    // its predicate, so `if x > 5` scored 2 and `cond` paid once per
11606    // construct whatever its clause count.
11607    //
11608    // Expected values come from the twins, not the rule: each `if` row
11609    // is what Ruby and Java score for the same `if` (`if x > 5` 1,
11610    // `if b` 1, `if a && b` 2 — measured), every non-chain row sits at
11611    // `cyclomatic - 1`, and `cond3` equals Go's tagless
11612    // `switch { case x > 5: … case b: … default: … }` (2), whose
11613    // `default:` is as free as `cond`'s `true ->`. Its nested-`if` twin
11614    // `nested` pays Fitzpatrick Rule 5 for each `else` (#1531), as
11615    // Ruby's `if … elsif … else` and Java's `if … else if … else` do (4
11616    // against cyclomatic 3 — measured). The chain rows sit one under
11617    // parity exactly as Ruby's and Java's `if a && b` (2 / 3) do.
11618    //
11619    // Paths only one row exercises (§11): `cmp_comment` the comment
11620    // skip in a parenthesised peel, `assign` the match-`=` peel,
11621    // `negcmp` the `not` peel, `arith` a non-comparison operator the
11622    // slot pays for, `capture` a chain operand that no terminal list
11623    // held (`&f/1` scored one short of `a && b` on main), and the
11624    // `true ->` rows the default-arm gate shared with cyclomatic.
11625    #[cfg(feature = "elixir")]
11626    #[test]
11627    fn elixir_if_and_cond_predicates_are_slots() {
11628        check_func_space::<ElixirParser, _>(
11629            "defmodule Foo do
11630  def cmp(x), do: (if x > 5 do 1 end)
11631  def bare(b), do: (if b do 1 end)
11632  def kw(x), do: if(x > 5, do: 1)
11633  def unless_cmp(x), do: (unless x > 5 do 1 end)
11634  def cmp_comment(x) do
11635    if (x > 5 # c
11636    ) do
11637      1
11638    end
11639  end
11640  def assign(x), do: (if (y = x > 5) do y end)
11641  def negcmp(x), do: (if not(x > 5) do 1 end)
11642  def arith(x), do: (if x + 1 do 1 end)
11643  def chain(a, b), do: (if a && b do 1 end)
11644  def capture(a), do: (if a && &f/1 do 1 end)
11645  def cond_cmp(x) do
11646    cond do
11647      x > 5 -> 1
11648      true -> 2
11649    end
11650  end
11651  def cond_bare(b) do
11652    cond do
11653      b -> 1
11654      true -> 2
11655    end
11656  end
11657  def cond3(x, b) do
11658    cond do
11659      x > 5 -> 1
11660      b -> 2
11661      true -> 3
11662    end
11663  end
11664  def nested(x, b) do
11665    if x > 5 do
11666      1
11667    else
11668      if b do 2 else 3 end
11669    end
11670  end
11671  def cond_no_default(a, b, c) do
11672    cond do
11673      a -> 1
11674      b -> 2
11675      c -> 3
11676    end
11677  end
11678end
11679",
11680            "foo.ex",
11681            |space| {
11682                assert_members_score(
11683                    &space.spaces[0],
11684                    &[
11685                        ("cmp", 1, 2),
11686                        ("bare", 1, 2),
11687                        ("kw", 1, 2),
11688                        ("unless_cmp", 1, 2),
11689                        ("cmp_comment", 1, 2),
11690                        ("assign", 1, 2),
11691                        ("negcmp", 1, 2),
11692                        ("arith", 1, 2),
11693                        ("chain", 2, 3),
11694                        ("capture", 2, 3),
11695                        ("cond_cmp", 1, 2),
11696                        ("cond_bare", 1, 2),
11697                        ("cond3", 2, 3),
11698                        ("nested", 4, 3),
11699                        ("cond_no_default", 3, 4),
11700                    ],
11701                );
11702            },
11703        );
11704    }
11705
11706    // Every clause construct pays one condition per clause (#1531), as
11707    // `Cyclomatic` counts one decision per clause through the same gate.
11708    // `case` and `with` paid once per construct and the others not at
11709    // all, so a two-clause `case` scored 1 against Ruby's
11710    // `case x; when 1; when 2; end`, Go's `switch` and Rust's `match`
11711    // at 2 (measured).
11712    //
11713    // Expected values come from cyclomatic, which this fix did not move:
11714    // every row sits at `cyclomatic - 1`. Paths only one row exercises
11715    // (§11): `case_default` the free `_ ->`, `case_guard` a guarded
11716    // clause's flat one beside its `>`, `rescue_in` a sole pattern that
11717    // scores itself (the `in` arm), `rescue_alias` a sole pattern the
11718    // clause slot pays for, `catch_two` a multi-pattern clause, `recv` a
11719    // `receive`'s `after` timeout clause — free since #1581, as the
11720    // default arm, so `recv` scores what `case2` does — and `with_else`
11721    // the `else` of a `with`, whose clauses pay rather than a Rule 5
11722    // `else`. Its `<-` clause pays one more since #1535, in both metrics.
11723    #[cfg(feature = "elixir")]
11724    #[test]
11725    fn elixir_clause_constructs_pay_per_clause() {
11726        check_func_space::<ElixirParser, _>(
11727            "defmodule Foo do
11728  def case2(x) do
11729    case x do
11730      1 -> :a
11731      2 -> :b
11732    end
11733  end
11734  def case_default(x) do
11735    case x do
11736      1 -> :a
11737      2 -> :b
11738      _ -> :c
11739    end
11740  end
11741  def case_guard(x) do
11742    case x do
11743      y when y > 0 -> :a
11744      _ -> :b
11745    end
11746  end
11747  def with_else(x) do
11748    with {:ok, a} <- g(x) do
11749      a
11750    else
11751      {:error, e} -> e
11752      _ -> nil
11753    end
11754  end
11755  def recv(x) do
11756    receive do
11757      {:a, y} -> y
11758      {:b, z} -> z
11759    after
11760      100 -> x
11761    end
11762  end
11763  def rescue_in() do
11764    try do
11765      g()
11766    rescue
11767      e in RuntimeError -> e
11768    end
11769  end
11770  def rescue_alias() do
11771    try do
11772      g()
11773    rescue
11774      RuntimeError -> 1
11775    end
11776  end
11777  def catch_two() do
11778    try do
11779      g()
11780    catch
11781      :exit, r -> r
11782    end
11783  end
11784end
11785",
11786            "foo.ex",
11787            |space| {
11788                assert_members_score(
11789                    &space.spaces[0],
11790                    &[
11791                        ("case2", 2, 3),
11792                        ("case_default", 2, 3),
11793                        ("case_guard", 2, 3),
11794                        ("with_else", 2, 3),
11795                        ("recv", 2, 3),
11796                        ("rescue_in", 1, 2),
11797                        ("rescue_alias", 1, 2),
11798                        ("catch_two", 1, 2),
11799                    ],
11800                );
11801            },
11802        );
11803    }
11804
11805    // A multi-clause anonymous fn dispatches like a `case`, less its head
11806    // clause, which is the closure's definition (#776): `fn x -> x end`
11807    // has no decision and `fn 0 -> :a; _ -> :b end` one — the trailing
11808    // `_ ->` included, since the head already took the free path.
11809    // Expected: each total equals the closure's cyclomatic decisions.
11810    #[cfg(feature = "elixir")]
11811    #[test]
11812    fn elixir_multi_clause_fn_pays_after_its_head() {
11813        for (source, conditions) in [
11814            ("f = fn x -> x end\n", 0),
11815            ("f = fn 0 -> :a; _ -> :b end\n", 1),
11816            ("f = fn 0 -> :a; 1 -> :b; _ -> :c end\n", 2),
11817        ] {
11818            let metrics =
11819                metrics_verbatim(LANG::Elixir, source.as_bytes(), MetricsOptions::default());
11820            assert_eq!(metrics.abc.conditions_sum(), conditions, "{source}");
11821        }
11822    }
11823
11824    // A typespec's function type (`(any -> any)`) is a `stab_clause` too,
11825    // under a parenthesised `block`, and is type syntax with no decision
11826    // behind it. Cyclomatic counted each one; the shared clause gate
11827    // would have carried that into ABC (#1531). Expected: nothing but the
11828    // unit's base path, as for the typespec `when` in
11829    // `elixir_typespec_when_is_not_a_guard`.
11830    #[cfg(feature = "elixir")]
11831    #[test]
11832    fn elixir_typespec_function_type_is_not_a_clause() {
11833        let metrics = metrics_verbatim(
11834            LANG::Elixir,
11835            b"@spec f(list, (any -> any)) :: list\n@type cb :: (integer, integer -> boolean)\n",
11836            MetricsOptions::default(),
11837        );
11838        assert_eq!(metrics.abc.conditions_sum(), 0);
11839        assert_eq!(metrics.cyclomatic.cyclomatic_sum(), 1);
11840    }
11841
11842    // The keyword form of each clause construct holds its clauses in a
11843    // parenthesised `block` under a `do:` / `else:` / `rescue:` pair, the
11844    // shape a typespec function type also has. Excluding `block` for the
11845    // typespec dropped every keyword-form clause (`ck` read 1/4, and `rk`
11846    // 0/3); each `*k` row must equal its `*b` block-form twin, `dk`'s
11847    // `true ->` included (it was a decision in the keyword form).
11848    #[cfg(feature = "elixir")]
11849    #[test]
11850    fn elixir_keyword_form_clauses_match_block_form() {
11851        let src = "defmodule M do
11852  def ck(x), do: case(x, do: (1 -> :a; 2 -> :b; 3 -> :c))
11853  def cb(x) do
11854    case x do
11855      1 -> :a
11856      2 -> :b
11857      3 -> :c
11858    end
11859  end
11860  def dk(x), do: cond(do: (x > 1 -> :a; x < 0 -> :b; true -> :c))
11861  def db(x) do
11862    cond do
11863      x > 1 -> :a
11864      x < 0 -> :b
11865      true -> :c
11866    end
11867  end
11868  def rk(), do: receive(do: ({:a, y} -> y; {:b, z} -> z))
11869  def rb() do
11870    receive do
11871      {:a, y} -> y
11872      {:b, z} -> z
11873    end
11874  end
11875  def tk(), do: try(do: g(), rescue: (e in RuntimeError -> e; _ -> 2))
11876  def tb() do
11877    try do
11878      g()
11879    rescue
11880      e in RuntimeError -> e
11881      _ -> 2
11882    end
11883  end
11884  def wk(x), do: with({:ok, a} <- x, do: a, else: ({:error, e} -> e; _ -> 0))
11885  def wb(x) do
11886    with {:ok, a} <- x do
11887      a
11888    else
11889      {:error, e} -> e
11890      _ -> 0
11891    end
11892  end
11893end
11894";
11895        assert_fixture_spells::<ElixirParser>(
11896            src,
11897            "foo.ex",
11898            &[
11899                (Elixir::Block as u16, 5, "the five keyword-form sections"),
11900                (Elixir::StabClause as u16, 24, "clauses, 12 per spelling"),
11901            ],
11902        );
11903        check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
11904            assert_members_score(
11905                &space.spaces[0],
11906                &[
11907                    ("ck", 3, 4),
11908                    ("cb", 3, 4),
11909                    ("dk", 2, 3),
11910                    ("db", 2, 3),
11911                    ("rk", 2, 3),
11912                    ("rb", 2, 3),
11913                    ("tk", 1, 2),
11914                    ("tb", 1, 2),
11915                    ("wk", 2, 3),
11916                    ("wb", 2, 3),
11917                ],
11918            );
11919        });
11920    }
11921
11922    // Fitzpatrick Rule 5 counts the `else` of an `if` / `unless` (#1531),
11923    // in both of Elixir's spellings. Expected values are the twins':
11924    // Ruby's `if b … else … end`, Java's `if (b) {} else {}`, Python's
11925    // and Go's each score 2 against cyclomatic 2 (measured), one over
11926    // parity by the documented `else` arm. `kw_other` is the control
11927    // that the `else:` key is read only on an `if` / `unless`: on any
11928    // other call it is an ordinary keyword argument.
11929    #[cfg(feature = "elixir")]
11930    #[test]
11931    fn elixir_if_else_pays_rule_5() {
11932        check_func_space::<ElixirParser, _>(
11933            "defmodule Foo do
11934  def block(b) do
11935    if b do
11936      1
11937    else
11938      2
11939    end
11940  end
11941  def kw(b), do: if(b, do: 1, else: 2)
11942  def unless_block(b) do
11943    unless b do
11944      1
11945    else
11946      2
11947    end
11948  end
11949  def cmp(x) do
11950    if x > 5 do
11951      1
11952    else
11953      2
11954    end
11955  end
11956  def no_else(b) do
11957    if b do
11958      1
11959    end
11960  end
11961  def kw_no_else(b), do: if(b, do: 1)
11962  def kw_other(b), do: g(b, else: 1)
11963end
11964",
11965            "foo.ex",
11966            |space| {
11967                assert_members_score(
11968                    &space.spaces[0],
11969                    &[
11970                        ("block", 2, 2),
11971                        ("kw", 2, 2),
11972                        ("unless_block", 2, 2),
11973                        ("cmp", 2, 2),
11974                        ("no_else", 1, 2),
11975                        ("kw_no_else", 1, 2),
11976                        ("kw_other", 0, 1),
11977                    ],
11978                );
11979            },
11980        );
11981    }
11982
11983    // `for` is a comprehension/loop, NOT in the issue's condition
11984    // list. It is still a Call so it contributes one branch, but no
11985    // condition.
11986    #[cfg(feature = "elixir")]
11987    #[test]
11988    fn elixir_for_is_branch_not_condition() {
11989        check_metrics::<ElixirParser>(
11990            "defmodule Foo do\n  def f(xs) do\n    for x <- xs, do: x * 2\n  end\nend\n",
11991            "foo.ex",
11992            |metric| {
11993                assert_eq!(metric.abc.conditions_sum(), 0);
11994                insta::assert_json_snapshot!(metric.abc);
11995            },
11996        );
11997    }
11998
11999    // Mixed shape, verified by hand: defmodule Call + def Call + if Call
12000    // + Call to side_effect/0 + assignment `x = 1` + comparison `x > 0`.
12001    // - Assignments: `x = 1` → A = 1.
12002    // - Branches: `defmodule` and `def` are declarative and excluded;
12003    //   `if` Call + `side_effect()` Call → 2 Calls, plus 0 `|>` → B = 2.
12004    // - Conditions: `x > 0` → 1; the `if` predicate slot it fills adds
12005    //   nothing more (#1527) → C = 1.
12006    #[cfg(feature = "elixir")]
12007    #[test]
12008    fn elixir_mixed_abc() {
12009        check_metrics::<ElixirParser>(
12010            "defmodule Foo do\n  def f do\n    x = 1\n    if x > 0 do\n      side_effect()\n    end\n  end\nend\n",
12011            "foo.ex",
12012            |metric| {
12013                assert_eq!(metric.abc.assignments_sum(), 1);
12014                assert_eq!(metric.abc.branches_sum(), 2);
12015                assert_eq!(metric.abc.conditions_sum(), 1);
12016                insta::assert_json_snapshot!(metric.abc);
12017            },
12018        );
12019    }
12020
12021    #[cfg(feature = "elixir")]
12022    #[test]
12023    fn elixir_unary_conditions_in_chain() {
12024        // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
12025        // `&&` / `||` chain is one condition. For `if a && b || c` the
12026        // walker adds a, b, c → 3, and the `if` predicate slot adds
12027        // nothing because the chain already scored it (#1527). expected:
12028        // 3, as Ruby and Java score the same `if`, against cyclomatic 4
12029        // (base 1 + if + && + ||).
12030        check_metrics::<ElixirParser>(
12031            "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",
12032            "foo.ex",
12033            |metric| {
12034                assert_eq!(metric.abc.conditions_sum(), 3);
12035            },
12036        );
12037    }
12038
12039    // Elixir spells negation two ways and the operand peel once
12040    // recognised only `!`, so `a && not b` scored 3 where `a && !b`
12041    // scored 4 — the `not` operand reached no terminal and vanished.
12042    // Both forms now read the same. `not` is the stricter of the two (it
12043    // raises on a non-boolean operand where `!` accepts any truthy
12044    // value), so it is at least as good a proof that what it wraps is
12045    // boolean, which is all the walker's flag claims.
12046    //
12047    // Both members in one fixture because the claim is that they agree:
12048    // asserting either alone says nothing about the pair (§11), and a
12049    // shared `check_metrics` total could not tell 3 + 4 from 4 + 3.
12050    //
12051    // `assert_members_score` rather than the parity-asserting sibling:
12052    // each member scores its two `&&` operands and nothing for the `if`
12053    // slot the chain fills (#1527) — 2 against cyclomatic
12054    // `base 1 + if + &&` = 3, as Ruby's `if a && !b` does.
12055    #[cfg(feature = "elixir")]
12056    #[test]
12057    fn elixir_keyword_not_negates_like_bang() {
12058        check_func_space::<ElixirParser, _>(
12059            "defmodule Foo do\n  def kw(a, b) do\n    if a && not b do\n      IO.puts(\"x\")\n    end\n  end\n  def bang(a, b) do\n    if a && !b do\n      IO.puts(\"x\")\n    end\n  end\nend\n",
12060            "foo.ex",
12061            |space| {
12062                assert_members_score(&space.spaces[0], &[("kw", 2, 3), ("bang", 2, 3)]);
12063            },
12064        );
12065    }
12066
12067    #[cfg(feature = "elixir")]
12068    #[test]
12069    fn elixir_comparison_operands_add_nothing() {
12070        // Isolation check: comparison operands of a `&&` chain are nested
12071        // `binary_operator` nodes, not bare boolean leaves, so the walker
12072        // adds nothing. expected: 2 = `>` (1) + `>` (1); the `&&` walker
12073        // and the `if` slot the chain fills contribute 0 (#1527).
12074        check_metrics::<ElixirParser>(
12075            "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",
12076            "foo.ex",
12077            |metric| {
12078                assert_eq!(metric.abc.conditions_sum(), 2);
12079            },
12080        );
12081    }
12082
12083    #[cfg(feature = "elixir")]
12084    #[test]
12085    fn elixir_keyword_and_or_chain_counts_operands() {
12086        // The keyword forms `and` / `or` get the same Rule 9 treatment as
12087        // `&&` / `||`. expected: 3 = operands a, b, c; the `if` slot the
12088        // chain fills adds nothing (#1527).
12089        check_metrics::<ElixirParser>(
12090            "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",
12091            "foo.ex",
12092            |metric| {
12093                assert_eq!(metric.abc.conditions_sum(), 3);
12094            },
12095        );
12096    }
12097
12098    // Sigil delimiter choice must not move ABC: `~s<hi>` and `~s(hi)`
12099    // are the same value spelled differently, but the `<` / `>`
12100    // delimiter tokens carry the comparison kind ids, so the unguarded
12101    // condition arm scored `~s<hi>` as 2 conditions and `~s(hi)` as 0.
12102    // The parent-is-`Sigil` guard (mirroring the Halstead getter's,
12103    // #1256) suppresses the delimiter case. expected, for each
12104    // spelling: A = 1 (the `x =` pattern match), B = 0 (a bare sigil
12105    // is not a `Call` node — verified by AST dump: `binary_operator`
12106    // wrapping `identifier`, `=`, `sigil`), C = 0 (no comparison, no
12107    // guard, no keyword Call).
12108    #[cfg(feature = "elixir")]
12109    #[test]
12110    fn elixir_sigil_delimiter_choice_is_abc_invariant() {
12111        for src in ["x = ~s<hi>\n", "x = ~s(hi)\n"] {
12112            check_metrics::<ElixirParser>(src, "foo.ex", |metric| {
12113                assert_eq!(metric.abc.assignments_sum(), 1);
12114                assert_eq!(metric.abc.branches_sum(), 0);
12115                assert_eq!(metric.abc.conditions_sum(), 0);
12116            });
12117        }
12118    }
12119
12120    // Control for the guard above: `<` *outside* a sigil is a genuine
12121    // comparison and must keep counting even with a `<`-delimited
12122    // sigil in the same unit. expected: A = 2 (`x =`, `y =`), C = 1
12123    // (only `a < b`; the sigil's `<` / `>` delimiters are guarded).
12124    #[cfg(feature = "elixir")]
12125    #[test]
12126    fn elixir_lt_comparison_still_counts_beside_sigil() {
12127        check_metrics::<ElixirParser>("x = ~s<hi>\ny = a < b\n", "foo.ex", |metric| {
12128            assert_eq!(metric.abc.assignments_sum(), 2);
12129            assert_eq!(metric.abc.conditions_sum(), 1);
12130        });
12131    }
12132
12133    // #1297's sweep converted six grammars from a denylist of
12134    // non-comparison `<` / `>` parents to an allowlist and recorded
12135    // Elixir as already covered by its sigil guard. It was not: an
12136    // operator *named* rather than applied — the capture `&</2` and the
12137    // qualified call `Kernel.<(a, b)` — puts the bare token under an
12138    // `operator_identifier`, which no denylist entry mentioned, so each
12139    // scored a condition against zero decisions. Same shape as the C#
12140    // `operator <` declaration the sweep did fix.
12141    //
12142    // Each function is its own space and its own row, so a fix that
12143    // reached one spelling and not the other fails here. `max/2` is the
12144    // control: a qualified call whose name is not an operator was
12145    // always 0, so a guard that merely stopped counting `<` everywhere
12146    // would pass this row and fail the two below it.
12147    //
12148    // The six equality and ordering tokens share the gate (#1531): they
12149    // sat on an ungated arm, so `&==/2` and `Kernel.==(a, b)` scored a
12150    // condition each and `caps` five. `eq` is their applied control.
12151    #[cfg(feature = "elixir")]
12152    #[test]
12153    fn elixir_operator_identifier_is_not_a_condition() {
12154        check_func_space::<ElixirParser, _>(
12155            "defmodule M do\n\
12156               def asc(l), do: Enum.sort(l, &</2)\n\
12157               def lt(a, b), do: Kernel.<(a, b)\n\
12158               def big(a, b), do: Kernel.max(a, b)\n\
12159               def cmp(a, b), do: a < b\n\
12160               def eq_cap(l), do: Enum.sort(l, &==/2)\n\
12161               def eq_remote(a, b), do: Kernel.==(a, b)\n\
12162               def caps(), do: {&===/2, &!=/2, &!==/2, &<=/2, &>=/2}\n\
12163               def eq(a, b), do: a == b\n\
12164             end\n",
12165            "foo.ex",
12166            |space| {
12167                let module = &space.spaces[0];
12168                let scored: Vec<(String, u64)> = module
12169                    .spaces
12170                    .iter()
12171                    .map(|f| {
12172                        (
12173                            f.name.clone().unwrap_or_default(),
12174                            f.metrics.abc.conditions(),
12175                        )
12176                    })
12177                    .collect();
12178                assert_eq!(
12179                    scored,
12180                    vec![
12181                        ("asc".to_owned(), 0),
12182                        ("lt".to_owned(), 0),
12183                        ("big".to_owned(), 0),
12184                        ("cmp".to_owned(), 1),
12185                        ("eq_cap".to_owned(), 0),
12186                        ("eq_remote".to_owned(), 0),
12187                        ("caps".to_owned(), 0),
12188                        ("eq".to_owned(), 1),
12189                    ],
12190                );
12191            },
12192        );
12193    }
12194
12195    // ----- C++ -----
12196
12197    #[cfg(feature = "cpp")]
12198    #[test]
12199    fn cpp_empty_unit_zero() {
12200        // No code → A=B=C=0. Wires up the trait and exercises the
12201        // per-language compute reachability.
12202        check_metrics::<CppParser>("", "empty.cpp", |metric| {
12203            assert_eq!(metric.abc.assignments_sum(), 0);
12204            assert_eq!(metric.abc.branches_sum(), 0);
12205            assert_eq!(metric.abc.conditions_sum(), 0);
12206            insta::assert_json_snapshot!(metric.abc);
12207        });
12208    }
12209
12210    #[cfg(feature = "cpp")]
12211    #[test]
12212    fn cpp_plain_and_compound_assignments_count() {
12213        // `int x = 0` is an `init_declarator` carrying an `=` token
12214        // and counts as 1 (post-#393: the literal Fitzpatrick rule
12215        // counts every `=` operator, matching the JS impl's
12216        // `let x = 5` treatment). `x = 5`, `x += 2`, `x = 7` all
12217        // parse as `assignment_expression` → 3. Total A = 4.
12218        check_metrics::<CppParser>(
12219            "void f() { int x = 0; x = 5; x += 2; x = 7; }",
12220            "foo.cpp",
12221            |metric| {
12222                assert_eq!(metric.abc.assignments_sum(), 4);
12223                assert_eq!(metric.abc.branches_sum(), 0);
12224                assert_eq!(metric.abc.conditions_sum(), 0);
12225                insta::assert_json_snapshot!(metric.abc);
12226            },
12227        );
12228    }
12229
12230    #[cfg(feature = "cpp")]
12231    #[test]
12232    fn cpp_increment_and_decrement_count_as_assignment() {
12233        // `x++` / `--x` / prefix and postfix forms each parse as
12234        // `update_expression` and count as 1 assignment per
12235        // Fitzpatrick — 4. `int x = 0` (init_declarator with `=`)
12236        // adds 1 (post-#393). Total A = 5.
12237        check_metrics::<CppParser>(
12238            "void f() { int x = 0; x++; --x; ++x; x--; }",
12239            "foo.cpp",
12240            |metric| {
12241                assert_eq!(metric.abc.assignments_sum(), 5);
12242                insta::assert_json_snapshot!(metric.abc);
12243            },
12244        );
12245    }
12246
12247    #[cfg(feature = "cpp")]
12248    #[test]
12249    fn cpp_init_declarators_count_as_assignments() {
12250        // Issue #393 regression: `int a=1;`, `int b=2;`, `int c=a+b;`,
12251        // `int d=0;` are all `init_declarator` nodes with `=` → 4
12252        // assignments. `d=5;` is one plain `assignment_expression`,
12253        // `d+=1;` is one compound. Total A = 4 + 1 + 1 = 6.
12254        check_metrics::<CppParser>(
12255            "void f() { int a=1; int b=2; int c=a+b; int d=0; d=5; d+=1; }",
12256            "foo.cpp",
12257            |metric| {
12258                assert_eq!(metric.abc.assignments_sum(), 6);
12259                insta::assert_json_snapshot!(metric.abc);
12260            },
12261        );
12262    }
12263
12264    #[cfg(feature = "cpp")]
12265    #[test]
12266    fn cpp_declaration_without_initializer_does_not_count() {
12267        // `int a;` parses as a plain declarator inside `declaration`,
12268        // NOT an `init_declarator` (the latter only appears when an
12269        // initializer is present). Regression test for issue #393:
12270        // un-initialised declarations contribute zero to A.
12271        check_metrics::<CppParser>("void f() { int a; a = 5; }", "foo.cpp", |metric| {
12272            // Only `a = 5` (assignment_expression) → A = 1.
12273            assert_eq!(metric.abc.assignments_sum(), 1);
12274            insta::assert_json_snapshot!(metric.abc);
12275        });
12276    }
12277
12278    #[cfg(feature = "cpp")]
12279    #[test]
12280    fn cpp_init_declarator_brace_paren_init_does_not_count() {
12281        // `init_declarator` has two grammar forms: `declarator = value`
12282        // (the `=` form) and `declarator argument_list_or_initializer_list`
12283        // (the `int x(5);` / `int x{5};` direct-init forms). Only the
12284        // first form contains an `=` token, so only it should count.
12285        // Regression test pinning that distinction so that
12286        // refactorings of the init_declarator arm don't accidentally
12287        // start counting direct-init too.
12288        check_metrics::<CppParser>(
12289            "void f() { int x(5); int y{7}; x = 1; }",
12290            "foo.cpp",
12291            |metric| {
12292                // Only `x = 1` (assignment_expression) → A = 1.
12293                assert_eq!(metric.abc.assignments_sum(), 1);
12294                insta::assert_json_snapshot!(metric.abc);
12295            },
12296        );
12297    }
12298
12299    #[cfg(feature = "cpp")]
12300    #[test]
12301    fn cpp_calls_are_branches() {
12302        // Free call + member-fn call (parses as `call_expression` with
12303        // a `field_expression` callee) + `new` allocation. All three
12304        // are branches → B = 3. `auto* p = new int(5)` is also an
12305        // `init_declarator` with `=` so it contributes one assignment
12306        // (post-#393); the snapshot pins that magnitude.
12307        check_metrics::<CppParser>(
12308            "struct S { void m(); }; void g(); void f() { g(); S s; s.m(); auto* p = new int(5); }",
12309            "foo.cpp",
12310            |metric| {
12311                assert_eq!(metric.abc.branches_sum(), 3);
12312                assert_eq!(metric.abc.assignments_sum(), 1);
12313                insta::assert_json_snapshot!(metric.abc);
12314            },
12315        );
12316    }
12317
12318    #[cfg(feature = "cpp")]
12319    #[test]
12320    fn cpp_comparisons_count_conditions() {
12321        // `<`, `>`, `<=`, `>=`, `==`, `!=`, and the C++20 spaceship
12322        // `<=>` each contribute one condition. The `||` short-
12323        // circuits add 0 (Fitzpatrick Rule 5, issue #395). Six
12324        // comparisons in the `||` chain plus `<=>` (1) plus the
12325        // outer `== 0` (1) → C = 8.
12326        check_metrics::<CppParser>(
12327            "#include <compare>\n\
12328             bool f(int a, int b) {\n\
12329                 return a < b || a > b || a <= b || a >= b || a == b || a != b || (a <=> b) == 0;\n\
12330             }\n",
12331            "foo.cpp",
12332            |metric| {
12333                // `<`, `>`, `<=`, `>=`, `==`, `!=` → 6 comparisons
12334                // from the chained `||` expression. `(a <=> b) == 0`
12335                // adds the spaceship `<=>` (1) + the outer `== 0`
12336                // (1) → 8 total. The six `||` short-circuits add 0
12337                // (Fitzpatrick Rule 5; issue #395).
12338                assert_eq!(metric.abc.conditions_sum(), 8);
12339                insta::assert_json_snapshot!(metric.abc);
12340            },
12341        );
12342    }
12343
12344    #[cfg(feature = "cpp")]
12345    #[test]
12346    fn cpp_short_circuit_ops_not_counted_directly() {
12347        // `&&` and `||` do NOT count on their own (see the
12348        // module-level `Stats` doc-comment; #395). Phase-2 walker
12349        // counts each operand of a logical chain once (#403), but
12350        // when every operand is itself a relational expression
12351        // (`a == b`, `a > 0`, `b < 0`) the walker doesn't add
12352        // anything on top of the existing comparison-token tally
12353        // — a relational sub-expression scores itself
12354        // (`cpp_condition_scores_itself`), so its chain slot pays
12355        // nothing on top.
12356        check_metrics::<CppParser>(
12357            "bool f(int a, int b) { return a == b && a > 0 || b < 0; }",
12358            "foo.cpp",
12359            |metric| {
12360                // == 1, > 1, < 1; the walker on && and || finds
12361                // BinaryExpression operands (not terminal-bool) and
12362                // adds nothing. Total: 3.
12363                assert_eq!(metric.abc.conditions_sum(), 3);
12364                insta::assert_json_snapshot!(metric.abc);
12365            },
12366        );
12367    }
12368
12369    #[cfg(feature = "cpp")]
12370    #[test]
12371    fn cpp_generic_brackets_not_conditions() {
12372        // `<` / `>` in `std::vector<int>` are `template_argument_list`
12373        // delimiters, NOT comparison operators. The `binary_expression`
12374        // parent check must filter them out → C = 0.
12375        check_metrics::<CppParser>(
12376            "#include <vector>\nstd::vector<int> f() { return std::vector<int>{}; }",
12377            "foo.cpp",
12378            |metric| {
12379                assert_eq!(metric.abc.conditions_sum(), 0);
12380                insta::assert_json_snapshot!(metric.abc);
12381            },
12382        );
12383    }
12384
12385    #[cfg(feature = "cpp")]
12386    #[test]
12387    fn cpp_else_and_ternary_count_conditions() {
12388        // `if (cond) ... else ...` + ternary `cond ? a : b`. The
12389        // `if`-keyword is NOT a condition (its condition is the
12390        // comparison inside, which counts separately). `else` adds 1,
12391        // `?` adds 1. Two comparisons (`a > b`, `b < 0`) → 2. Total = 4.
12392        check_metrics::<CppParser>(
12393            "int f(int a, int b) {\n\
12394                 if (a > b) { return a; } else { return b; }\n\
12395                 return (b < 0) ? -b : b;\n\
12396             }\n",
12397            "foo.cpp",
12398            |metric| {
12399                assert_eq!(metric.abc.conditions_sum(), 4);
12400                insta::assert_json_snapshot!(metric.abc);
12401            },
12402        );
12403    }
12404
12405    // Issue #1102. A ternary's condition and both branch operands are
12406    // Fitzpatrick Rule 9 unary conditions, exactly as `java_walk_ternary`
12407    // has always counted them. Before the fix the C family scored
12408    // `a ? !b : !c` as 1 — the `?` token alone — against Java's 4.
12409    #[cfg(feature = "cpp")]
12410    #[test]
12411    fn cpp_ternary_operand_slots_count_as_unary_conditions() {
12412        // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4.
12413        check_metrics::<CppParser>("void f() { x = a ? !b : !c; }", "foo.cpp", |metric| {
12414            assert_eq!(metric.abc.conditions_sum(), 4);
12415        });
12416        // No-double-count pin: `?` (1) + `>` (1) = 2, unchanged by the
12417        // fix. The parenthesised condition peels to a
12418        // `binary_expression`, which scores itself, and
12419        // neither branch is negated — the `!` is the type-free proxy for
12420        // "this operand is boolean", so an unnegated branch contributes
12421        // nothing.
12422        check_metrics::<CppParser>("void f() { x = (a > 0) ? b : -b; }", "foo.cpp", |metric| {
12423            assert_eq!(metric.abc.conditions_sum(), 2);
12424        });
12425        // Nested: outer `?` (1) + outer condition `a` (1) + inner `?`
12426        // (1) + inner condition `b` (1) = 4. The outer consequence is
12427        // the inner ternary — which scores itself, and is no
12428        // paren / `!` wrapper — so it adds nothing on its own and the
12429        // inner one is reached by the walk, not by descent.
12430        check_metrics::<CppParser>("void f() { x = a ? b ? c : d : e; }", "foo.cpp", |metric| {
12431            assert_eq!(metric.abc.conditions_sum(), 4);
12432        });
12433        // A negated *condition* is the input where the slot must peel a
12434        // `!`: `cpp_wrapper_operand` takes `!a` down to `a`, which does
12435        // not score itself, so the slot pays once. Every other condition
12436        // fixture in this file wraps a comparison, which scores itself
12437        // and so pays 0 from the slot. `?` (1) + `!a` (1) = 2.
12438        check_metrics::<CppParser>("void f() { x = !a ? b : c; }", "foo.cpp", |metric| {
12439            assert_eq!(metric.abc.conditions_sum(), 2);
12440        });
12441    }
12442
12443    // The GNU short-ternary `a ?: b` elides the consequence, so the
12444    // C-family grammar marks that field optional and the alternative
12445    // lands at child(3) rather than child(4). Addressing the operand
12446    // slots by grammar field name — never by index — is what keeps `!b`
12447    // counted here; a fixed `child(4)` reads `None` and scores 2.
12448    #[cfg(feature = "cpp")]
12449    #[test]
12450    fn cpp_elided_ternary_consequence_still_walks_the_alternative() {
12451        // `?` (1) + condition `a` (1) + `!b` (1) = 3.
12452        check_metrics::<CppParser>("void f() { x = a ?: !b; }", "foo.cpp", |metric| {
12453            assert_eq!(metric.abc.conditions_sum(), 3);
12454        });
12455    }
12456
12457    // `cpp_walk_ternary` is shared by the C, ObjC, and Mozcpp ABC impls
12458    // exactly as `cpp_count_condition` is, so each needs its own
12459    // dispatcher arm. Mozcpp owns no file extension and so gets no
12460    // integration-snapshot coverage at all — this parity assertion is
12461    // its only guard.
12462    //
12463    // The expected value is *derived from the C++ run*, not hardcoded,
12464    // so the four languages cannot silently drift apart if the C++
12465    // expectation ever legitimately moves.
12466    #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12467    #[test]
12468    fn c_family_ternary_operand_slots_agree_with_cpp() {
12469        #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12470        const SRC: &str = "void f() { x = a ? !b : !c; }\n";
12471        let conditions = abc_conditions;
12472
12473        let cpp = conditions(LANG::Cpp, SRC);
12474        // Non-degenerate: a zeroed reference would make every
12475        // comparison below vacuous.
12476        assert_eq!(cpp, 4, "C++ reference value for `a ? !b : !c`");
12477
12478        assert_eq!(conditions(LANG::C, SRC), cpp, "C must match C++");
12479        assert_eq!(conditions(LANG::Mozcpp, SRC), cpp, "Mozcpp must match C++");
12480        assert_eq!(
12481            conditions(
12482                LANG::Objc,
12483                "@implementation Foo\n\
12484                 - (void)bar {\n\
12485                     x = a ? !b : !c;\n\
12486                 }\n\
12487                 @end\n",
12488            ),
12489            cpp,
12490            "ObjC must match C++"
12491        );
12492    }
12493
12494    // Issue #1276, C-family half. `cpp_walk_for_statement` is the
12495    // `for` header's counterpart to the `if` / `while` arms: the slot
12496    // is a bare expression rather than a `condition_clause`, read by
12497    // the same slot rule `cpp_walk_ternary` uses. Every fixture is a
12498    // shape only that walker can classify —
12499    // a comparison-shaped condition proves nothing, the `<` token arm
12500    // counts it either way (grammar-dispatch §11).
12501    #[cfg(feature = "cpp")]
12502    #[test]
12503    fn cpp_for_condition_slot_counts_unary_conditions() {
12504        // Bare identifier: the whole condition, no operator token.
12505        check_metrics::<CppParser>("void f(int a) { for (; a; ) {} }", "foo.cpp", |metric| {
12506            assert_eq!(metric.abc.conditions_sum(), 1);
12507        });
12508        // Negation: the slot peels the `!` (`cpp_wrapper_operand`) and
12509        // pays for the operand beneath it.
12510        check_metrics::<CppParser>("void f(int a) { for (; !a; ) {} }", "foo.cpp", |metric| {
12511            assert_eq!(metric.abc.conditions_sum(), 1);
12512        });
12513        // Parentheses: counted because the slot peels them
12514        // (`cpp_wrapper_operand`) — the path #1276 found dead.
12515        check_metrics::<CppParser>("void f(int a) { for (; (a); ) {} }", "foo.cpp", |metric| {
12516            assert_eq!(metric.abc.conditions_sum(), 1);
12517        });
12518        // No-double-count pin: the `<` token arm already counted this
12519        // shape before the fix and the walker must not add a second.
12520        // The two assignments confirm the header parsed as the
12521        // three-clause form rather than degenerating.
12522        check_metrics::<CppParser>(
12523            "void f(int n) { for (int i = 0; i < n; i++) {} }",
12524            "foo.cpp",
12525            |metric| {
12526                assert_eq!(metric.abc.conditions_sum(), 1);
12527                assert_eq!(metric.abc.assignments_sum(), 2);
12528            },
12529        );
12530        // Empty condition: no `condition` field, no decision, zero.
12531        check_metrics::<CppParser>("void f() { for (;;) { break; } }", "foo.cpp", |metric| {
12532            assert_eq!(metric.abc.conditions_sum(), 0);
12533        });
12534    }
12535
12536    // `cpp_walk_for_statement` is shared by the C, ObjC and Mozcpp ABC
12537    // impls the way `cpp_walk_ternary` is, so each needs its own
12538    // dispatcher arm — and Mozcpp, which owns no file extension, has no
12539    // integration-snapshot coverage at all, making this its only guard.
12540    // The expected value is derived from the C++ run rather than
12541    // hardcoded, so the four cannot silently drift apart.
12542    #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12543    #[test]
12544    fn c_family_for_condition_slot_agrees_with_cpp() {
12545        #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12546        const SRC: &str = "void f(int a) { for (; !a; ) {} }\n";
12547        let conditions = abc_conditions;
12548
12549        let cpp = conditions(LANG::Cpp, SRC);
12550        // Non-degenerate: a zeroed reference makes every comparison
12551        // below vacuous.
12552        assert_eq!(cpp, 1, "C++ reference value for `for (; !a; )`");
12553
12554        assert_eq!(conditions(LANG::C, SRC), cpp, "C must match C++");
12555        assert_eq!(conditions(LANG::Mozcpp, SRC), cpp, "Mozcpp must match C++");
12556        assert_eq!(
12557            conditions(
12558                LANG::Objc,
12559                "@implementation Foo\n\
12560                 - (void)bar {\n\
12561                     for (; !a; ) {}\n\
12562                 }\n\
12563                 @end\n",
12564            ),
12565            cpp,
12566            "ObjC must match C++"
12567        );
12568    }
12569
12570    #[cfg(feature = "cpp")]
12571    #[test]
12572    fn cpp_switch_cases_count_default_excluded() {
12573        // `case 1`, `case 2` → 2 conditions. `default` is intentionally
12574        // excluded (the unconditional fallthrough, mirroring cyclomatic's
12575        // `Case`-only count). Since #469 every C-family language —
12576        // Java, C#, Groovy, JS, TS — agrees on this; C++ already did.
12577        // C = 2.
12578        check_metrics::<CppParser>(
12579            "void f(int x) {\n\
12580                 switch (x) {\n\
12581                     case 1: break;\n\
12582                     case 2: break;\n\
12583                     default: break;\n\
12584                 }\n\
12585             }\n",
12586            "foo.cpp",
12587            |metric| {
12588                assert_eq!(metric.abc.conditions_sum(), 2);
12589                insta::assert_json_snapshot!(metric.abc);
12590            },
12591        );
12592    }
12593
12594    #[cfg(feature = "cpp")]
12595    #[test]
12596    fn cpp_try_catch_count_conditions() {
12597        // `try` and `catch` each add one condition (Fitzpatrick's rule;
12598        // Java's impl above counts them too).
12599        check_metrics::<CppParser>(
12600            "void f() { try { } catch (int) { } catch (...) { } }",
12601            "foo.cpp",
12602            |metric| {
12603                // 1 `try` + 2 `catch` arms = 3.
12604                assert_eq!(metric.abc.conditions_sum(), 3);
12605                insta::assert_json_snapshot!(metric.abc);
12606            },
12607        );
12608    }
12609
12610    #[cfg(feature = "cpp")]
12611    #[test]
12612    fn cpp_complex_function_abc() {
12613        // Mixed-shape regression: assignments, calls, conditions,
12614        // ternary, switch, new. Verified by hand:
12615        // - assignments: `int x = 0` (init_declarator with `=`),
12616        //   `x = 5`, `x += 2`, `x++`, `x = (a > b) ? a : b`, `x = b`,
12617        //   `auto* p = new int(5)` (init_declarator with `=`) → A = 7
12618        //   (post-#393: every `=` in an init_declarator counts).
12619        // - branches: `f(a, b)` self-call + `new int(5)` → B = 2.
12620        // - conditions: `a == b` (1) + `a > 0` (1) inside the if;
12621        //   `&&` itself is NOT a condition (Fitzpatrick Rule 5,
12622        //   issue #395). `a > b` (1) + `?` (1) in the ternary.
12623        //   `else` (1, from the `else if` keyword) + `a < b` (1)
12624        //   in the else-if. `!x` contributes 1 via the unary-
12625        //   conditional walker (Fitzpatrick Rule 9, issue #403):
12626        //   the `||` walker treats `!x` as a unary boolean operand
12627        //   and counts the wrapped Identifier once. `case 1`,
12628        //   `case 2` → 2. `default` excluded. Total C = 9.
12629        check_metrics::<CppParser>(
12630            "int f(int a, int b) {\n\
12631                 int x = 0;\n\
12632                 x = 5;\n\
12633                 x += 2;\n\
12634                 x++;\n\
12635                 if (a == b && a > 0) {\n\
12636                     x = (a > b) ? a : b;\n\
12637                 } else if (a < b || !x) {\n\
12638                     x = b;\n\
12639                 }\n\
12640                 switch (x) {\n\
12641                     case 1: break;\n\
12642                     case 2: break;\n\
12643                     default: break;\n\
12644                 }\n\
12645                 auto* p = new int(5);\n\
12646                 return f(a, b);\n\
12647             }\n",
12648            "foo.cpp",
12649            |metric| {
12650                assert_eq!(metric.abc.assignments_sum(), 7);
12651                assert_eq!(metric.abc.branches_sum(), 2);
12652                assert_eq!(metric.abc.conditions_sum(), 9);
12653                insta::assert_json_snapshot!(metric.abc);
12654            },
12655        );
12656    }
12657
12658    #[cfg(feature = "cpp")]
12659    #[test]
12660    fn cpp_if_multiple_conditions() {
12661        // Fitzpatrick Rule 9 walker (issue #403): each operand of a
12662        // `&&` / `||` chain is one condition.
12663        check_metrics::<CppParser>(
12664            "void f(bool a, bool b, bool c, bool d) {\n\
12665             \x20   if (a || b || c || d) {}        // +4c\n\
12666             \x20   if (a && b && c) {}             // +3c\n\
12667             \x20   if (!a && !b) {}                // +2c\n\
12668             }\n",
12669            "foo.cpp",
12670            |metric| {
12671                assert_eq!(metric.abc.conditions_sum(), 9);
12672                insta::assert_json_snapshot!(metric.abc);
12673            },
12674        );
12675    }
12676
12677    #[cfg(feature = "cpp")]
12678    #[test]
12679    fn cpp_while_and_do_while_conditions() {
12680        // Exercise both the WhileStatement and DoStatement arms via
12681        // the walker on the `&&` / `||` tokens inside their parens.
12682        check_metrics::<CppParser>(
12683            "void f(bool a, bool b) {\n\
12684             \x20   while (a || b) {}              // +2c\n\
12685             \x20   do {} while (a && !b);         // +2c\n\
12686             }\n",
12687            "foo.cpp",
12688            |metric| {
12689                assert_eq!(metric.abc.conditions_sum(), 4);
12690                insta::assert_json_snapshot!(metric.abc);
12691            },
12692        );
12693    }
12694
12695    #[cfg(feature = "cpp")]
12696    #[test]
12697    fn cpp_if_constexpr_condition_counts() {
12698        // Regression for the code-review finding: C++ `if constexpr
12699        // (cond)` puts the `constexpr` keyword at child(1) and the
12700        // condition_clause at child(2). Pre-fix, the dispatcher used
12701        // child(1) and counted zero conditions for the `constexpr`
12702        // form. The fix uses `child_by_field_name("condition")`
12703        // which returns the condition_clause regardless of the
12704        // optional `constexpr` keyword.
12705        check_metrics::<CppParser>(
12706            "template <int N> void f() {\n\
12707             \x20   if constexpr (true) { }      // +1c\n\
12708             \x20   if (false) { }               // +1c\n\
12709             }\n",
12710            "foo.cpp",
12711            |metric| {
12712                assert_eq!(metric.abc.conditions_sum(), 2);
12713                insta::assert_json_snapshot!(metric.abc);
12714            },
12715        );
12716    }
12717
12718    #[cfg(feature = "cpp")]
12719    #[test]
12720    fn cpp_cast_expression_in_logical_chain_counts() {
12721        // Regression for findings.md round-2 #1 (C++):
12722        // `if ((bool)ptr && ready) {}` had the `||` walker missing
12723        // the `(bool)ptr` operand because `CastExpression` was not
12724        // in the former `cpp_bool_terminal_kinds!()`. Mirrors C#'s
12725        // former `csharp_bool_terminal_kinds!()`, which listed
12726        // `CastExpression` (lesson 19, #372).
12727        check_metrics::<CppParser>(
12728            "void f(void* ptr, bool ready) { if ((bool)ptr && ready) { } }\n",
12729            "foo.cpp",
12730            |metric| {
12731                // `&&` walker counts both operands: `(bool)ptr` (1)
12732                // and `ready` (1). Total: 2.
12733                assert_eq!(metric.abc.conditions_sum(), 2);
12734                insta::assert_json_snapshot!(metric.abc);
12735            },
12736        );
12737    }
12738
12739    #[cfg(feature = "cpp")]
12740    #[test]
12741    fn cpp_qualified_identifier_condition_counts() {
12742        // Regression for findings.md #3 (C++): tree-sitter-cpp emits
12743        // `qualified_identifier` under four kind_ids (573..576) per
12744        // the production-rule path; runtime kind for `ns::flag` is
12745        // 574 (`QualifiedIdentifier2`). Pre-fix the
12746        // former `cpp_bool_terminal_kinds!()` macro listed neither the
12747        // primary nor any alias, so `if (n::flag) {}` reported zero
12748        // conditions. The slot now pays for every variant, since none
12749        // scores itself (lesson #2).
12750        check_metrics::<CppParser>(
12751            "namespace n { extern bool flag; }\n\
12752             void f() { if (n::flag) { } }\n",
12753            "foo.cpp",
12754            |metric| {
12755                assert_eq!(metric.abc.conditions_sum(), 1);
12756                insta::assert_json_snapshot!(metric.abc);
12757            },
12758        );
12759    }
12760
12761    #[cfg(feature = "cpp")]
12762    #[test]
12763    fn cpp_if_boolean_literal_condition() {
12764        check_metrics::<CppParser>(
12765            "void f() {\n\
12766             \x20   if (true) {}                 // +1c\n\
12767             \x20   if (!false) {}               // +1c\n\
12768             \x20   while (true) {}              // +1c\n\
12769             \x20   do {} while (false);         // +1c\n\
12770             }\n",
12771            "foo.cpp",
12772            |metric| {
12773                assert_eq!(metric.abc.conditions_sum(), 4);
12774                insta::assert_json_snapshot!(metric.abc);
12775            },
12776        );
12777    }
12778
12779    #[cfg(feature = "cpp")]
12780    #[test]
12781    fn cpp_methods_arguments_with_conditions() {
12782        check_metrics::<CppParser>(
12783            "void f(bool a, bool b) {\n\
12784             \x20   m(a, b);                     // +1b\n\
12785             \x20   m(!a, !b);                   // +1b +2c\n\
12786             }\n",
12787            "foo.cpp",
12788            |metric| {
12789                assert_eq!(metric.abc.branches_sum(), 2);
12790                assert_eq!(metric.abc.conditions_sum(), 2);
12791                insta::assert_json_snapshot!(metric.abc);
12792            },
12793        );
12794    }
12795
12796    #[cfg(feature = "cpp")]
12797    #[test]
12798    fn cpp_return_with_conditions() {
12799        check_metrics::<CppParser>(
12800            "bool m1(int z) { return !(z >= 0); }\n\
12801             bool m2(bool x) { return (((!x))); }\n\
12802             bool m3(bool x, bool y) { return x && y; }\n",
12803            "foo.cpp",
12804            |metric| {
12805                // m1: !(z >= 0) → `>=` (1). `!` wraps a paren'd
12806                //     BinaryExpression — inspect_container reaches
12807                //     the inner BinaryExpression and stops, no
12808                //     walker count. +1.
12809                // m2: (((!x))) → ReturnStatement → inspect_container
12810                //     unwraps three parens + one unary → reaches `x`
12811                //     in has_boolean_content=true (seeded by the
12812                //     unary `!`). +1.
12813                // m3: x && y → `&&` walker counts both → +2.
12814                // Sum: 1 + 1 + 2 = 4.
12815                assert_eq!(metric.abc.conditions_sum(), 4);
12816                insta::assert_json_snapshot!(metric.abc);
12817            },
12818        );
12819    }
12820
12821    // #1547: `co_return` is a coroutine's `return`, so ABC reads its value
12822    // the same way. Each row's expected value is its `return` twin's. A
12823    // `co_yield` hands a value back without returning, as the JS family's
12824    // `yield` does, and scores no value slot either.
12825    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
12826    fn assert_co_return_scores_like_return(lang: LANG) {
12827        let source = |body: &str| format!("task f(bool a) {{ {body} }}\n");
12828        let rows = [
12829            ("co_return !a;", "return !a;", 1),
12830            ("co_return /*c*/ !a;", "return /*c*/ !a;", 1),
12831            ("co_return a;", "return a;", 0),
12832            ("co_return;", "return;", 0),
12833        ];
12834        for (coroutine, twin, expected) in rows {
12835            assert_eq!(
12836                abc_conditions(lang, &source(twin)),
12837                expected,
12838                "{lang:?} `{twin}`"
12839            );
12840            assert_eq!(
12841                abc_conditions(lang, &source(coroutine)),
12842                expected,
12843                "{lang:?} `{coroutine}`"
12844            );
12845        }
12846        assert_eq!(
12847            abc_conditions(lang, &source("co_yield !a;")),
12848            0,
12849            "{lang:?} co_yield"
12850        );
12851    }
12852
12853    #[cfg(feature = "cpp")]
12854    #[test]
12855    fn cpp_co_return_value_scores_like_return_1547() {
12856        assert_co_return_scores_like_return(LANG::Cpp);
12857    }
12858
12859    // Mozcpp owns no file extension, so this is its only coverage.
12860    #[cfg(feature = "mozcpp")]
12861    #[test]
12862    fn mozcpp_co_return_value_scores_like_return_1547() {
12863        assert_co_return_scores_like_return(LANG::Mozcpp);
12864    }
12865
12866    #[cfg(feature = "cpp")]
12867    #[test]
12868    fn cpp_short_circuit_with_boolean_literal_operand() {
12869        // `a && true` reports 2 conditions: one for the identifier
12870        // operand, one for the `True` literal operand.
12871        check_metrics::<CppParser>(
12872            "bool f(bool a) { return a && true; }\n",
12873            "foo.cpp",
12874            |metric| {
12875                assert_eq!(metric.abc.conditions_sum(), 2);
12876                insta::assert_json_snapshot!(metric.abc);
12877            },
12878        );
12879    }
12880
12881    #[cfg(feature = "javascript")]
12882    #[test]
12883    fn javascript_empty_unit_zero() {
12884        // No code → A=B=C=0. Wires up the trait and exercises the
12885        // per-language compute reachability.
12886        check_metrics::<JavascriptParser>("", "empty.js", |metric| {
12887            assert_eq!(metric.abc.assignments_sum(), 0);
12888            assert_eq!(metric.abc.branches_sum(), 0);
12889            assert_eq!(metric.abc.conditions_sum(), 0);
12890            insta::assert_json_snapshot!(metric.abc);
12891        });
12892    }
12893
12894    #[cfg(feature = "javascript")]
12895    #[test]
12896    fn javascript_plain_and_compound_assignments_count() {
12897        // `let` / `var` declarations behave like TypeScript: only a
12898        // `const` initializer is suppressed. So `let x = 0` does count as
12899        // A=+1; only `const PI = 3.14` would be elided. Plain `x = 5`,
12900        // `x += 2`, `x = 7` all count → A = 4 total here.
12901        check_metrics::<JavascriptParser>(
12902            "function f() { let x = 0; x = 5; x += 2; x = 7; }",
12903            "foo.js",
12904            |metric| {
12905                assert_eq!(metric.abc.assignments_sum(), 4);
12906                assert_eq!(metric.abc.branches_sum(), 0);
12907                assert_eq!(metric.abc.conditions_sum(), 0);
12908                insta::assert_json_snapshot!(metric.abc);
12909            },
12910        );
12911    }
12912
12913    #[cfg(feature = "javascript")]
12914    #[test]
12915    fn javascript_const_initializer_not_assignment() {
12916        // `const PI = 3.14` must NOT count as an assignment — its `=`
12917        // initialises a `const` binding. `let x = 1` and `var y = 2`
12918        // still count (matches the TS impl: only `const` suppresses).
12919        check_metrics::<JavascriptParser>(
12920            "function f() { const PI = 3.14; let x = 1; var y = 2; x = 9; }",
12921            "foo.js",
12922            |metric| {
12923                // `const PI` suppressed; `let x = 1`, `var y = 2`,
12924                // `x = 9` all count → A = 3.
12925                assert_eq!(metric.abc.assignments_sum(), 3);
12926                insta::assert_json_snapshot!(metric.abc);
12927            },
12928        );
12929    }
12930
12931    #[cfg(feature = "javascript")]
12932    #[test]
12933    fn javascript_asi_const_does_not_suppress_later_assignments() {
12934        // The issue #1277 reproducer verbatim. JavaScript half of the
12935        // cluster documented at
12936        // `typescript_asi_const_does_not_suppress_later_assignments`.
12937        check_metrics::<JavascriptParser>(
12938            "function f() {
12939                const a = 1
12940                x = 2
12941                return x
12942            }",
12943            "foo.js",
12944            |metric| {
12945                // Pre-#1277 this reported 0; the semicolon-terminated
12946                // spelling reported 1.
12947                assert_eq!(metric.abc.assignments_sum(), 1);
12948            },
12949        );
12950    }
12951
12952    #[cfg(feature = "javascript")]
12953    #[test]
12954    fn javascript_nested_arrow_const_does_not_leak() {
12955        // The ASI leak beside a nested space: the arrow body opens its
12956        // own space, so `y = 1` was always counted there, but
12957        // `const f = () => …` has no `;`, so `h`'s sentinel stayed live
12958        // and suppressed `z = 2`. The stack was per-space state that
12959        // `Stats::merge` never carried, so this is the terminator defect
12960        // and not a second route.
12961        check_metrics::<JavascriptParser>(
12962            "function h() {
12963                const f = () => { y = 1 }
12964                z = 2
12965            }",
12966            "foo.js",
12967            |metric| {
12968                // `y = 1` and `z = 2`; the `const` initializer is
12969                // suppressed. Pre-#1277: 1.
12970                assert_eq!(metric.abc.assignments_sum(), 2);
12971            },
12972        );
12973    }
12974
12975    #[cfg(feature = "javascript")]
12976    #[test]
12977    fn javascript_non_declarator_equals_still_count() {
12978        // An `=` that does not belong to a `const` declarator is always
12979        // an assignment: a class `field_definition` initializer, a
12980        // default-parameter `assignment_pattern`, and a destructured
12981        // parameter's defaults, whose climb crosses the same pattern
12982        // layers as a `const` pattern's but ends at `formal_parameters`.
12983        // A predicate that accepted any pattern ancestor as a declarator
12984        // would zero the last three.
12985        check_metrics::<JavascriptParser>(
12986            "class K { f = 1; g(p = 2, {q = 3} = {}) { let d = 4 } }",
12987            "foo.js",
12988            |metric| {
12989                // `f = 1`, `p = 2`, `q = 3`, `= {}`, `let d = 4`.
12990                assert_eq!(metric.abc.assignments_sum(), 5);
12991            },
12992        );
12993    }
12994
12995    #[cfg(feature = "javascript")]
12996    #[test]
12997    fn javascript_const_initializer_value_assignments_still_count() {
12998        // An `=` inside a `const` initializer's *value* is an
12999        // `assignment_expression`: its climb reaches no pattern layer and
13000        // no declarator, so it counts. The pre-#1277 sentinel
13001        // blanket-suppressed every `=` between `const` and `;`, which is
13002        // the shape that moved the pdf.js corpus snapshots
13003        // (`const bbox = (this.data.rect = …)`).
13004        check_metrics::<JavascriptParser>(
13005            "function m(o, a, b) { const x = (o.p = 1); const y = a || (b = 2); }",
13006            "foo.js",
13007            |metric| {
13008                // `o.p = 1` and `b = 2`; the two `const` initializers are
13009                // suppressed. Pre-#1277: 0.
13010                assert_eq!(metric.abc.assignments_sum(), 2);
13011            },
13012        );
13013    }
13014
13015    #[cfg(feature = "javascript")]
13016    #[test]
13017    fn javascript_const_declarator_shapes_stay_suppressed() {
13018        // JavaScript half of
13019        // `typescript_const_declarator_shapes_stay_suppressed`.
13020        check_metrics::<JavascriptParser>(
13021            "function f(o, xs) {
13022                const a = 1, b = 2
13023                const {c = 5, d: {e = 6} = {}} = o
13024                const [g = 7, ...[h = 8]] = xs
13025                let i = 3
13026                var j = 4
13027                for (const x of xs) { k(x) }
13028            }",
13029            "foo.js",
13030            |metric| {
13031                assert_eq!(metric.abc.assignments_sum(), 2);
13032            },
13033        );
13034    }
13035
13036    #[cfg(feature = "javascript")]
13037    #[test]
13038    fn javascript_increment_and_decrement_count_as_assignment() {
13039        // `x++` (post) and `--x` (pre) both update an lvalue and so
13040        // count as assignments. Combined with the `let x = 0`
13041        // initializer (which counts under the JS/TS rule — only `const`
13042        // suppresses), A = 3.
13043        check_metrics::<JavascriptParser>(
13044            "function f() { let x = 0; x++; --x; }",
13045            "foo.js",
13046            |metric| {
13047                assert_eq!(metric.abc.assignments_sum(), 3);
13048                insta::assert_json_snapshot!(metric.abc);
13049            },
13050        );
13051    }
13052
13053    #[cfg(feature = "javascript")]
13054    #[test]
13055    fn javascript_calls_are_branches() {
13056        // `g(1)` is a `call_expression` → B = 1. `new Foo(2)` is a
13057        // `new_expression` → B = 1. Total B = 2.
13058        check_metrics::<JavascriptParser>(
13059            "function f() { g(1); new Foo(2); }",
13060            "foo.js",
13061            |metric| {
13062                assert_eq!(metric.abc.branches_sum(), 2);
13063                assert_eq!(metric.abc.conditions_sum(), 0);
13064                insta::assert_json_snapshot!(metric.abc);
13065            },
13066        );
13067    }
13068
13069    #[cfg(feature = "javascript")]
13070    #[test]
13071    fn javascript_comparisons_count_conditions() {
13072        // `==`, `===`, `!=`, `!==`, `<`, `>`, `<=`, `>=` each count
13073        // once. The `&&` / `||` short-circuit operators are NOT
13074        // counted as conditions in this impl (matches the TS
13075        // precedent — short-circuit ops are folded into the
13076        // surrounding `if` / control-flow arm, not separately).
13077        // Total C = 8.
13078        check_metrics::<JavascriptParser>(
13079            "function f(a, b) { return a == b && a === b && a != b && a !== b && a < b && a > b && a <= b && a >= b; }",
13080            "foo.js",
13081            |metric| {
13082                assert_eq!(metric.abc.conditions_sum(), 8);
13083                insta::assert_json_snapshot!(metric.abc);
13084            },
13085        );
13086    }
13087
13088    // #1297 in the `js_abc_compute!` expansion, where `LT` / `GT` had
13089    // no gate at all: plain JavaScript has no generics, but
13090    // tree-sitter-javascript parses JSX unconditionally, so the same
13091    // seven tag brackets that
13092    // `tsx_jsx_elements_are_not_conditions` covers scored seven
13093    // conditions in a `.js` file too. Same fixture minus the type
13094    // annotations, same discriminating numbers: 8 pre-fix, 1 after.
13095    #[cfg(feature = "javascript")]
13096    #[test]
13097    fn javascript_jsx_elements_are_not_conditions() {
13098        check_metrics::<JavascriptParser>(
13099            "function f(a, b) {
13100                if (a < b) { return <div className=\"x\"><span>hi</span></div>; }
13101                return <br />;
13102            }",
13103            "foo.js",
13104            |metric| {
13105                assert_eq!(metric.abc.conditions_sum(), 1);
13106                // Fixture-presence anchor: the `className="x"` attribute
13107                // `=` is this fixture's only assignment, so the test
13108                // fails if a later edit drops the JSX. Without it the
13109                // surviving `a < b` alone also scores 1, and the
13110                // conditions assertion would pass while proving nothing.
13111                assert_eq!(metric.abc.assignments_sum(), 1);
13112            },
13113        );
13114    }
13115
13116    #[cfg(feature = "javascript")]
13117    #[test]
13118    fn javascript_number_truthy_condition_counts() {
13119        // Regression for #772: JS treats every non-zero number as
13120        // truthy, so `while (5)` and `x && 5` should each count their
13121        // numeric literal as a Fitzpatrick unary condition. Pre-fix
13122        // the former `javascript_bool_terminal_kinds!()` listed `True` /
13123        // `False` but omitted `Number`, so the walker dropped every numeric-
13124        // truthy operand (mirrors the Lua `Number` fix).
13125        check_metrics::<JavascriptParser>(
13126            "function f(x) { while (5) {} return x && 5; }",
13127            "foo.js",
13128            |metric| {
13129                // `while (5)` → Number literal (+1). `x && 5` → both
13130                // operands count: identifier `x` (+1), Number `5` (+1).
13131                // Total: 3.
13132                assert_eq!(metric.abc.conditions_sum(), 3);
13133                insta::assert_json_snapshot!(metric.abc);
13134            },
13135        );
13136    }
13137
13138    #[cfg(feature = "typescript")]
13139    #[test]
13140    fn typescript_number_truthy_condition_counts() {
13141        // Regression for #772: TS shares the JS truthy semantics. The
13142        // numeric *literal* `5` (kind `Number`) counts; the type-keyword
13143        // `number` (kind `Number2`, the `predefined_type`) must not —
13144        // see the former `typescript_bool_terminal_kinds!`.
13145        check_metrics::<TypescriptParser>(
13146            "function f(x: number) { while (5) {} return x && 5; }",
13147            "foo.ts",
13148            |metric| {
13149                // `while (5)` → +1; `x && 5` → `x` (+1) + `5` (+1).
13150                // Total: 3. The `: number` annotation contributes 0.
13151                assert_eq!(metric.abc.conditions_sum(), 3);
13152                insta::assert_json_snapshot!(metric.abc);
13153            },
13154        );
13155    }
13156
13157    #[cfg(feature = "javascript")]
13158    #[test]
13159    fn javascript_nullish_coalescing_counts_condition() {
13160        // `a ?? b` is one nullish-coalescing operator → C = 1.
13161        check_metrics::<JavascriptParser>(
13162            "function f(a, b) { return a ?? b; }",
13163            "foo.js",
13164            |metric| {
13165                assert_eq!(metric.abc.conditions_sum(), 1);
13166                insta::assert_json_snapshot!(metric.abc);
13167            },
13168        );
13169    }
13170
13171    #[cfg(feature = "javascript")]
13172    #[test]
13173    fn javascript_else_ternary_case_default_try_catch() {
13174        // `else`, `?` (ternary), `case`, `try`, `catch` all count.
13175        // `default` is the unconditional fallthrough → +0 (#469).
13176        // With the comparisons:
13177        //   - `a > 0` → 1
13178        //   - `else` opens an else_clause → 1
13179        //   - `?` ternary → 1
13180        //   - the ternary's bare-identifier condition `a` → 1 (#1102)
13181        //   - `case 1` → 1
13182        //   - `default` → 0 (fallthrough, #469)
13183        //   - `try` + `catch` → 2
13184        // Total C = 7.
13185        check_metrics::<JavascriptParser>(
13186            "function f(a) { if (a > 0) {} else {} let x = a ? 1 : 2; switch (x) { case 1: break; default: break; } try { } catch (e) { } }",
13187            "foo.js",
13188            |metric| {
13189                assert_eq!(metric.abc.conditions_sum(), 7);
13190                insta::assert_json_snapshot!(metric.abc);
13191            },
13192        );
13193    }
13194
13195    // Issue #1102, JS-family half. See
13196    // `cpp_ternary_operand_slots_count_as_unary_conditions` for the
13197    // rule; the two families were behind Java by the same three units.
13198    #[cfg(feature = "javascript")]
13199    #[test]
13200    fn javascript_ternary_operand_slots_count_as_unary_conditions() {
13201        // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4.
13202        check_metrics::<JavascriptParser>(
13203            "function f() { x = a ? !b : !c; }",
13204            "foo.js",
13205            |metric| assert_eq!(metric.abc.conditions_sum(), 4),
13206        );
13207        // No-double-count pin: `?` (1) + `>` (1) = 2, unchanged by the
13208        // fix — the parenthesised condition unwraps to a
13209        // `binary_expression` (not a boolean terminal) and neither
13210        // branch is negated.
13211        check_metrics::<JavascriptParser>(
13212            "function f() { x = (a > 0) ? b : -b; }",
13213            "foo.js",
13214            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
13215        );
13216        // Nested: two `?` tokens plus the two bare-identifier
13217        // conditions = 4.
13218        check_metrics::<JavascriptParser>(
13219            "function f() { x = a ? b ? c : d : e; }",
13220            "foo.js",
13221            |metric| assert_eq!(metric.abc.conditions_sum(), 4),
13222        );
13223        // A negated condition is the only input reaching the walker's
13224        // `else` fallback — see the C++ sibling for why. `?` (1) +
13225        // `!a` (1) = 2.
13226        check_metrics::<JavascriptParser>("function f() { x = !a ? b : c; }", "foo.js", |metric| {
13227            assert_eq!(metric.abc.conditions_sum(), 2);
13228        });
13229    }
13230
13231    // TypeScript expands the same `ts_abc_compute!` arm from a separate
13232    // macro body than JavaScript's `js_abc_compute!`, so wiring one and
13233    // not the other is a live failure mode; TSX and Mozjs are clones of
13234    // these two.
13235    #[cfg(feature = "typescript")]
13236    #[test]
13237    fn typescript_ternary_operand_slots_count_as_unary_conditions() {
13238        check_metrics::<TypescriptParser>(
13239            "function f() { x = a ? !b : !c; }",
13240            "foo.ts",
13241            |metric| assert_eq!(metric.abc.conditions_sum(), 4),
13242        );
13243        check_metrics::<TypescriptParser>(
13244            "function f() { x = (a > 0) ? b : -b; }",
13245            "foo.ts",
13246            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
13247        );
13248    }
13249
13250    // Issue #1276, JS-family half. See
13251    // `cpp_for_condition_slot_counts_unary_conditions` for the rule.
13252    // The JS grammar marks the `condition` field on both the expression
13253    // and the `;` closing it, so `child_by_field_name` is the only
13254    // addressing that lands on the expression for every header shape.
13255    #[cfg(feature = "javascript")]
13256    #[test]
13257    fn javascript_for_condition_slot_counts_unary_conditions() {
13258        // Bare identifier: no operator token anywhere in the header.
13259        check_metrics::<JavascriptParser>("function f(a) { for (; a; ) {} }", "foo.js", |metric| {
13260            assert_eq!(metric.abc.conditions_sum(), 1);
13261        });
13262        // Negation, through the `!` unwrap.
13263        check_metrics::<JavascriptParser>(
13264            "function f(a) { for (; !a; ) {} }",
13265            "foo.js",
13266            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
13267        );
13268        // Parentheses: counts only via the `ForStatement`
13269        // boolean-context seed #1276 found dead.
13270        check_metrics::<JavascriptParser>(
13271            "function f(a) { for (; (a); ) {} }",
13272            "foo.js",
13273            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
13274        );
13275        // No-double-count pin: the `<` arm already counted this shape.
13276        // `let i = 0` and `i++` are the two assignments, which also
13277        // confirms the header parsed as the three-clause form.
13278        check_metrics::<JavascriptParser>(
13279            "function f(n) { for (let i = 0; i < n; i++) {} }",
13280            "foo.js",
13281            |metric| {
13282                assert_eq!(metric.abc.conditions_sum(), 1);
13283                assert_eq!(metric.abc.assignments_sum(), 2);
13284            },
13285        );
13286        // Empty condition: the slot holds an `empty_statement`, which
13287        // is neither a terminal nor a wrapper, so it counts nothing.
13288        check_metrics::<JavascriptParser>(
13289            "function f() { for (;;) { break; } }",
13290            "foo.js",
13291            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
13292        );
13293    }
13294
13295    // TypeScript expands the `ForStatement` arm from `ts_abc_compute!`,
13296    // a separate macro body from JavaScript's `js_abc_compute!`, so
13297    // wiring one and not the other is a live failure mode; TSX and
13298    // Mozjs are the clones of those two. The expected values are
13299    // derived from the JavaScript run rather than hardcoded.
13300    #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13301    #[test]
13302    fn js_family_for_condition_slot_agrees_with_javascript() {
13303        #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13304        const BARE: &str = "function f(a) { for (; a; ) {} }\n";
13305        #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13306        const EMPTY: &str = "function f() { for (;;) { break; } }\n";
13307        let conditions = abc_conditions;
13308
13309        let bare = conditions(LANG::Javascript, BARE);
13310        // Non-degenerate: a zeroed reference makes the comparisons
13311        // below vacuous.
13312        assert_eq!(bare, 1, "JavaScript reference value for `for (; a; )`");
13313        assert_eq!(
13314            conditions(LANG::Javascript, EMPTY),
13315            0,
13316            "JavaScript `for (;;)`"
13317        );
13318
13319        for lang in [LANG::Mozjs, LANG::Typescript, LANG::Tsx] {
13320            assert_eq!(conditions(lang, BARE), bare, "{lang:?} bare for-condition");
13321            assert_eq!(conditions(lang, EMPTY), 0, "{lang:?} empty for-condition");
13322        }
13323    }
13324
13325    #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13326    #[test]
13327    fn js_family_ternary_operand_slots_agree_with_javascript() {
13328        // `a ? !b : !c` is the one shape that tells the ternary walker
13329        // from the `for`-header walker: both read the `condition` field
13330        // and share the `(&Node, &mut f64)` signature, so a transposed
13331        // pair in a `ts_abc_compute!` / `js_abc_compute!` invocation
13332        // compiles, passes every condition-slot test, and drops only the
13333        // two branch operands. TypeScript alone pinned those before; the
13334        // other three expansions now do too.
13335        #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13336        const SRC: &str = "function f(a, b, c) { x = a ? !b : !c; }\n";
13337        let javascript = abc_conditions(LANG::Javascript, SRC);
13338        // expected: 4 — the `?`, the `a` condition slot and both negated
13339        // branch operands; non-degenerate by construction.
13340        assert_eq!(
13341            javascript, 4,
13342            "JavaScript reference value for `a ? !b : !c`"
13343        );
13344        for lang in [LANG::Mozjs, LANG::Typescript, LANG::Tsx] {
13345            assert_eq!(
13346                abc_conditions(lang, SRC),
13347                javascript,
13348                "{lang:?} ternary operand slots"
13349            );
13350        }
13351    }
13352
13353    #[cfg(feature = "javascript")]
13354    #[test]
13355    fn javascript_instanceof_counts_condition() {
13356        // `x instanceof Foo` is a binary expression whose operator is
13357        // the `instanceof` keyword token → C = 1.
13358        check_metrics::<JavascriptParser>(
13359            "function f(x) { return x instanceof Foo; }",
13360            "foo.js",
13361            |metric| {
13362                assert_eq!(metric.abc.conditions_sum(), 1);
13363                insta::assert_json_snapshot!(metric.abc);
13364            },
13365        );
13366    }
13367
13368    #[cfg(feature = "javascript")]
13369    #[test]
13370    fn javascript_complex_function_abc() {
13371        // Mixed-shape regression. Verified by hand:
13372        // - assignments: `let x = 0` (a `let` initializer counts)
13373        //   + `x = 5`, `x += 2`, `x++`, `x = (a>b)?a:b`, `x = b`,
13374        //   `let p = ...` (likewise) → A = 7.
13375        // - branches: `f(a, b)` self-call + `new Bar()` → B = 2.
13376        // - conditions: `a == b`, `a > 0` → 2 inside the if header
13377        //   (`&&` is not counted directly). `else` (1) + `a > b`,
13378        //   `?` → 2 in the ternary. `a < b` → 1 in the else-if.
13379        //   `!x` → 1 from the Fitzpatrick Rule 9 walker on `||`
13380        //   (issue #403): the wrapped Identifier counts once.
13381        //   `case 1` → 1 in the switch; `default` → 0 (fallthrough,
13382        //   #469). Total C = 8.
13383        check_metrics::<JavascriptParser>(
13384            "function f(a, b) {\n\
13385                 let x = 0;\n\
13386                 x = 5;\n\
13387                 x += 2;\n\
13388                 x++;\n\
13389                 if (a == b && a > 0) {\n\
13390                     x = (a > b) ? a : b;\n\
13391                 } else if (a < b || !x) {\n\
13392                     x = b;\n\
13393                 }\n\
13394                 switch (x) {\n\
13395                     case 1: break;\n\
13396                     default: break;\n\
13397                 }\n\
13398                 let p = new Bar();\n\
13399                 return f(a, b);\n\
13400             }\n",
13401            "foo.js",
13402            |metric| {
13403                assert_eq!(metric.abc.assignments_sum(), 7);
13404                assert_eq!(metric.abc.branches_sum(), 2);
13405                assert_eq!(metric.abc.conditions_sum(), 8);
13406                insta::assert_json_snapshot!(metric.abc);
13407            },
13408        );
13409    }
13410
13411    #[cfg(feature = "mozjs")]
13412    #[test]
13413    fn mozjs_asi_const_does_not_suppress_later_assignments() {
13414        // Mozjs half of the #1277 cluster; the fork carries its own
13415        // kind-id numbering, so it needs its own fixture. See
13416        // `typescript_asi_const_does_not_suppress_later_assignments`.
13417        check_metrics::<MozjsParser>(
13418            "function f() {
13419                const a = 1
13420                x = 2
13421                y = 3
13422            }",
13423            "foo.jsm",
13424            |metric| {
13425                assert_eq!(metric.abc.assignments_sum(), 2);
13426            },
13427        );
13428    }
13429
13430    #[cfg(feature = "mozjs")]
13431    #[test]
13432    fn mozjs_const_declarator_shapes_stay_suppressed() {
13433        // Mozjs half of
13434        // `javascript_const_declarator_shapes_stay_suppressed`.
13435        check_metrics::<MozjsParser>(
13436            "function f(o, xs) {
13437                const a = 1, b = 2
13438                const {c = 5, d: {e = 6} = {}} = o
13439                const [g = 7, ...[h = 8]] = xs
13440                let i = 3
13441                var j = 4
13442                for (const x of xs) { k(x) }
13443            }",
13444            "foo.jsm",
13445            |metric| {
13446                assert_eq!(metric.abc.assignments_sum(), 2);
13447            },
13448        );
13449    }
13450
13451    #[cfg(feature = "mozjs")]
13452    #[test]
13453    fn mozjs_complex_function_abc() {
13454        // Mozjs shares JavaScript's expression / statement vocabulary;
13455        // the `js_abc_compute!` macro expands identical token-level
13456        // rules for both. This test pins parity against the JS impl.
13457        check_metrics::<MozjsParser>(
13458            "function f(a, b) {\n\
13459                 let x = 0;\n\
13460                 x = 5;\n\
13461                 x += 2;\n\
13462                 x++;\n\
13463                 if (a == b && a > 0) {\n\
13464                     x = (a > b) ? a : b;\n\
13465                 } else if (a < b || !x) {\n\
13466                     x = b;\n\
13467                 }\n\
13468                 switch (x) {\n\
13469                     case 1: break;\n\
13470                     default: break;\n\
13471                 }\n\
13472                 let p = new Bar();\n\
13473                 return f(a, b);\n\
13474             }\n",
13475            "foo.js",
13476            |metric| {
13477                assert_eq!(metric.abc.assignments_sum(), 7);
13478                assert_eq!(metric.abc.branches_sum(), 2);
13479                assert_eq!(metric.abc.conditions_sum(), 8);
13480                insta::assert_json_snapshot!(metric.abc);
13481            },
13482        );
13483    }
13484
13485    // ----- JS / TS / Tsx / Mozjs Phase-2B condition slots -----
13486
13487    #[cfg(feature = "javascript")]
13488    #[test]
13489    fn javascript_await_expression_condition_counts() {
13490        // Regression for findings.md round-2 #2 (JS):
13491        // `if (await ready()) {}` parses with `await_expression` as
13492        // the condition node inside the `parenthesized_expression`.
13493        // The former `javascript_inspect_container` unwrapped the paren
13494        // but the await child was not in the terminal-bool set, so the
13495        // walker broke without counting. Mirrors C# (lesson 19).
13496        check_metrics::<JavascriptParser>(
13497            "async function ready() { return true; }\n\
13498             async function f() { if (await ready()) { } }\n",
13499            "foo.js",
13500            |metric| {
13501                assert_eq!(metric.abc.branches_sum(), 1);
13502                assert_eq!(metric.abc.conditions_sum(), 1);
13503                insta::assert_json_snapshot!(metric.abc);
13504            },
13505        );
13506    }
13507
13508    #[cfg(feature = "javascript")]
13509    #[test]
13510    fn javascript_member_expression_condition_counts() {
13511        // Regression for findings.md #3 (JS-family): tree-sitter-
13512        // javascript emits `member_expression` under three kind_ids
13513        // (191 primary, 208, 228 — `MemberExpression2/3`) depending
13514        // on the production rule path. The verifier in this audit
13515        // confirmed runtime kind for `o.x` is 208. Pre-fix the
13516        // shared former `js_family_bool_terminal_kinds!()` macro listed
13517        // only the primary, so every `if (o.x) {}` / `o.x && o.y`
13518        // condition silently reported zero. The slot now pays for all
13519        // three aliases, none of which scores itself (lesson #2).
13520        check_metrics::<JavascriptParser>(
13521            "function f(o) {\n\
13522             \x20   if (o.x) {}                  // +1c\n\
13523             \x20   return o.x && o.y;           // +2c (walker on &&)\n\
13524             }\n",
13525            "foo.js",
13526            |metric| {
13527                assert_eq!(metric.abc.conditions_sum(), 3);
13528                insta::assert_json_snapshot!(metric.abc);
13529            },
13530        );
13531    }
13532
13533    #[cfg(feature = "javascript")]
13534    #[test]
13535    fn javascript_if_boolean_literal_condition() {
13536        check_metrics::<JavascriptParser>(
13537            "function f() {\n\
13538             \x20   if (true) {}                 // +1c\n\
13539             \x20   if (!false) {}               // +1c\n\
13540             \x20   while (true) {}              // +1c\n\
13541             \x20   do {} while (false);         // +1c\n\
13542             }\n",
13543            "foo.js",
13544            |metric| {
13545                assert_eq!(metric.abc.conditions_sum(), 4);
13546                insta::assert_json_snapshot!(metric.abc);
13547            },
13548        );
13549    }
13550
13551    #[cfg(feature = "javascript")]
13552    #[test]
13553    fn javascript_methods_arguments_with_conditions() {
13554        check_metrics::<JavascriptParser>(
13555            "function f(a, b) {\n\
13556             \x20   m(a, b);                     // +1b\n\
13557             \x20   m(!a, !b);                   // +1b +2c\n\
13558             }\n",
13559            "foo.js",
13560            |metric| {
13561                assert_eq!(metric.abc.branches_sum(), 2);
13562                assert_eq!(metric.abc.conditions_sum(), 2);
13563                insta::assert_json_snapshot!(metric.abc);
13564            },
13565        );
13566    }
13567
13568    #[cfg(feature = "javascript")]
13569    #[test]
13570    fn javascript_return_with_conditions() {
13571        check_metrics::<JavascriptParser>(
13572            "function m1(z) { return !(z >= 0); }\n\
13573             function m2(x) { return (((!x))); }\n\
13574             function m3(x, y) { return x && y; }\n",
13575            "foo.js",
13576            |metric| {
13577                // m1: 1 (`>=`). m2: 1 (walker unwraps to `x`).
13578                // m3: 2 (`&&` walker counts both terminals).
13579                assert_eq!(metric.abc.conditions_sum(), 4);
13580                insta::assert_json_snapshot!(metric.abc);
13581            },
13582        );
13583    }
13584
13585    #[cfg(feature = "typescript")]
13586    #[test]
13587    fn typescript_if_boolean_literal_condition() {
13588        check_metrics::<TypescriptParser>(
13589            "function f() {\n\
13590             \x20   if (true) {}\n\
13591             \x20   if (!false) {}\n\
13592             \x20   while (true) {}\n\
13593             \x20   do {} while (false);\n\
13594             }\n",
13595            "foo.ts",
13596            |metric| {
13597                assert_eq!(metric.abc.conditions_sum(), 4);
13598                insta::assert_json_snapshot!(metric.abc);
13599            },
13600        );
13601    }
13602
13603    #[cfg(feature = "typescript")]
13604    #[test]
13605    fn typescript_methods_arguments_with_conditions() {
13606        check_metrics::<TypescriptParser>(
13607            "function f(a: boolean, b: boolean) {\n\
13608             \x20   m(a, b);\n\
13609             \x20   m(!a, !b);\n\
13610             }\n",
13611            "foo.ts",
13612            |metric| {
13613                assert_eq!(metric.abc.branches_sum(), 2);
13614                assert_eq!(metric.abc.conditions_sum(), 2);
13615                insta::assert_json_snapshot!(metric.abc);
13616            },
13617        );
13618    }
13619
13620    #[cfg(feature = "typescript")]
13621    #[test]
13622    fn typescript_return_with_conditions() {
13623        check_metrics::<TypescriptParser>(
13624            "function m1(z: number): boolean { return !(z >= 0); }\n\
13625             function m2(x: boolean): boolean { return (((!x))); }\n\
13626             function m3(x: boolean, y: boolean): boolean { return x && y; }\n",
13627            "foo.ts",
13628            |metric| {
13629                assert_eq!(metric.abc.conditions_sum(), 4);
13630                insta::assert_json_snapshot!(metric.abc);
13631            },
13632        );
13633    }
13634
13635    #[cfg(feature = "typescript")]
13636    #[test]
13637    fn tsx_if_boolean_literal_condition() {
13638        check_metrics::<TsxParser>(
13639            "function f() {\n\
13640             \x20   if (true) {}\n\
13641             \x20   if (!false) {}\n\
13642             \x20   while (true) {}\n\
13643             \x20   do {} while (false);\n\
13644             }\n",
13645            "foo.tsx",
13646            |metric| {
13647                assert_eq!(metric.abc.conditions_sum(), 4);
13648                insta::assert_json_snapshot!(metric.abc);
13649            },
13650        );
13651    }
13652
13653    #[cfg(feature = "typescript")]
13654    #[test]
13655    fn tsx_methods_arguments_with_conditions() {
13656        check_metrics::<TsxParser>(
13657            "function f(a: boolean, b: boolean) {\n\
13658             \x20   m(a, b);\n\
13659             \x20   m(!a, !b);\n\
13660             }\n",
13661            "foo.tsx",
13662            |metric| {
13663                assert_eq!(metric.abc.branches_sum(), 2);
13664                assert_eq!(metric.abc.conditions_sum(), 2);
13665                insta::assert_json_snapshot!(metric.abc);
13666            },
13667        );
13668    }
13669
13670    #[cfg(feature = "typescript")]
13671    #[test]
13672    fn tsx_return_with_conditions() {
13673        check_metrics::<TsxParser>(
13674            "function m1(z: number): boolean { return !(z >= 0); }\n\
13675             function m2(x: boolean): boolean { return (((!x))); }\n\
13676             function m3(x: boolean, y: boolean): boolean { return x && y; }\n",
13677            "foo.tsx",
13678            |metric| {
13679                assert_eq!(metric.abc.conditions_sum(), 4);
13680                insta::assert_json_snapshot!(metric.abc);
13681            },
13682        );
13683    }
13684
13685    #[cfg(feature = "mozjs")]
13686    #[test]
13687    fn mozjs_jsx_elements_are_not_conditions() {
13688        // #1297 in the second expansion of `js_abc_compute!`. The
13689        // vendored mozjs fork carries the same three JSX productions as
13690        // upstream tree-sitter-javascript with its own `kind_id`s, so
13691        // the gate is a distinct instantiation and needs its own
13692        // fixture — a passing JavaScript test says nothing about the
13693        // macro's other expansion. Same shape and same discriminating
13694        // numbers as `javascript_jsx_elements_are_not_conditions`: 8
13695        // pre-fix, 1 after.
13696        check_metrics::<MozjsParser>(
13697            "function f(a, b) {
13698                if (a < b) { return <div className=\"x\"><span>hi</span></div>; }
13699                return <br />;
13700            }",
13701            "foo.js",
13702            |metric| {
13703                assert_eq!(metric.abc.conditions_sum(), 1);
13704                // Fixture-presence anchor: the `className="x"` attribute
13705                // `=` is this fixture's only assignment, so the test
13706                // fails if a later edit drops the JSX. Without it the
13707                // surviving `a < b` alone also scores 1, and the
13708                // conditions assertion would pass while proving nothing.
13709                assert_eq!(metric.abc.assignments_sum(), 1);
13710            },
13711        );
13712    }
13713
13714    #[cfg(feature = "mozjs")]
13715    #[test]
13716    fn mozjs_if_boolean_literal_condition() {
13717        check_metrics::<MozjsParser>(
13718            "function f() {\n\
13719             \x20   if (true) {}\n\
13720             \x20   if (!false) {}\n\
13721             \x20   while (true) {}\n\
13722             \x20   do {} while (false);\n\
13723             }\n",
13724            "foo.js",
13725            |metric| {
13726                assert_eq!(metric.abc.conditions_sum(), 4);
13727                insta::assert_json_snapshot!(metric.abc);
13728            },
13729        );
13730    }
13731
13732    #[cfg(feature = "mozjs")]
13733    #[test]
13734    fn mozjs_methods_arguments_with_conditions() {
13735        check_metrics::<MozjsParser>(
13736            "function f(a, b) {\n\
13737             \x20   m(a, b);\n\
13738             \x20   m(!a, !b);\n\
13739             }\n",
13740            "foo.js",
13741            |metric| {
13742                assert_eq!(metric.abc.branches_sum(), 2);
13743                assert_eq!(metric.abc.conditions_sum(), 2);
13744                insta::assert_json_snapshot!(metric.abc);
13745            },
13746        );
13747    }
13748
13749    #[cfg(feature = "mozjs")]
13750    #[test]
13751    fn mozjs_return_with_conditions() {
13752        check_metrics::<MozjsParser>(
13753            "function m1(z) { return !(z >= 0); }\n\
13754             function m2(x) { return (((!x))); }\n\
13755             function m3(x, y) { return x && y; }\n",
13756            "foo.js",
13757            |metric| {
13758                assert_eq!(metric.abc.conditions_sum(), 4);
13759                insta::assert_json_snapshot!(metric.abc);
13760            },
13761        );
13762    }
13763
13764    // ----- JS / TS / Tsx / Mozjs unary-conditional walker -----
13765
13766    #[cfg(feature = "javascript")]
13767    #[test]
13768    fn javascript_if_multiple_conditions() {
13769        check_metrics::<JavascriptParser>(
13770            "function f(a, b, c, d) {\n\
13771             \x20   if (a || b || c || d) {}        // +4c\n\
13772             \x20   if (a && b && c) {}             // +3c\n\
13773             \x20   if (!a && !b) {}                // +2c\n\
13774             }\n",
13775            "foo.js",
13776            |metric| {
13777                assert_eq!(metric.abc.conditions_sum(), 9);
13778                insta::assert_json_snapshot!(metric.abc);
13779            },
13780        );
13781    }
13782
13783    #[cfg(feature = "javascript")]
13784    #[test]
13785    fn javascript_while_and_do_while_conditions() {
13786        check_metrics::<JavascriptParser>(
13787            "function f(a, b) {\n\
13788             \x20   while (a || b) {}              // +2c\n\
13789             \x20   do {} while (a && !b);         // +2c\n\
13790             }\n",
13791            "foo.js",
13792            |metric| {
13793                assert_eq!(metric.abc.conditions_sum(), 4);
13794                insta::assert_json_snapshot!(metric.abc);
13795            },
13796        );
13797    }
13798
13799    #[cfg(feature = "javascript")]
13800    #[test]
13801    fn javascript_short_circuit_with_boolean_literal_operand() {
13802        check_metrics::<JavascriptParser>(
13803            "function f(a) { return a && true; }\n",
13804            "foo.js",
13805            |metric| {
13806                assert_eq!(metric.abc.conditions_sum(), 2);
13807                insta::assert_json_snapshot!(metric.abc);
13808            },
13809        );
13810    }
13811
13812    #[cfg(feature = "typescript")]
13813    #[test]
13814    fn typescript_if_multiple_conditions() {
13815        check_metrics::<TypescriptParser>(
13816            "function f(a: boolean, b: boolean, c: boolean, d: boolean) {\n\
13817             \x20   if (a || b || c || d) {}        // +4c\n\
13818             \x20   if (a && b && c) {}             // +3c\n\
13819             \x20   if (!a && !b) {}                // +2c\n\
13820             }\n",
13821            "foo.ts",
13822            |metric| {
13823                assert_eq!(metric.abc.conditions_sum(), 9);
13824                insta::assert_json_snapshot!(metric.abc);
13825            },
13826        );
13827    }
13828
13829    #[cfg(feature = "typescript")]
13830    #[test]
13831    fn typescript_while_and_do_while_conditions() {
13832        check_metrics::<TypescriptParser>(
13833            "function f(a: boolean, b: boolean) {\n\
13834             \x20   while (a || b) {}              // +2c\n\
13835             \x20   do {} while (a && !b);         // +2c\n\
13836             }\n",
13837            "foo.ts",
13838            |metric| {
13839                assert_eq!(metric.abc.conditions_sum(), 4);
13840                insta::assert_json_snapshot!(metric.abc);
13841            },
13842        );
13843    }
13844
13845    #[cfg(feature = "typescript")]
13846    #[test]
13847    fn typescript_short_circuit_with_boolean_literal_operand() {
13848        check_metrics::<TypescriptParser>(
13849            "function f(a: boolean): boolean { return a && true; }\n",
13850            "foo.ts",
13851            |metric| {
13852                assert_eq!(metric.abc.conditions_sum(), 2);
13853                insta::assert_json_snapshot!(metric.abc);
13854            },
13855        );
13856    }
13857
13858    #[cfg(feature = "typescript")]
13859    #[test]
13860    fn tsx_if_multiple_conditions() {
13861        check_metrics::<TsxParser>(
13862            "function f(a: boolean, b: boolean, c: boolean, d: boolean) {\n\
13863             \x20   if (a || b || c || d) {}        // +4c\n\
13864             \x20   if (a && b && c) {}             // +3c\n\
13865             \x20   if (!a && !b) {}                // +2c\n\
13866             }\n",
13867            "foo.tsx",
13868            |metric| {
13869                assert_eq!(metric.abc.conditions_sum(), 9);
13870                insta::assert_json_snapshot!(metric.abc);
13871            },
13872        );
13873    }
13874
13875    #[cfg(feature = "typescript")]
13876    #[test]
13877    fn tsx_while_and_do_while_conditions() {
13878        check_metrics::<TsxParser>(
13879            "function f(a: boolean, b: boolean) {\n\
13880             \x20   while (a || b) {}              // +2c\n\
13881             \x20   do {} while (a && !b);         // +2c\n\
13882             }\n",
13883            "foo.tsx",
13884            |metric| {
13885                assert_eq!(metric.abc.conditions_sum(), 4);
13886                insta::assert_json_snapshot!(metric.abc);
13887            },
13888        );
13889    }
13890
13891    #[cfg(feature = "typescript")]
13892    #[test]
13893    fn tsx_short_circuit_with_boolean_literal_operand() {
13894        check_metrics::<TsxParser>(
13895            "function f(a: boolean): boolean { return a && true; }\n",
13896            "foo.tsx",
13897            |metric| {
13898                assert_eq!(metric.abc.conditions_sum(), 2);
13899                insta::assert_json_snapshot!(metric.abc);
13900            },
13901        );
13902    }
13903
13904    #[cfg(feature = "mozjs")]
13905    #[test]
13906    fn mozjs_if_multiple_conditions() {
13907        check_metrics::<MozjsParser>(
13908            "function f(a, b, c, d) {\n\
13909             \x20   if (a || b || c || d) {}        // +4c\n\
13910             \x20   if (a && b && c) {}             // +3c\n\
13911             \x20   if (!a && !b) {}                // +2c\n\
13912             }\n",
13913            "foo.js",
13914            |metric| {
13915                assert_eq!(metric.abc.conditions_sum(), 9);
13916                insta::assert_json_snapshot!(metric.abc);
13917            },
13918        );
13919    }
13920
13921    #[cfg(feature = "mozjs")]
13922    #[test]
13923    fn mozjs_while_and_do_while_conditions() {
13924        check_metrics::<MozjsParser>(
13925            "function f(a, b) {\n\
13926             \x20   while (a || b) {}              // +2c\n\
13927             \x20   do {} while (a && !b);         // +2c\n\
13928             }\n",
13929            "foo.js",
13930            |metric| {
13931                assert_eq!(metric.abc.conditions_sum(), 4);
13932                insta::assert_json_snapshot!(metric.abc);
13933            },
13934        );
13935    }
13936
13937    #[cfg(feature = "mozjs")]
13938    #[test]
13939    fn mozjs_short_circuit_with_boolean_literal_operand() {
13940        check_metrics::<MozjsParser>(
13941            "function f(a) { return a && true; }\n",
13942            "foo.js",
13943            |metric| {
13944                assert_eq!(metric.abc.conditions_sum(), 2);
13945                insta::assert_json_snapshot!(metric.abc);
13946            },
13947        );
13948    }
13949
13950    // ---------- Perl ABC tests ----------
13951
13952    #[cfg(feature = "perl")]
13953    #[test]
13954    fn perl_empty_unit_zero() {
13955        // Empty source produces zero ABC magnitude — pins the trait
13956        // wiring without exercising any compute branch.
13957        check_metrics::<PerlParser>("", "empty.pl", |metric| {
13958            assert_eq!(metric.abc.assignments_sum(), 0);
13959            assert_eq!(metric.abc.branches_sum(), 0);
13960            assert_eq!(metric.abc.conditions_sum(), 0);
13961            insta::assert_json_snapshot!(metric.abc);
13962        });
13963    }
13964
13965    #[cfg(feature = "perl")]
13966    #[test]
13967    fn perl_plain_and_compound_assignments_count() {
13968        // `my $x = 0` parses as a `binary_expression` with an `=`
13969        // token, so the initialiser counts (Perl has no equivalent of
13970        // the JS `const` initialiser-suppression rule). Each
13971        // assignment operator token contributes one assignment:
13972        // `=`, `=`, `+=`, `.=`, `**=` → A = 5. Two of those `=` come
13973        // from the `my $x = 0` initialiser and the later `$x = 5`
13974        // reassignment.
13975        check_metrics::<PerlParser>(
13976            "sub f { my $x = 0; $x = 5; $x += 2; $x .= \"a\"; $x **= 3; }",
13977            "foo.pl",
13978            |metric| {
13979                assert_eq!(metric.abc.assignments_sum(), 5);
13980                assert_eq!(metric.abc.branches_sum(), 0);
13981                assert_eq!(metric.abc.conditions_sum(), 0);
13982                insta::assert_json_snapshot!(metric.abc);
13983            },
13984        );
13985    }
13986
13987    #[cfg(feature = "perl")]
13988    #[test]
13989    fn perl_calls_are_branches() {
13990        // `foo()` parses as `call_expression_with_args_with_brackets`
13991        // wrapping an inner `call_expression_with_bareword(foo)`;
13992        // `bar 1, 2` wraps `bar` likewise under spaced-args; `shift`
13993        // appears as a standalone bareword. The bareword-inside-
13994        // wrapper case must NOT double-count — only the outer wrapper
13995        // contributes a branch. So B = 3 (foo, bar, shift), not 5.
13996        check_metrics::<PerlParser>(
13997            "sub f { foo(); bar 1, 2; my $a = shift; }",
13998            "foo.pl",
13999            |metric| {
14000                // shift's `my $a = shift` initialiser contributes one
14001                // assignment via the `=` token.
14002                assert_eq!(metric.abc.assignments_sum(), 1);
14003                assert_eq!(metric.abc.branches_sum(), 3);
14004                assert_eq!(metric.abc.conditions_sum(), 0);
14005                insta::assert_json_snapshot!(metric.abc);
14006            },
14007        );
14008    }
14009
14010    #[cfg(feature = "perl")]
14011    #[test]
14012    fn perl_method_invocation_counts_as_branch() {
14013        // `$obj->method(...)` parses as `method_invocation`. Any
14014        // arrow-dispatch counts as one branch regardless of how the
14015        // arguments are passed.
14016        check_metrics::<PerlParser>(
14017            "sub f { my $obj = shift; $obj->run($x); $obj->ping; }",
14018            "foo.pl",
14019            |metric| {
14020                // `my $obj = shift` → A=1, B=1 (shift bareword).
14021                // `$obj->run($x)` and `$obj->ping` → 2 more branches.
14022                assert_eq!(metric.abc.assignments_sum(), 1);
14023                assert_eq!(metric.abc.branches_sum(), 3);
14024                assert_eq!(metric.abc.conditions_sum(), 0);
14025                insta::assert_json_snapshot!(metric.abc);
14026            },
14027        );
14028    }
14029
14030    // #1297's own sweep cleared Perl, and it was wrong. `<STDIN>` does
14031    // lex as a single `standard_input` token and scores 0, which is
14032    // what the issue measured — but the filehandle and lexical-handle
14033    // readlines do not: `<FH>` is `standard_input_to_identifier` and
14034    // `<$fh>` is `standard_input_to_variable`, each a plain three-token
14035    // sequence whose brackets are the same bare `<` / `>` a comparison
14036    // uses, and the arm was ungated. Each scored two phantom
14037    // conditions.
14038    //
14039    // All three readline spellings against two genuine comparisons, so
14040    // every mis-aim lands on its own number: 6 pre-fix, 2 once the arm
14041    // allows `BinaryExpression`, 0 if it allows the wrong parent or the
14042    // fixture stops parsing. The assertion is also the grammar-dispatch
14043    // §8 pin — `cyclomatic()` is 3 on this space, so decisions is 2 and
14044    // the two counts agree exactly.
14045    #[cfg(feature = "perl")]
14046    #[test]
14047    fn perl_readline_angle_brackets_are_not_conditions() {
14048        check_func_space::<PerlParser, _>(
14049            "sub g {\n\
14050                 my ($a, $b, $fh) = @_;\n\
14051                 my $l = <FH>;\n\
14052                 my $m = <$fh>;\n\
14053                 my $n = <STDIN>;\n\
14054                 if ($a < $b) { return 1; }\n\
14055                 if ($a > $b) { return 2; }\n\
14056                 return 0;\n\
14057             }",
14058            "foo.pl",
14059            |space| {
14060                assert_deepest_conditions_match_cyclomatic(&space, 2);
14061                // Fixture-presence anchor: the three readlines are
14062                // three of this sub's four assignments (the fourth is
14063                // the `my (...) = @_` unpack), so dropping them from
14064                // the fixture fails here rather than leaving a test
14065                // that still reports 2 conditions and proves nothing.
14066                assert_eq!(space.metrics.abc.assignments_sum(), 4);
14067            },
14068        );
14069    }
14070
14071    #[cfg(feature = "perl")]
14072    #[test]
14073    fn perl_numeric_and_string_comparisons_count_conditions() {
14074        // Numeric ops `==`, `!=`, `<`, `>`, `<=`, `>=`, `<=>` and
14075        // string ops `eq`, `ne`, `lt`, `gt`, `le`, `ge`, `cmp` each
14076        // fire once per token. The sample below uses one of each →
14077        // C = 14. No assignments, no branches.
14078        check_metrics::<PerlParser>(
14079            "sub f {\n\
14080                 my $r;\n\
14081                 $r = $a == $b;\n\
14082                 $r = $a != $b;\n\
14083                 $r = $a <  $b;\n\
14084                 $r = $a >  $b;\n\
14085                 $r = $a <= $b;\n\
14086                 $r = $a >= $b;\n\
14087                 $r = $a <=> $b;\n\
14088                 $r = $a eq $b;\n\
14089                 $r = $a ne $b;\n\
14090                 $r = $a lt $b;\n\
14091                 $r = $a gt $b;\n\
14092                 $r = $a le $b;\n\
14093                 $r = $a ge $b;\n\
14094                 $r = $a cmp $b;\n\
14095             }",
14096            "foo.pl",
14097            |metric| {
14098                // 15 `=` tokens: one declaration `my $r` (no `=`),
14099                // then 14 `$r = …` plus there's no `=` in `my $r;`.
14100                // Actually: `my $r;` has no `=`; the 14 `$r = …` are
14101                // 14 `=` tokens. So A=14, C=14.
14102                assert_eq!(metric.abc.assignments_sum(), 14);
14103                assert_eq!(metric.abc.branches_sum(), 0);
14104                assert_eq!(metric.abc.conditions_sum(), 14);
14105                insta::assert_json_snapshot!(metric.abc);
14106            },
14107        );
14108    }
14109
14110    #[cfg(feature = "perl")]
14111    #[test]
14112    fn perl_short_circuit_not_counted_directly_ternary_counts() {
14113        // `&&`, `||`, `//`, low-precedence `and`, `or` are NOT
14114        // counted as conditions on their own (Fitzpatrick Rule 5;
14115        // #395) — instead each operand is counted as a unary
14116        // conditional by the walker (Rule 9; #403). `xor` evaluates
14117        // both operands, so it is a value like `^` and, outside a
14118        // slot, scores nothing (#1536). The pinned
14119        // tree-sitter-perl parses the low-precedence `and` as a
14120        // two-operand `unary_expression` rather than a
14121        // `binary_expression`; the walker reads the operands of either
14122        // since #1526, which is what moved this count from 10 to 14.
14123        // The low-precedence forms bind looser than `=`, so
14124        // `$r = $a and $b` is `($r = $a) and $b`, and its left operand
14125        // pays through the assignment's value.
14126        // Net: 5 chain lines × 2 operands + 0 for the `xor` line + 1
14127        // ternary node + 1 for the ternary's bare `$a` condition operand
14128        // (#1102) = 12.
14129        check_metrics::<PerlParser>(
14130            "sub f {\n\
14131                 my $r;\n\
14132                 $r = $a && $b;\n\
14133                 $r = $a || $b;\n\
14134                 $r = $a // $b;\n\
14135                 $r = $a and $b;\n\
14136                 $r = $a or  $b;\n\
14137                 $r = $a xor $b;\n\
14138                 $r = $a ? 1 : 2;\n\
14139             }",
14140            "foo.pl",
14141            |metric| {
14142                // 7 `=` tokens (one per reassignment line).
14143                assert_eq!(metric.abc.assignments_sum(), 7);
14144                assert_eq!(metric.abc.branches_sum(), 0);
14145                // 5 chain lines × 2 operands + 1 ternary node + 1 for
14146                // its bare `$a` condition operand = 12.
14147                assert_eq!(metric.abc.conditions_sum(), 12);
14148                insta::assert_json_snapshot!(metric.abc);
14149            },
14150        );
14151    }
14152
14153    // Issue #1102, Perl half. See
14154    // `cpp_ternary_operand_slots_count_as_unary_conditions` for the
14155    // rule. Like PHP, Perl's ABC dispatcher has no `?`-token arm — the
14156    // grammar does emit the token, but the `ternary_expression` node is
14157    // what carries the tally's +1. tree-sitter-perl names the branch
14158    // fields `true` / `false` rather than the C-family `consequence` /
14159    // `alternative`, so a copied C-family gate would match nothing.
14160    #[cfg(feature = "perl")]
14161    #[test]
14162    fn perl_ternary_operand_slots_count_as_unary_conditions() {
14163        // ternary (1) + condition `$a` (1) + `!$b` (1) + `!$c` (1) = 4.
14164        check_metrics::<PerlParser>("sub f { my $x = $a ? !$b : !$c; }", "foo.pl", |metric| {
14165            assert_eq!(metric.abc.conditions_sum(), 4);
14166        });
14167        // No-double-count pin: ternary (1) + `>` (1) = 2, unchanged by
14168        // the fix.
14169        check_metrics::<PerlParser>(
14170            "sub f { my $x = ($a > 0) ? $b : -$b; }",
14171            "foo.pl",
14172            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
14173        );
14174        // A negated *condition*: the slot peels the `!`
14175        // (`perl_wrapper_operand`) and pays for `$a`, which does not
14176        // score itself. ternary (1) + `!$a` (1) = 2.
14177        check_metrics::<PerlParser>("sub f { my $x = !$a ? $b : $c; }", "foo.pl", |metric| {
14178            assert_eq!(metric.abc.conditions_sum(), 2);
14179        });
14180        // Nested: two ternary nodes plus the two bare-variable
14181        // conditions = 4.
14182        check_metrics::<PerlParser>(
14183            "sub f { my $x = $a ? ($b ? $c : $d) : $e; }",
14184            "foo.pl",
14185            |metric| assert_eq!(metric.abc.conditions_sum(), 4),
14186        );
14187    }
14188
14189    // Issue #1273. tree-sitter-perl 1.1.2 parses `$a > $b ? 1 : 2` as
14190    // `$a > ($b ? 1 : 2)`, and the ternary's condition slot then paid
14191    // for `$b`: 3 against JavaScript's 2 for `a > b ? 1 : 2`. Every
14192    // chaining comparison does it; each row must also equal its
14193    // parenthesised spelling, which parses correctly and so takes its
14194    // value from outside the fix.
14195    #[cfg(feature = "perl")]
14196    #[test]
14197    fn perl_comparison_conditioned_ternary_counts_the_comparison_once() {
14198        let operators = [
14199            "<", ">", "<=", ">=", "lt", "gt", "le", "ge", "==", "!=", "eq", "ne",
14200        ];
14201        for op in operators {
14202            let bare = format!("sub f {{ my $m = $a {op} $b ? 1 : 2; }}\n");
14203            let grouped = format!("sub f {{ my $m = ($a {op} $b) ? 1 : 2; }}\n");
14204            // The ternary (1) and the comparison token (1).
14205            assert_eq!(abc_conditions(LANG::Perl, &bare), 2, "`{op}`");
14206            assert_eq!(abc_conditions(LANG::Perl, &grouped), 2, "`({op})`");
14207        }
14208    }
14209
14210    // Issue #1273, the general shape: a chaining comparison swallows
14211    // every lower-precedence operator to its right, so a logical chain
14212    // misparses too — `$a > $b && $c` is `$a > ($b && $c)`, and the
14213    // chain walker paid for `$b`. Expected values are the JavaScript
14214    // twins (`a > b && c ? 1 : 2` = 3, …) or, where JavaScript has no
14215    // spelling, the parenthesised Perl one.
14216    #[cfg(feature = "perl")]
14217    #[test]
14218    fn perl_misparsed_comparison_operand_is_not_a_condition() {
14219        let cases = [
14220            // `<` `<` `?`; `($a < $b < $c) ? 1 : 2` is 3.
14221            ("my $m = $a < $b < $c ? 1 : 2;", 3),
14222            // `>` `?` `>` `?`, as `a > b ? (c > d ? 1 : 2) : 3`.
14223            ("my $m = $a > $b ? $c > $d ? 1 : 2 : 3;", 4),
14224            // Two ternaries and two comparisons, as the JS twin.
14225            ("if ($a > $b ? $c : $d) { } return $a == $b ? 1 : 0;", 4),
14226            // The chain is the ternary's condition: `>` `?` `$c`.
14227            ("my $m = $a > $b && $c ? 1 : 2;", 3),
14228            // The ternary hangs off the chain's right comparison.
14229            ("my $m = $x > 0 && $y > 0 ? 1 : 2;", 3),
14230            ("my $m = $x && $a > $b ? 1 : 2;", 3),
14231            ("if ($a > $b && $c) { }", 2),
14232            ("if ($a eq $b && $c eq $d) { }", 2),
14233            // `&&` is the first operand of `||`: the climb crosses it.
14234            ("if ($a > $b && $c || $d) { }", 3),
14235            ("if ($a > $b && $c && $d) { }", 3),
14236            // `and` is a two-operand `unary_expression`, which the
14237            // climb crosses too: `(($a > $b) && $c) and $d` is 3.
14238            ("if ($a > $b and $c) { }", 2),
14239            ("if ($a > $b && $c and $d) { }", 3),
14240            // A comment between the operator and the ternary is an
14241            // `extra` sibling, not the operator.
14242            ("my $m = $a > # c\n $b ? 1 : 2;", 2),
14243        ];
14244        for (body, expected) in cases {
14245            let src = format!("sub f {{ {body} }}\n");
14246            assert_eq!(abc_conditions(LANG::Perl, &src), expected, "`{body}`");
14247        }
14248    }
14249
14250    // Issue #1273: shapes the workaround must leave alone. The explicit
14251    // `$a > ($b ? 1 : 2)` puts an `array` between the comparison and
14252    // the ternary, so `$b` really is a condition: 3, as
14253    // `a > (b ? 1 : 2)` in JavaScript. `<=>`, `cmp` and `=~` do not
14254    // chain and parse correctly; those rows pin that grammar fact, not
14255    // the workaround's operator list — adding `<=>` to the list moves
14256    // nothing, since no tree ever puts a ternary straight after it.
14257    #[cfg(feature = "perl")]
14258    #[test]
14259    fn perl_correctly_parsed_ternaries_keep_their_condition_slot() {
14260        let cases = [
14261            ("my $m = ($a > $b) ? 1 : 2;", 2),
14262            ("my $m = $a <=> $b ? 1 : 2;", 2),
14263            ("my $m = $a cmp $b ? 1 : 2;", 2),
14264            ("my $m = $a =~ /x/ ? 1 : 2;", 2),
14265            ("my $m = $a > ($b ? 1 : 2);", 3),
14266            ("my $m = $a ? !$b : !$c;", 4),
14267            ("my $m = $a && $b ? 1 : 2;", 3),
14268        ];
14269        for (body, expected) in cases {
14270            let src = format!("sub f {{ {body} }}\n");
14271            assert_eq!(abc_conditions(LANG::Perl, &src), expected, "`{body}`");
14272        }
14273    }
14274
14275    #[cfg(feature = "perl")]
14276    #[test]
14277    fn perl_elsif_and_else_count_conditions() {
14278        // `if (… == …) { … } elsif (… < …) { … } else { … }` →
14279        // 2 comparison tokens (`==`, `<`), plus `elsif_clause` and
14280        // `else_clause` each + 1 → C = 4. Branches: 0 (only
14281        // assignments). Assignments: just the `=` initialisers /
14282        // reassignments — there are 4 here (`$x` init plus three
14283        // `$x = …` reassigns).
14284        check_metrics::<PerlParser>(
14285            "sub f {\n\
14286                 my $x = 0;\n\
14287                 if ($a == $b) {\n\
14288                     $x = 1;\n\
14289                 } elsif ($a < $b) {\n\
14290                     $x = 2;\n\
14291                 } else {\n\
14292                     $x = 3;\n\
14293                 }\n\
14294             }",
14295            "foo.pl",
14296            |metric| {
14297                assert_eq!(metric.abc.assignments_sum(), 4);
14298                assert_eq!(metric.abc.branches_sum(), 0);
14299                assert_eq!(metric.abc.conditions_sum(), 4);
14300                insta::assert_json_snapshot!(metric.abc);
14301            },
14302        );
14303    }
14304
14305    #[cfg(feature = "perl")]
14306    #[test]
14307    fn perl_regex_match_operators_count_conditions() {
14308        // `=~` and `!~` are pattern-match operators; we count both
14309        // as conditions because they evaluate the regex match in a
14310        // boolean context.
14311        check_metrics::<PerlParser>(
14312            "sub f { my $s = shift; my $m = $s =~ /foo/; my $n = $s !~ /bar/; }",
14313            "foo.pl",
14314            |metric| {
14315                // 3 `=` tokens, 0 branches except `shift` bareword.
14316                assert_eq!(metric.abc.assignments_sum(), 3);
14317                assert_eq!(metric.abc.branches_sum(), 1);
14318                assert_eq!(metric.abc.conditions_sum(), 2);
14319                insta::assert_json_snapshot!(metric.abc);
14320            },
14321        );
14322    }
14323
14324    #[cfg(feature = "perl")]
14325    #[test]
14326    fn perl_complex_function_abc() {
14327        // Mixed program exercising every category. Computed
14328        // expected:
14329        //   Assignments: `my $i = 0` (1), `$i++` is a unary
14330        //     increment — Perl's grammar emits `PLUSPLUS` not an `=`
14331        //     operator, so it does NOT count under the operator-
14332        //     token rule. The for-loop's `$i++` is similarly
14333        //     uncounted.
14334        //     Total A: 1 from `my $i = 0`, 1 from `$total += $i`
14335        //     (the `+=` token) → A = 2.
14336        //   Branches: `do_work($i)` → 1; `print "done\n"` is a
14337        //     call_expression_with_spaced_args → 1; `return $total`
14338        //     uses the `return` keyword not a call → 0. B = 2.
14339        //   Conditions: `$i < 10` (`<`) → 1; `$i % 2 == 0` (`==`) →
14340        //     1; `else_clause` → 1. C = 3.
14341        check_metrics::<PerlParser>(
14342            "sub run {\n\
14343                 my $total = 0;\n\
14344                 for (my $i = 0; $i < 10; $i++) {\n\
14345                     if ($i % 2 == 0) {\n\
14346                         do_work($i);\n\
14347                     } else {\n\
14348                         $total += $i;\n\
14349                     }\n\
14350                 }\n\
14351                 print \"done\\n\";\n\
14352                 return $total;\n\
14353             }",
14354            "foo.pl",
14355            |metric| {
14356                // `my $total = 0` is one `=`; `my $i = 0` is another
14357                // `=`; `$total += $i` is one `+=`. Total = 3.
14358                assert_eq!(metric.abc.assignments_sum(), 3);
14359                assert_eq!(metric.abc.branches_sum(), 2);
14360                assert_eq!(metric.abc.conditions_sum(), 3);
14361                insta::assert_json_snapshot!(metric.abc);
14362            },
14363        );
14364    }
14365
14366    #[cfg(feature = "perl")]
14367    #[test]
14368    fn perl_if_multiple_conditions() {
14369        // Fitzpatrick Rule 9 walker (issue #403): each operand of a
14370        // `&&` / `||` / `//` / `and` / `or` chain is one
14371        // condition. ScalarVariable operands ($a, $b, …) qualify as
14372        // terminal-bool kinds for the walker.
14373        check_metrics::<PerlParser>(
14374            "sub f {\n\
14375                 my ($a, $b, $c, $d) = @_;\n\
14376                 if ($a || $b || $c || $d) { return 1; }    # +4c\n\
14377                 if ($a && $b && $c) { return 2; }          # +3c\n\
14378                 if (!$a && !$b) { return 3; }              # +2c\n\
14379                 return 0;\n\
14380             }",
14381            "foo.pl",
14382            |metric| {
14383                assert_eq!(metric.abc.conditions_sum(), 9);
14384                insta::assert_json_snapshot!(metric.abc);
14385            },
14386        );
14387    }
14388
14389    #[cfg(feature = "perl")]
14390    #[test]
14391    fn perl_while_and_until_conditions() {
14392        // Perl has no `do { ... } while(cond);` shape in this grammar
14393        // — `while` and `until` are the loop forms with a condition
14394        // slot. The walker fires on each `&&` / `||` token inside
14395        // those headers.
14396        check_metrics::<PerlParser>(
14397            "sub f {\n\
14398                 my ($a, $b) = @_;\n\
14399                 while ($a || $b) { last; }            # +2c\n\
14400                 until ($a && !$b) { last; }           # +2c\n\
14401             }",
14402            "foo.pl",
14403            |metric| {
14404                assert_eq!(metric.abc.conditions_sum(), 4);
14405                insta::assert_json_snapshot!(metric.abc);
14406            },
14407        );
14408    }
14409
14410    #[cfg(feature = "perl")]
14411    #[test]
14412    fn perl_for_header_condition_slot_counts_unary_conditions() {
14413        // The Perl half of #1276. The C-style `for` header's condition
14414        // slot is a Rule 9 unary condition like the `if` slot: a bare
14415        // `$ok`, a negated `!$ok` and a parenthesised `($ok)` each count
14416        // one, an empty header and a `foreach` count zero, and a
14417        // comparison-shaped `$i < $n` stays at the one the `<` arm
14418        // already counts. Every C-style row is the three-clause spelling
14419        // because tree-sitter-perl does not parse an empty initializer.
14420        let cases = [
14421            ("for (my $i = 0; $ok; $i++) { }", 1),
14422            ("for (my $i = 0; !$ok; $i++) { }", 1),
14423            ("for (my $i = 0; ($ok); $i++) { }", 1),
14424            ("for (my $i = 0; $i < $n; $i++) { }", 1),
14425            ("for (my $i = 0; $ok && $j; $i++) { }", 2),
14426            ("for (;;) { last; }", 0),
14427            ("for my $x (@l) { }", 0),
14428        ];
14429        let mut ran = 0;
14430        for (body, expected) in cases {
14431            let src = format!("sub f {{ {body} }}\n");
14432            assert_eq!(abc_conditions(LANG::Perl, &src), expected, "`{body}`");
14433            ran += 1;
14434        }
14435        assert_eq!(ran, cases.len());
14436        assert!(cases.iter().any(|&(_, n)| n == 0));
14437        assert!(cases.iter().any(|&(_, n)| n == 2));
14438
14439        // The slot agrees with the `if` slot for every shape, which is
14440        // the property the fix restores; the reference is non-degenerate
14441        // by the table above.
14442        for condition in ["$ok", "!$ok", "($ok)", "$ok && $j"] {
14443            let in_for = format!("sub f {{ for (my $i = 0; {condition}; $i++) {{ }} }}\n");
14444            let in_if = format!("sub f {{ if ({condition}) {{ }} }}\n");
14445            assert_eq!(
14446                abc_conditions(LANG::Perl, &in_for),
14447                abc_conditions(LANG::Perl, &in_if),
14448                "`{condition}` must score alike in a `for` header and an `if`",
14449            );
14450        }
14451    }
14452
14453    #[cfg(feature = "perl")]
14454    #[test]
14455    fn perl_short_circuit_counts_scalar_variable_operands() {
14456        // `$a && $b` reports 2 conditions — one walker count per
14457        // `ScalarVariable` operand. Renamed from the cross-language
14458        // `_with_boolean_literal_operand` convention because Perl has
14459        // no readily-grammar-exposed boolean literal in an `&&`
14460        // operand slot at the pinned grammar version (the `Boolean`
14461        // kind only fires on the `boolean` pragma's named constants,
14462        // not bareword `1` / `0`). Two scalar variables are the
14463        // grammar-stable terminal-set witness for Perl.
14464        check_metrics::<PerlParser>(
14465            "sub f { my ($a) = @_; return $a && $b; }\n",
14466            "foo.pl",
14467            |metric| {
14468                assert_eq!(metric.abc.conditions_sum(), 2);
14469                insta::assert_json_snapshot!(metric.abc);
14470            },
14471        );
14472    }
14473
14474    #[cfg(feature = "perl")]
14475    #[test]
14476    fn perl_array_in_binary_operand_descends_to_scalar_context_value() {
14477        // Regression test for the code-review findings on the
14478        // Phase-2B Perl walker:
14479        //   - Pre-fix-A: the former `perl_inspect_container` descended `Array`
14480        //     via `node.child(1)` — the FIRST element — wrongly
14481        //     attributing `$x` for `($x, $y)` (semantically `$y`
14482        //     is the scalar-context value).
14483        //   - Fix-A (the `array_is_paren` guard, 5db8078): dropped
14484        //     Array-as-paren entirely in `BinaryExpression` operand
14485        //     contexts to avoid the wrong attribution — but
14486        //     regressed `$a || ($x)` (single paren-grouped operand)
14487        //     to C=1 instead of 2.
14488        //   - Fix-B (this change): keeps Array-as-paren unconditional
14489        //     but descends via the LAST named child. `$a || ($x)`
14490        //     reaches `$x` (count both operands → 2);
14491        //     `$a || ($x, $y)` reaches `$y` (count `$a` + `$y` →
14492        //     still 2, matching Fitzpatrick Rule 7 "one per
14493        //     operand"); `if ($a)` still reaches `$a` (single-
14494        //     element grouping → 1).
14495        check_metrics::<PerlParser>(
14496            "sub f { my ($a, $x, $y) = @_;\n\
14497             \x20   my $r = $a || ($x, $y);    # +2c: $a + last-named $y\n\
14498             \x20   my $s = $a || ($x);        # +2c: $a + only-named $x\n\
14499             \x20   $r + $s;\n\
14500             }\n",
14501            "foo.pl",
14502            |metric| {
14503                // 2 + 2 = 4 unary conditions from the two `||`s.
14504                assert_eq!(metric.abc.conditions_sum(), 4);
14505                insta::assert_json_snapshot!(metric.abc);
14506            },
14507        );
14508    }
14509
14510    #[cfg(feature = "perl")]
14511    #[test]
14512    fn perl_if_scalar_variable_condition() {
14513        // Renamed from the cross-language
14514        // `_if_boolean_literal_condition` convention because
14515        // Perl has no readily-grammar-exposed boolean literal in
14516        // an `if (cond)` slot at the pinned grammar version:
14517        // tree-sitter-perl's `Boolean` kind only fires for the
14518        // `boolean` pragma's named constants (not bareword `1` /
14519        // `0`, which surface as `Integer` / not in the
14520        // terminal-bool set). A scalar-variable condition is the
14521        // grammar-stable witness — `if ($a)` reaches
14522        // `scalar_variable` via the `Array` paren unwrap.
14523        check_metrics::<PerlParser>(
14524            "sub f { my ($a) = @_; if ($a) { return 1; } }\n",
14525            "foo.pl",
14526            |metric| {
14527                assert_eq!(metric.abc.conditions_sum(), 1);
14528                insta::assert_json_snapshot!(metric.abc);
14529            },
14530        );
14531    }
14532
14533    #[cfg(feature = "perl")]
14534    #[test]
14535    fn perl_methods_arguments_with_conditions() {
14536        // `call(!$a, !$b)` — argument list walker counts each
14537        // unary-conditional argument once. Cannot use `m(...)` as
14538        // the function name — tree-sitter-perl parses `m(...)` as
14539        // the regex-match operator, not a function call.
14540        check_metrics::<PerlParser>(
14541            "sub f { my ($a, $b) = @_; call($a, $b); call(!$a, !$b); }\n",
14542            "foo.pl",
14543            |metric| {
14544                // Two calls × 1 branch each = 2 branches.
14545                // `call(!$a, !$b)` contributes 2 walker conditions
14546                // (one per `!`-wrapped scalar-variable argument);
14547                // `call($a, $b)` contributes 0 (bare-args don't
14548                // count via the Arguments walker — list_kind !=
14549                // BinaryExpression).
14550                assert_eq!(metric.abc.branches_sum(), 2);
14551                assert_eq!(metric.abc.conditions_sum(), 2);
14552                insta::assert_json_snapshot!(metric.abc);
14553            },
14554        );
14555    }
14556
14557    #[cfg(feature = "perl")]
14558    #[test]
14559    fn perl_return_with_conditions() {
14560        // `return !$a` reports 1 condition via the walker (unary
14561        // unwrap to scalar-variable terminal). `return $a` reports
14562        // 0 (no paren / unary wrap, has_boolean_content stays
14563        // false from ReturnExpression parent).
14564        check_metrics::<PerlParser>(
14565            "sub m1 { my ($z) = @_; return !($z); }\n\
14566             sub m2 { my ($x) = @_; return (((!$x))); }\n\
14567             sub m3 { my ($x, $y) = @_; return $x && $y; }\n",
14568            "foo.pl",
14569            |metric| {
14570                // m1: !($z) → walker on `!` unwraps paren to $z (1).
14571                // m2: (((!$x))) → walker unwraps three parens + one
14572                //     unary to $x (1).
14573                // m3: $x && $y → walker on `&&` counts both (2).
14574                // Sum: 4.
14575                assert_eq!(metric.abc.conditions_sum(), 4);
14576                insta::assert_json_snapshot!(metric.abc);
14577            },
14578        );
14579    }
14580
14581    // A bare `/re/` or `m{re}` is a match against `$_`, a relational
14582    // operator spelled with no operator token, so it scores by use
14583    // (#1467, the rule #1461 applied to five other languages). Each
14584    // `*_out` member and `ret` / `notm` / `grp` / `arg` / `smart` was 0
14585    // before the arm: only a boolean slot reached the pattern, through
14586    // the former `perl_bool_terminal_kinds!()`. `mbare_out` is the `m{}` spelling, a
14587    // sibling kind rather than an alias. `smart` levels a smartmatch
14588    // with its `=~` sibling: no arm counts `~~`, so the pattern does.
14589    //
14590    // The in-slot members are the §5 check that the slot stopped
14591    // counting the pattern in the change that gave it an arm: each
14592    // reads what it read before. `bound_out` / `nbound` pin the gate on
14593    // `=~` / `!~` — the operator token scores, the pattern under it does
14594    // not — and `ctrl_out` is the `==` control the rest level with.
14595    // `s///`, `tr///` and `qr//` are not matches and stay at 0; the
14596    // bound `s///` / `tr///` agree (#1540, the test below).
14597    #[cfg(feature = "perl")]
14598    #[test]
14599    fn perl_bare_match_is_a_condition_by_use() {
14600        check_func_space::<PerlParser, _>(
14601            "sub bare_out { my $r = /^#/; return $r; }\n\
14602             sub mbare_out { my $r = m{^#}; return $r; }\n\
14603             sub ret { return /x/; }\n\
14604             sub notm { my $r = !/x/; return $r; }\n\
14605             sub grp { my @a = @_; return grep { /x/ } @a; }\n\
14606             sub arg { foo(/x/); }\n\
14607             sub smart { my ($x) = @_; my $r = $x ~~ /re/; return $r; }\n\
14608             sub bare_in { if (/^#/) { return 1; } return 0; }\n\
14609             sub mbare_in { unless (m{^#}) { return 1; } return 0; }\n\
14610             sub modif { return 0 unless /x/; return 1; }\n\
14611             sub andy { my ($y) = @_; return /x/ && $y; }\n\
14612             sub oror { return /a/ || /b/; }\n\
14613             sub tern { return /x/ ? 1 : 0; }\n\
14614             sub forc { for (my $i = 0; /x/; $i++) { } }\n\
14615             sub bound_out { my ($x) = @_; my $r = ($x =~ /^#/); return $r; }\n\
14616             sub nbound { my ($x) = @_; my $r = ($x !~ /^#/); return $r; }\n\
14617             sub ctrl_out { my ($x) = @_; my $r = ($x == 1); return $r; }\n\
14618             sub subst { my $n = s/x/y/; return $n; }\n\
14619             sub tr1 { my $n = tr/a/b/; return $n; }\n\
14620             sub qrx { my $q = qr/x/; return $q; }\n",
14621            "foo.pl",
14622            |space| {
14623                assert_members_score(
14624                    &space,
14625                    &[
14626                        ("bare_out", 1, 1),
14627                        ("mbare_out", 1, 1),
14628                        ("ret", 1, 1),
14629                        // `!` adds nothing outside a slot; the match is
14630                        // the one condition.
14631                        ("notm", 1, 1),
14632                        ("grp", 1, 1),
14633                        ("arg", 1, 1),
14634                        ("smart", 1, 1),
14635                        ("bare_in", 1, 2),
14636                        ("mbare_in", 1, 2),
14637                        ("modif", 1, 2),
14638                        // One per `&&` / `||` operand, the match included.
14639                        ("andy", 2, 2),
14640                        ("oror", 2, 2),
14641                        // The ternary node plus its condition.
14642                        ("tern", 2, 2),
14643                        ("forc", 1, 2),
14644                        ("bound_out", 1, 1),
14645                        ("nbound", 1, 1),
14646                        ("ctrl_out", 1, 1),
14647                        ("subst", 0, 1),
14648                        ("tr1", 0, 1),
14649                        ("qrx", 0, 1),
14650                    ],
14651                );
14652            },
14653        );
14654    }
14655
14656    // A substitution or transliteration bound with `=~` / `!~` is the
14657    // same edit as its bare form on `$_`, so it scores by use exactly as
14658    // that twin does: 0 (#1540). Each `*_s` / `*_tr` / `*_y` row below
14659    // was 1 through the operator token before the gate; `count` (an
14660    // assigned count) and `sr` (a `/r` copy, a value) are uses too. A
14661    // match keeps its 1 whatever spells it — a pattern, `qr//`, or a
14662    // variable holding one. The `if_*` rows are the slot side: each pays
14663    // once, as its bare twin `if_s` does, because
14664    // `perl_condition_scores_itself` stops claiming a rewrite binding in
14665    // the same change; `tern_s` and `neg_s` reach that predicate through
14666    // a ternary condition and a `!`-proved `return` operand.
14667    #[cfg(feature = "perl")]
14668    #[test]
14669    fn perl_bound_rewrite_scores_like_its_bare_twin() {
14670        check_func_space::<PerlParser, _>(
14671            "sub ctrl { my ($x) = @_; $x; return $x; }\n\
14672             sub m_bare { my ($x) = @_; /re/; return $x; }\n\
14673             sub m_bound { my ($x) = @_; $x =~ /re/; return $x; }\n\
14674             sub m_nbound { my ($x) = @_; $x !~ /re/; return $x; }\n\
14675             sub m_qr { my ($x) = @_; $x =~ qr/re/; return $x; }\n\
14676             sub m_var { my ($x, $q) = @_; $x =~ $q; return $x; }\n\
14677             sub s_bare { my ($x) = @_; s/a/b/; return $x; }\n\
14678             sub s_bound { my ($x) = @_; $x =~ s/a/b/; return $x; }\n\
14679             sub s_nbound { my ($x) = @_; $x !~ s/a/b/; return $x; }\n\
14680             sub tr_bare { my ($x) = @_; tr/a/b/; return $x; }\n\
14681             sub tr_bound { my ($x) = @_; $x =~ tr/a/b/; return $x; }\n\
14682             sub tr_nbound { my ($x) = @_; $x !~ tr/a/b/; return $x; }\n\
14683             sub y_bound { my ($x) = @_; $x =~ y/a/b/; return $x; }\n\
14684             sub count { my ($x) = @_; my $n = ($x =~ s/a/b/g); return $x; }\n\
14685             sub sr { my ($x) = @_; my $r = $x =~ s/a/b/r; return $r; }\n\
14686             sub if_s { my ($x) = @_; if (s/a/b/) { g(); } return $x; }\n\
14687             sub if_m { my ($x) = @_; if ($x =~ /re/) { g(); } return $x; }\n\
14688             sub if_bs { my ($x) = @_; if ($x =~ s/a/b/) { g(); } return $x; }\n\
14689             sub if_nbs { my ($x) = @_; if ($x !~ s/a/b/) { g(); } return $x; }\n\
14690             sub if_btr { my ($x) = @_; if ($x =~ tr/a/b/) { g(); } return $x; }\n\
14691             sub if_by { my ($x) = @_; if ($x =~ y/a/b/) { g(); } return $x; }\n\
14692             sub if_bsr { my ($x) = @_; if ($x =~ s/a/b/r) { g(); } return $x; }\n\
14693             sub tern_s { my ($x) = @_; return ($x =~ s/a/b/) ? 1 : 0; }\n\
14694             sub neg_s { my ($x) = @_; return !($x =~ s/a/b/); }\n",
14695            "foo.pl",
14696            |space| {
14697                assert_members_score(
14698                    &space,
14699                    &[
14700                        ("ctrl", 0, 1),
14701                        ("m_bare", 1, 1),
14702                        ("m_bound", 1, 1),
14703                        ("m_nbound", 1, 1),
14704                        ("m_qr", 1, 1),
14705                        ("m_var", 1, 1),
14706                        ("s_bare", 0, 1),
14707                        ("s_bound", 0, 1),
14708                        ("s_nbound", 0, 1),
14709                        ("tr_bare", 0, 1),
14710                        ("tr_bound", 0, 1),
14711                        ("tr_nbound", 0, 1),
14712                        ("y_bound", 0, 1),
14713                        ("count", 0, 1),
14714                        ("sr", 0, 1),
14715                        ("if_s", 1, 2),
14716                        ("if_m", 1, 2),
14717                        ("if_bs", 1, 2),
14718                        ("if_nbs", 1, 2),
14719                        ("if_btr", 1, 2),
14720                        ("if_by", 1, 2),
14721                        ("if_bsr", 1, 2),
14722                        // The ternary node plus its condition slot.
14723                        ("tern_s", 2, 2),
14724                        ("neg_s", 1, 1),
14725                    ],
14726                );
14727            },
14728        );
14729    }
14730
14731    // The pattern `split` takes first is its delimiter, not a match
14732    // against `$_`, so it is no condition (#1467) in any spelling: the
14733    // spaced and parenthesised call forms, the `m{}` pattern, and the
14734    // `CORE::` qualifier. Three discriminating members keep the exclusion
14735    // narrow: `spl2` puts the pattern in `split`'s *second* slot, where
14736    // it is an ordinary match whose result `split` receives, `grpe`
14737    // puts one first in a call that is not `split`, and `tsplit` puts
14738    // one first in a list whose grandparent holds a bare `split` without
14739    // being that call — the ternary node plus `$c` plus the match.
14740    #[cfg(feature = "perl")]
14741    #[test]
14742    fn perl_split_delimiter_is_not_a_condition() {
14743        use crate::{ParserTrait, Perl};
14744
14745        let src = "sub spl { my ($s) = @_; my @p = split /,/, $s; return @p; }\n\
14746                   sub splp { my ($s) = @_; my @p = split(/,/, $s); return @p; }\n\
14747                   sub splm { my ($s) = @_; my @p = split m{,}, $s; return @p; }\n\
14748                   sub splc { my ($s) = @_; my @p = CORE::split(/,/, $s); return @p; }\n\
14749                   sub spl2 { my @p = split ' ', /x/; return @p; }\n\
14750                   sub grpe { my @a = @_; return grep /x/, @a; }\n\
14751                   sub tsplit { my ($c) = @_; my @r = $c ? split : (/x/); return @r; }\n";
14752
14753        // Fixture census: each member but `tsplit` holds exactly one
14754        // pattern, as a direct argument of its call — so a zero below
14755        // is the exclusion at work and not a pattern the fixture lost.
14756        let parser = PerlParser::new(
14757            src.as_bytes().to_vec(),
14758            &std::path::PathBuf::from("foo.pl"),
14759            None,
14760        );
14761        let call_arguments = parser
14762            .root()
14763            .preorder()
14764            .filter(|n| {
14765                matches!(
14766                    n.kind_id().into(),
14767                    Perl::PatternMatcher | Perl::PatternMatcherM
14768                )
14769            })
14770            .filter(|n| {
14771                n.parent()
14772                    .and_then(|list| list.parent())
14773                    .is_some_and(|call| {
14774                        matches!(
14775                            call.kind_id().into(),
14776                            Perl::CallExpressionWithSpacedArgs
14777                                | Perl::CallExpressionWithArgsWithBrackets
14778                        )
14779                    })
14780            })
14781            .count();
14782        assert_eq!(call_arguments, 6, "one call-argument pattern per member");
14783
14784        check_func_space::<PerlParser, _>(src, "foo.pl", |space| {
14785            assert_members_score(
14786                &space,
14787                &[
14788                    ("spl", 0, 1),
14789                    ("splp", 0, 1),
14790                    ("splm", 0, 1),
14791                    ("splc", 0, 1),
14792                    ("spl2", 1, 1),
14793                    ("grpe", 1, 1),
14794                    ("tsplit", 3, 2),
14795                ],
14796            );
14797        });
14798    }
14799
14800    // tree-sitter-perl names its separators, so a trailing `,` or `=>`
14801    // was read as the last element of a condition list and paid a slot
14802    // of its own: `if ($a > 1,)` scored 2. `perl -MO=Deparse` reduces
14803    // every `*_sep` row to its `*_twin`, which is where the expected
14804    // values come from; the chain rows put the list inside a `&&`
14805    // operand, which reaches the same peel through
14806    // `perl_count_chain_operands`.
14807    #[cfg(feature = "perl")]
14808    #[test]
14809    fn perl_trailing_separator_scores_like_its_bare_twin() {
14810        check_func_space::<PerlParser, _>(
14811            "sub cmp_twin { my ($a) = @_; if ($a > 1) { g(); } }\n\
14812             sub cmp_comma { my ($a) = @_; if ($a > 1,) { g(); } }\n\
14813             sub cmp_fat { my ($a) = @_; if ($a > 1 =>) { g(); } }\n\
14814             sub var_twin { my ($a) = @_; if ($a) { g(); } }\n\
14815             sub var_comma { my ($a) = @_; if ($a,) { g(); } }\n\
14816             sub chain_twin { my ($a, $b) = @_; if (($a > 1) && $b) { g(); } }\n\
14817             sub chain_comma { my ($a, $b) = @_; if (($a > 1,) && $b) { g(); } }\n",
14818            "foo.pl",
14819            |space| {
14820                assert_members_score(
14821                    &space,
14822                    &[
14823                        ("cmp_twin", 1, 2),
14824                        ("cmp_comma", 1, 2),
14825                        ("cmp_fat", 1, 2),
14826                        ("var_twin", 1, 2),
14827                        ("var_comma", 1, 2),
14828                        ("chain_twin", 2, 3),
14829                        ("chain_comma", 2, 3),
14830                    ],
14831                );
14832            },
14833        );
14834    }
14835
14836    // A pattern's role is read on the outermost `( … )` around it, which
14837    // is the value inside it, so each `*_paren` row scores as its bare
14838    // `*_twin` (#1467, #1540); `perl -MO=Deparse` prints each pair
14839    // identically. The reversed binding is the other direction: in
14840    // `/foo/ =~ $x` the pattern is the *left* operand, so it first
14841    // matches `$_` and that result is bound (Deparse: `/foo/ =~ /$x/`) —
14842    // the match and the `=~` both pay, as in the explicit `rev_twin`.
14843    #[cfg(feature = "perl")]
14844    #[test]
14845    fn perl_pattern_role_reads_through_parentheses() {
14846        check_func_space::<PerlParser, _>(
14847            "sub bind_twin { my ($x) = @_; return $x =~ /foo/; }\n\
14848             sub bind_paren { my ($x) = @_; return $x =~ (/foo/); }\n\
14849             sub bind_parens_m { my ($x) = @_; return $x =~ (((m{foo}))); }\n\
14850             sub bind_list { my ($x, $y) = @_; return $x =~ (/foo/, $y); }\n\
14851             sub bind_list_twin { my ($x, $y) = @_; /foo/; return $x =~ $y; }\n\
14852             sub split_twin { my ($x) = @_; my @p = split(/foo/, $x); return @p; }\n\
14853             sub split_paren { my ($x) = @_; my @p = split((/foo/), $x); return @p; }\n\
14854             sub split_sole { my @p = split(/foo/); return @p; }\n\
14855             sub subst_twin { my ($x) = @_; $x =~ s/a/b/; return $x; }\n\
14856             sub subst_paren { my ($x) = @_; $x =~ (s/a/b/); return $x; }\n\
14857             sub rev { my ($x) = @_; return /foo/ =~ $x; }\n\
14858             sub rev_twin { my ($x) = @_; my $m = /foo/; return $m =~ $x; }\n",
14859            "foo.pl",
14860            |space| {
14861                assert_members_score(
14862                    &space,
14863                    &[
14864                        ("bind_twin", 1, 1),
14865                        ("bind_paren", 1, 1),
14866                        ("bind_parens_m", 1, 1),
14867                        // A comma operator: `/foo/` matches `$_` and `$y`
14868                        // is what binds, so the match pays as in the twin.
14869                        ("bind_list", 2, 1),
14870                        ("bind_list_twin", 2, 1),
14871                        ("split_twin", 0, 1),
14872                        ("split_paren", 0, 1),
14873                        // The pattern is the whole argument list, which is
14874                        // the call's own and no wrapper to climb through.
14875                        ("split_sole", 0, 1),
14876                        ("subst_twin", 0, 1),
14877                        ("subst_paren", 0, 1),
14878                        ("rev", 2, 1),
14879                        ("rev_twin", 2, 1),
14880                    ],
14881                );
14882            },
14883        );
14884    }
14885
14886    // Only the builtin `split` takes its first pattern as a delimiter:
14887    // `My::split` is a user sub, so the pattern it is handed is an
14888    // ordinary match, as it is for `My::other` (#1467). `CORE::split`
14889    // is the builtin by its full name.
14890    #[cfg(feature = "perl")]
14891    #[test]
14892    fn perl_split_exemption_is_the_builtin_only() {
14893        check_func_space::<PerlParser, _>(
14894            "sub mine { my ($x) = @_; my @p = My::split(/foo/, $x); return @p; }\n\
14895             sub other { my ($x) = @_; my @p = My::other(/foo/, $x); return @p; }\n\
14896             sub core { my ($x) = @_; my @p = CORE::split(/foo/, $x); return @p; }\n",
14897            "foo.pl",
14898            |space| {
14899                assert_members_score(&space, &[("mine", 1, 1), ("other", 1, 1), ("core", 0, 1)]);
14900            },
14901        );
14902    }
14903
14904    // ---------- Lua ABC tests ----------
14905
14906    #[cfg(feature = "lua")]
14907    #[test]
14908    fn lua_empty_unit_zero() {
14909        check_metrics::<LuaParser>("", "empty.lua", |metric| {
14910            assert_eq!(metric.abc.assignments_sum(), 0);
14911            assert_eq!(metric.abc.branches_sum(), 0);
14912            assert_eq!(metric.abc.conditions_sum(), 0);
14913            insta::assert_json_snapshot!(metric.abc);
14914        });
14915    }
14916
14917    #[cfg(feature = "lua")]
14918    #[test]
14919    fn lua_assignments_count_locals_and_plain() {
14920        // `local x = 0` wraps an `assignment_statement` under a
14921        // `variable_declaration`; the inner wrapper still counts.
14922        // Multi-target assignment `a, b = 1, 2` is a single
14923        // `assignment_statement` and contributes 1, NOT 2 — the
14924        // wrapper is the unit of counting (matches the Python rule:
14925        // one `Assignment` node, one assignment).
14926        check_metrics::<LuaParser>(
14927            "function f()\n\
14928                 local x = 0\n\
14929                 x = 1\n\
14930                 local a, b = 1, 2\n\
14931                 a, b = b, a\n\
14932             end",
14933            "foo.lua",
14934            |metric| {
14935                assert_eq!(metric.abc.assignments_sum(), 4);
14936                assert_eq!(metric.abc.branches_sum(), 0);
14937                assert_eq!(metric.abc.conditions_sum(), 0);
14938                insta::assert_json_snapshot!(metric.abc);
14939            },
14940        );
14941    }
14942
14943    #[cfg(feature = "lua")]
14944    #[test]
14945    fn lua_calls_are_branches() {
14946        // `print(x)`, `obj.m(x)`, `obj:m(x)`, `f(g(1))` — every
14947        // call form is a `function_call` node. The nested
14948        // `f(g(1))` counts as 2 branches (one per dispatch).
14949        check_metrics::<LuaParser>(
14950            "function r(x)\n\
14951                 print(x)\n\
14952                 obj.m(x)\n\
14953                 obj:m(x)\n\
14954                 return f(g(1))\n\
14955             end",
14956            "foo.lua",
14957            |metric| {
14958                assert_eq!(metric.abc.assignments_sum(), 0);
14959                assert_eq!(metric.abc.branches_sum(), 5);
14960                assert_eq!(metric.abc.conditions_sum(), 0);
14961                insta::assert_json_snapshot!(metric.abc);
14962            },
14963        );
14964    }
14965
14966    #[cfg(feature = "lua")]
14967    #[test]
14968    fn lua_comparisons_count_logical_ops_do_not() {
14969        // Each comparison token contributes one condition; `and` /
14970        // `or` are NOT counted on their own (Fitzpatrick Rule 5;
14971        // #395) — instead each operand is counted as a unary
14972        // conditional by the walker (Rule 9; #403). The two
14973        // `a and b` / `a or b` lines add 2 walker conditions each.
14974        check_metrics::<LuaParser>(
14975            "function f(a, b)\n\
14976                 local r\n\
14977                 r = a == b\n\
14978                 r = a ~= b\n\
14979                 r = a <  b\n\
14980                 r = a >  b\n\
14981                 r = a <= b\n\
14982                 r = a >= b\n\
14983                 r = a and b\n\
14984                 r = a or  b\n\
14985             end",
14986            "foo.lua",
14987            |metric| {
14988                // 8 `r = …` reassignments, plus `local r` (no `=`).
14989                assert_eq!(metric.abc.assignments_sum(), 8);
14990                assert_eq!(metric.abc.branches_sum(), 0);
14991                // 6 comparisons (+6) + 2 logical lines × 2 walker
14992                // operands (+4) = 10.
14993                assert_eq!(metric.abc.conditions_sum(), 10);
14994                insta::assert_json_snapshot!(metric.abc);
14995            },
14996        );
14997    }
14998
14999    // #1297: Lua 5.4 brackets a variable attribute with the same two
15000    // bare tokens a comparison uses, and the `<` / `>` arm had no gate,
15001    // so `local x <const> = 1` scored two conditions. Two attributes
15002    // against one genuine `a < b` separate every mis-aim: 5 pre-fix, 1
15003    // once the arm allows `BinaryExpression`, 0 if it allows the wrong
15004    // parent or the fixture stops parsing.
15005    //
15006    // No fixture-presence anchor is available, unlike the JSX and Perl
15007    // fixtures: once excluded, a Lua attribute contributes to no ABC
15008    // axis at all (`local x <const> = 1` and `local x = 1` score
15009    // identically on assignments, branches and conditions), and it
15010    // opens no space. Deleting the two attributes would leave this test
15011    // passing on the `a < b` alone. The coverage that the attributes
15012    // are reaching the arm is the revert test: removing the gate makes
15013    // this the only failing test in the suite.
15014    #[cfg(feature = "lua")]
15015    #[test]
15016    fn lua_variable_attributes_are_not_conditions() {
15017        check_metrics::<LuaParser>(
15018            "local function f(a, b)\n\
15019                 local x <const> = 1\n\
15020                 local y <close> = nil\n\
15021                 if a < b then return x end\n\
15022                 return y\n\
15023             end",
15024            "foo.lua",
15025            |metric| {
15026                assert_eq!(metric.abc.conditions_sum(), 1);
15027            },
15028        );
15029    }
15030
15031    #[cfg(feature = "lua")]
15032    #[test]
15033    fn lua_elseif_and_else_count_conditions() {
15034        // Each elseif / else arm of the if contributes one
15035        // condition, mirroring the Python rule.
15036        check_metrics::<LuaParser>(
15037            "function f(x)\n\
15038                 if x > 0 then\n\
15039                     return 1\n\
15040                 elseif x < 0 then\n\
15041                     return -1\n\
15042                 else\n\
15043                     return 0\n\
15044                 end\n\
15045             end",
15046            "foo.lua",
15047            |metric| {
15048                // Comparisons: `>`, `<` → 2; elseif_statement → 1;
15049                // else_statement → 1. C = 4. No branches (no calls).
15050                assert_eq!(metric.abc.assignments_sum(), 0);
15051                assert_eq!(metric.abc.branches_sum(), 0);
15052                assert_eq!(metric.abc.conditions_sum(), 4);
15053                insta::assert_json_snapshot!(metric.abc);
15054            },
15055        );
15056    }
15057
15058    #[cfg(feature = "lua")]
15059    #[test]
15060    fn lua_complex_function_abc() {
15061        // Combines every category to pin the metric.
15062        check_metrics::<LuaParser>(
15063            "function run(n)\n\
15064                 local total = 0\n\
15065                 for i = 1, n do\n\
15066                     if i % 2 == 0 then\n\
15067                         do_work(i)\n\
15068                     else\n\
15069                         total = total + i\n\
15070                     end\n\
15071                 end\n\
15072                 print(\"done\")\n\
15073                 return total\n\
15074             end",
15075            "foo.lua",
15076            |metric| {
15077                // Assignments: `local total = 0` (1), `total = total + i` (1) → 2.
15078                // Branches: `do_work(i)` (1), `print(\"done\")` (1) → 2.
15079                // Conditions: `==` (1), `else_statement` (1) → 2.
15080                assert_eq!(metric.abc.assignments_sum(), 2);
15081                assert_eq!(metric.abc.branches_sum(), 2);
15082                assert_eq!(metric.abc.conditions_sum(), 2);
15083                insta::assert_json_snapshot!(metric.abc);
15084            },
15085        );
15086    }
15087
15088    #[cfg(feature = "lua")]
15089    #[test]
15090    fn lua_if_multiple_conditions() {
15091        // Fitzpatrick Rule 9 walker (issue #403). Lua's `and` / `or`
15092        // are keyword tokens inside a `binary_expression`.
15093        check_metrics::<LuaParser>(
15094            "function f(a, b, c, d)\n\
15095                 if a or b or c or d then return 1 end       -- +4c\n\
15096                 if a and b and c then return 2 end          -- +3c\n\
15097                 if not a and not b then return 3 end        -- +2c\n\
15098                 return 0\n\
15099             end",
15100            "foo.lua",
15101            |metric| {
15102                assert_eq!(metric.abc.conditions_sum(), 9);
15103                insta::assert_json_snapshot!(metric.abc);
15104            },
15105        );
15106    }
15107
15108    #[cfg(feature = "lua")]
15109    #[test]
15110    fn lua_while_conditions() {
15111        // Lua has no `do { ... } while(cond);` — `while cond do …
15112        // end` and `repeat … until cond` are the loop forms.
15113        check_metrics::<LuaParser>(
15114            "function f(a, b)\n\
15115                 while a or b do break end                   -- +2c\n\
15116                 repeat break until a and not b              -- +2c\n\
15117             end",
15118            "foo.lua",
15119            |metric| {
15120                assert_eq!(metric.abc.conditions_sum(), 4);
15121                insta::assert_json_snapshot!(metric.abc);
15122            },
15123        );
15124    }
15125
15126    #[cfg(feature = "lua")]
15127    #[test]
15128    fn lua_short_circuit_with_boolean_literal_operand() {
15129        // `a and true` reports 2 conditions: one Identifier, one
15130        // True keyword literal.
15131        check_metrics::<LuaParser>("function f(a) return a and true end", "foo.lua", |metric| {
15132            assert_eq!(metric.abc.conditions_sum(), 2);
15133            insta::assert_json_snapshot!(metric.abc);
15134        });
15135    }
15136
15137    #[cfg(feature = "lua")]
15138    #[test]
15139    fn lua_number_truthy_condition_counts() {
15140        // Regression for findings.md #2: Lua treats every non-nil,
15141        // non-false value as truthy, so `if 1 then ... end` and
15142        // `return a and 2` should each count their numeric literal
15143        // as a Fitzpatrick Rule 6 / 7 unary condition. Pre-fix,
15144        // the former `lua_bool_terminal_kinds!()` listed `True` /
15145        // `False` / `Nil` but omitted `Number`, so the walker dropped
15146        // every numeric-truthy operand. The walker comment at the top
15147        // of the former `lua_inspect_container` already promised
15148        // numbers were terminal-bool kinds; this commit closed the gap.
15149        check_metrics::<LuaParser>(
15150            "function f(a)\n\
15151             \x20   if 1 then return 1 end\n\
15152             \x20   return a and 2\n\
15153             end",
15154            "foo.lua",
15155            |metric| {
15156                // `if 1 then` → walker counts the Number literal (+1).
15157                // `a and 2` → `and` walker counts both operands:
15158                //   identifier `a` (+1), Number `2` (+1).
15159                // Total: 3.
15160                assert_eq!(metric.abc.conditions_sum(), 3);
15161                insta::assert_json_snapshot!(metric.abc);
15162            },
15163        );
15164    }
15165
15166    #[cfg(feature = "lua")]
15167    #[test]
15168    fn lua_if_boolean_literal_condition() {
15169        check_metrics::<LuaParser>(
15170            "function f()\n\
15171                 if true then end                  -- +1c\n\
15172                 if not false then end             -- +1c\n\
15173                 while true do break end           -- +1c\n\
15174                 repeat break until false          -- +1c\n\
15175             end",
15176            "foo.lua",
15177            |metric| {
15178                assert_eq!(metric.abc.conditions_sum(), 4);
15179                insta::assert_json_snapshot!(metric.abc);
15180            },
15181        );
15182    }
15183
15184    #[cfg(feature = "lua")]
15185    #[test]
15186    fn lua_methods_arguments_with_conditions() {
15187        // `m(not a, not b)` — argument list walker counts each
15188        // unary-conditional argument once. Bare-identifier args
15189        // (`m(a, b)`) do not count (list_kind != BinaryExpression).
15190        check_metrics::<LuaParser>(
15191            "function f(a, b) m(a, b); m(not a, not b) end",
15192            "foo.lua",
15193            |metric| {
15194                assert_eq!(metric.abc.branches_sum(), 2);
15195                assert_eq!(metric.abc.conditions_sum(), 2);
15196                insta::assert_json_snapshot!(metric.abc);
15197            },
15198        );
15199    }
15200
15201    #[cfg(feature = "lua")]
15202    #[test]
15203    fn lua_return_with_conditions() {
15204        // `return not (z >= 0)` → walker on `not` unwraps the paren
15205        // chain and reaches the inner BinaryExpression; the inner
15206        // `>=` comparison is the actual Fitzpatrick condition.
15207        check_metrics::<LuaParser>(
15208            "function m1(z) return not (z >= 0) end\n\
15209             function m2(x) return (((not x))) end\n\
15210             function m3(x, y) return x and y end",
15211            "foo.lua",
15212            |metric| {
15213                // m1: `>=` (1). `not` wraps a paren'd
15214                //     BinaryExpression — `lua_count_negated` peels it
15215                //     to the comparison, which scores itself, so the
15216                //     return value pays nothing extra. +1.
15217                // m2: ReturnStatement → iterate expression_list →
15218                //     `lua_count_negated` peels the parens and the
15219                //     `not` (which proves boolean) down to `x`, which
15220                //     does not score itself. +1.
15221                // m3: x and y → `and` walker counts both → +2.
15222                // Sum: 4.
15223                assert_eq!(metric.abc.conditions_sum(), 4);
15224                insta::assert_json_snapshot!(metric.abc);
15225            },
15226        );
15227    }
15228
15229    // ---------- Tcl ABC tests ----------
15230
15231    #[cfg(feature = "tcl")]
15232    #[test]
15233    fn tcl_empty_unit_zero() {
15234        check_metrics::<TclParser>("", "empty.tcl", |metric| {
15235            assert_eq!(metric.abc.assignments_sum(), 0);
15236            assert_eq!(metric.abc.branches_sum(), 0);
15237            assert_eq!(metric.abc.conditions_sum(), 0);
15238            insta::assert_json_snapshot!(metric.abc);
15239        });
15240    }
15241
15242    #[cfg(feature = "tcl")]
15243    #[test]
15244    fn tcl_set_command_counts_assignment() {
15245        // `set` has its own grammar production; each invocation is
15246        // one assignment.
15247        check_metrics::<TclParser>(
15248            "proc f {} {\n\
15249                 set x 1\n\
15250                 set y 2\n\
15251                 set x [expr {$x + $y}]\n\
15252             }",
15253            "foo.tcl",
15254            |metric| {
15255                // 3 `set` invocations → A=3. The inner `expr` is a
15256                // sub-command (`command_substitution` + `expr_cmd`),
15257                // not a `command` node, so it doesn't add a branch.
15258                assert_eq!(metric.abc.assignments_sum(), 3);
15259                assert_eq!(metric.abc.branches_sum(), 0);
15260                assert_eq!(metric.abc.conditions_sum(), 0);
15261                insta::assert_json_snapshot!(metric.abc);
15262            },
15263        );
15264    }
15265
15266    #[cfg(feature = "tcl")]
15267    #[test]
15268    fn tcl_incr_append_lappend_count_assignment() {
15269        // Variable-mutation commands (`incr`, `append`, `lappend`)
15270        // are recognised by name and count as assignments, not
15271        // branches.
15272        check_metrics::<TclParser>(
15273            "proc f {} {\n\
15274                 set x 0\n\
15275                 incr x\n\
15276                 append s \"hi\"\n\
15277                 lappend lst 1\n\
15278             }",
15279            "foo.tcl",
15280            |metric| {
15281                // `set` (1) + `incr` (1) + `append` (1) + `lappend`
15282                // (1) → A=4. No branches, no conditions.
15283                assert_eq!(metric.abc.assignments_sum(), 4);
15284                assert_eq!(metric.abc.branches_sum(), 0);
15285                assert_eq!(metric.abc.conditions_sum(), 0);
15286                insta::assert_json_snapshot!(metric.abc);
15287            },
15288        );
15289    }
15290
15291    #[cfg(feature = "tcl")]
15292    #[test]
15293    fn tcl_qualified_mutator_commands_count_assignment() {
15294        // `::incr` is `incr` through the global namespace. Anchored on
15295        // `branches_sum() == 0` as well as the assignment total: an
15296        // unresolved name falls to the branch arm, so the two columns
15297        // move in opposite directions and no single total can mask the
15298        // regression (#1381 review).
15299        check_metrics::<TclParser>(
15300            "proc f {} {\n\
15301                 ::incr x\n\
15302                 ::append s \"hi\"\n\
15303                 ::lappend lst 1\n\
15304             }",
15305            "foo.tcl",
15306            |metric| {
15307                assert_eq!(metric.abc.assignments_sum(), 3);
15308                assert_eq!(metric.abc.branches_sum(), 0);
15309                assert_eq!(metric.abc.conditions_sum(), 0);
15310            },
15311        );
15312    }
15313
15314    #[cfg(feature = "tcl")]
15315    #[test]
15316    fn tcl_namespaced_mutator_command_stays_a_branch() {
15317        // Control: only the *leading* qualifier names the core command,
15318        // so `ns::incr` is a user proc and keeps the branch column — the
15319        // direction `BRACED_WORD_VALUE_CASES` pins for `ns::eval`.
15320        check_metrics::<TclParser>(
15321            "proc f {} {\n\
15322                 ns::incr x\n\
15323             }",
15324            "foo.tcl",
15325            |metric| {
15326                assert_eq!(metric.abc.assignments_sum(), 0);
15327                assert_eq!(metric.abc.branches_sum(), 1);
15328            },
15329        );
15330    }
15331
15332    #[cfg(feature = "tcl")]
15333    #[test]
15334    fn tcl_computed_command_name_is_not_an_assignment() {
15335        // A command whose leading word is computed (`$cmd args`) names no
15336        // builtin the parser can resolve, so it stays a branch. Pins the
15337        // field-addressed, `simple_word`-gated read the classifier shares
15338        // with the Cognitive / Cyclomatic detectors (grammar-dispatch §3):
15339        // the earlier `child(0)` byte-slice addressed the slot by position
15340        // and compared whatever literal text sat there, which agrees with
15341        // the gated read on today's grammar only because no mutator name is
15342        // spelled with a leading `$`. A grammar that moved the name out of
15343        // `child(0)`, or a mutator list that grew a computed-looking entry,
15344        // would diverge — this fixture is where that surfaces.
15345        check_metrics::<TclParser>(
15346            "proc f {cmd x} {\n\
15347                 $cmd $x\n\
15348                 incr x\n\
15349             }",
15350            "foo.tcl",
15351            |metric| {
15352                // `incr x` is the only assignment; `$cmd $x` is a branch.
15353                assert_eq!(metric.abc.assignments_sum(), 1);
15354                assert_eq!(metric.abc.branches_sum(), 1);
15355                assert_eq!(metric.abc.conditions_sum(), 0);
15356            },
15357        );
15358    }
15359
15360    #[cfg(feature = "tcl")]
15361    #[test]
15362    fn tcl_generic_commands_are_branches() {
15363        // Anything that isn't `set` or a known mutator command
15364        // counts as a branch — including builtins like `puts` and
15365        // `return`.
15366        check_metrics::<TclParser>(
15367            "proc f {} {\n\
15368                 puts \"hello\"\n\
15369                 do_work 1 2\n\
15370                 return 0\n\
15371             }",
15372            "foo.tcl",
15373            |metric| {
15374                // 3 commands, all branches.
15375                assert_eq!(metric.abc.assignments_sum(), 0);
15376                assert_eq!(metric.abc.branches_sum(), 3);
15377                assert_eq!(metric.abc.conditions_sum(), 0);
15378                insta::assert_json_snapshot!(metric.abc);
15379            },
15380        );
15381    }
15382
15383    #[cfg(feature = "tcl")]
15384    #[test]
15385    fn tcl_comparisons_count_logical_ops_do_not() {
15386        // `expr` predicates expose comparison / logical tokens at
15387        // the leaf level. Each comparison token contributes one
15388        // condition; `&&` and `||` are NOT counted on their own
15389        // (Fitzpatrick Rule 5; #395) — instead each operand is
15390        // counted as a unary conditional by the walker (Rule 9;
15391        // #403). The two logical lines add 2 walker conditions
15392        // each (variable-substitution operands).
15393        check_metrics::<TclParser>(
15394            "proc f {a b} {\n\
15395                 set r [expr {$a == $b}]\n\
15396                 set r [expr {$a != $b}]\n\
15397                 set r [expr {$a <  $b}]\n\
15398                 set r [expr {$a >  $b}]\n\
15399                 set r [expr {$a <= $b}]\n\
15400                 set r [expr {$a >= $b}]\n\
15401                 set r [expr {$a eq $b}]\n\
15402                 set r [expr {$a ne $b}]\n\
15403                 set r [expr {$a && $b}]\n\
15404                 set r [expr {$a || $b}]\n\
15405             }",
15406            "foo.tcl",
15407            |metric| {
15408                // 10 `set` assignments.
15409                assert_eq!(metric.abc.assignments_sum(), 10);
15410                assert_eq!(metric.abc.branches_sum(), 0);
15411                // 8 comparisons (+8) + 2 logical lines × 2 walker
15412                // operands (+4) = 12.
15413                assert_eq!(metric.abc.conditions_sum(), 12);
15414                insta::assert_json_snapshot!(metric.abc);
15415            },
15416        );
15417    }
15418
15419    #[cfg(feature = "tcl")]
15420    #[test]
15421    fn tcl_ternary_counts_condition() {
15422        // The `ternary_expr` node is one condition and its condition
15423        // slot `$a` — a bare truthy test — is another, matching C++'s
15424        // `int r = a ? b : c;` (also 2). The two branch operands are
15425        // unnegated and so contribute nothing (#1180).
15426        check_metrics::<TclParser>(
15427            "proc f {a b c} {\n\
15428                 set r [expr {$a ? $b : $c}]\n\
15429             }",
15430            "foo.tcl",
15431            |metric| {
15432                assert_eq!(metric.abc.assignments_sum(), 1);
15433                assert_eq!(metric.abc.branches_sum(), 0);
15434                assert_eq!(metric.abc.conditions_sum(), 2);
15435                insta::assert_json_snapshot!(metric.abc);
15436            },
15437        );
15438    }
15439
15440    /// The Tcl half of the ternary slot-location guard (#1180).
15441    ///
15442    /// `ternary_expr` exposes no grammar fields, so the slots are found
15443    /// relative to the `?` and `:` tokens. A *parenthesised* condition is
15444    /// the input that discriminates that from a fixed-index reading:
15445    /// `_expr` inlines `( … )` as anonymous children of `ternary_expr`,
15446    /// so `($a) ? !$b : !$c` shifts every operand right by one and
15447    /// `child(0)` / `child(2)` / `child(4)` land on `(`, `)` and `?`.
15448    /// Without this case the whole fixed-index revert passes.
15449    #[cfg(feature = "tcl")]
15450    #[test]
15451    fn tcl_parenthesised_ternary_condition_matches_the_bare_form() {
15452        let conditions = |source: &str| {
15453            crate::test_support::metrics_verbatim(
15454                crate::LANG::Tcl,
15455                source.as_bytes(),
15456                crate::MetricsOptions::default(),
15457            )
15458            .abc
15459            .conditions_sum()
15460        };
15461        let bare = conditions("proc f {a b c} {\n set r [expr {$a ? !$b : !$c}]\n}");
15462        assert_eq!(bare, 4, "the bare form is the documented reference value");
15463        assert_eq!(
15464            conditions("proc f {a b c} {\n set r [expr {($a) ? !$b : !$c}]\n}"),
15465            bare,
15466            "parenthesising the condition must not change the count"
15467        );
15468    }
15469
15470    /// A parenthesised operand under `!` counts the same as a bare one.
15471    ///
15472    /// `_expr` inlines `( … )` as anonymous children, so `!($a)` puts
15473    /// `(` where a positional read expects the operand. The walker's
15474    /// negation branch kept a fixed `child(1)` through the first draft of
15475    /// #1180 and scored these 0 while the unparenthesised forms scored 1
15476    /// — an inconsistency the fix itself introduced, since before it
15477    /// neither form counted. Found in review, not by the tests: the
15478    /// parenthesised fixtures added with #1180 covered the ternary
15479    /// *condition* slot only.
15480    #[cfg(feature = "tcl")]
15481    #[test]
15482    fn tcl_parenthesised_negated_operands_match_the_bare_form() {
15483        let conditions = |source: &str| {
15484            crate::test_support::metrics_verbatim(
15485                crate::LANG::Tcl,
15486                source.as_bytes(),
15487                crate::MetricsOptions::default(),
15488            )
15489            .abc
15490            .conditions_sum()
15491        };
15492        for (bare, parenthesised) in [
15493            (
15494                "proc f {a} {\n if {!$a} { puts x }\n}",
15495                "proc f {a} {\n if {!($a)} { puts x }\n}",
15496            ),
15497            (
15498                "proc f {a b} {\n if {$a && !$b} { puts x }\n}",
15499                "proc f {a b} {\n if {$a && !($b)} { puts x }\n}",
15500            ),
15501            (
15502                "proc f {a b c} {\n set r [expr {$a ? !$b : !$c}]\n}",
15503                "proc f {a b c} {\n set r [expr {$a ? !($b) : !$c}]\n}",
15504            ),
15505        ] {
15506            let want = conditions(bare);
15507            assert!(want > 0, "the bare form must count something: {bare}");
15508            assert_eq!(
15509                conditions(parenthesised),
15510                want,
15511                "parenthesising the negated operand changed the count\n  bare:  {bare}\n  paren: {parenthesised}"
15512            );
15513        }
15514    }
15515
15516    #[cfg(feature = "irules")]
15517    #[test]
15518    fn irules_abc_parenthesised_negated_operands_match_the_bare_form() {
15519        let conditions = |source: &str| {
15520            crate::test_support::metrics_verbatim(
15521                crate::LANG::Irules,
15522                source.as_bytes(),
15523                crate::MetricsOptions::default(),
15524            )
15525            .abc
15526            .conditions_sum()
15527        };
15528        let want = conditions("when X {\n    if { !$a } { log local0. hi }\n}\n");
15529        assert_eq!(want, 1);
15530        assert_eq!(
15531            conditions("when X {\n    if { !($a) } { log local0. hi }\n}\n"),
15532            want
15533        );
15534    }
15535
15536    #[cfg(feature = "tcl")]
15537    #[test]
15538    fn tcl_bare_truthy_and_negated_predicates_count_one_condition() {
15539        // The headline #1180 fix, on the Tcl side: both were 0 before.
15540        let conditions = |source: &str| {
15541            crate::test_support::metrics_verbatim(
15542                crate::LANG::Tcl,
15543                source.as_bytes(),
15544                crate::MetricsOptions::default(),
15545            )
15546            .abc
15547            .conditions_sum()
15548        };
15549        assert_eq!(conditions("proc f {a} {\n if {$a} { puts x }\n}"), 1);
15550        assert_eq!(conditions("proc f {a} {\n if {!$a} { puts x }\n}"), 1);
15551        assert_eq!(conditions("proc f {a} {\n while {$a} { puts x }\n}"), 1);
15552        assert_eq!(conditions("proc f {a} {\n while {!$a} { puts x }\n}"), 1);
15553    }
15554
15555    #[cfg(feature = "tcl")]
15556    #[test]
15557    fn tcl_bare_truthy_elseif_predicate_counts_one_condition() {
15558        // The `Tcl::Elseif` arm routes its predicate through
15559        // `tcl_condition_expr` exactly as `If` / `While` do (#1180), but
15560        // a comparison predicate is counted by its leaf operator, so it
15561        // cannot tell the routing from its absence. Only a bare truthy
15562        // predicate can: `$b` scores through the routed walker or not at
15563        // all.
15564        let conditions = |source: &str| {
15565            crate::test_support::metrics_verbatim(
15566                crate::LANG::Tcl,
15567                source.as_bytes(),
15568                crate::MetricsOptions::default(),
15569            )
15570            .abc
15571            .conditions_sum()
15572        };
15573        // `$a` truthy (1) + `elseif` clause (1) + `$b` truthy (1).
15574        assert_eq!(
15575            conditions("proc f {a b} {\n if {$a} { puts x } elseif {$b} { puts y }\n}"),
15576            3
15577        );
15578    }
15579
15580    #[cfg(feature = "tcl")]
15581    #[test]
15582    fn tcl_elseif_and_else_count_conditions() {
15583        // `if` / `elseif` / `else` clause productions each
15584        // contribute one condition. The leaf comparison inside the
15585        // predicate is counted independently.
15586        check_metrics::<TclParser>(
15587            "proc f {x} {\n\
15588                 if {$x > 0} {\n\
15589                     return 1\n\
15590                 } elseif {$x < 0} {\n\
15591                     return -1\n\
15592                 } else {\n\
15593                     return 0\n\
15594                 }\n\
15595             }",
15596            "foo.tcl",
15597            |metric| {
15598                // Branches: three `return` commands → 3.
15599                // Conditions: `>` (1), `<` (1), `elseif` (1), `else`
15600                // (1) → 4.
15601                assert_eq!(metric.abc.assignments_sum(), 0);
15602                assert_eq!(metric.abc.branches_sum(), 3);
15603                assert_eq!(metric.abc.conditions_sum(), 4);
15604                insta::assert_json_snapshot!(metric.abc);
15605            },
15606        );
15607    }
15608
15609    #[cfg(feature = "tcl")]
15610    #[test]
15611    fn tcl_if_multiple_conditions() {
15612        // Fitzpatrick Rule 9 walker (issue #403). Tcl's `expr` slot
15613        // exposes `&&` / `||` operands as variable substitutions
15614        // (`$a`, `$b`, …) inside a `binop_expr`.
15615        check_metrics::<TclParser>(
15616            "proc f {a b c d} {\n\
15617                 if {[expr {$a || $b || $c || $d}]} { return 1 }    \n\
15618                 if {[expr {$a && $b && $c}]} { return 2 }          \n\
15619                 return 0\n\
15620             }",
15621            "foo.tcl",
15622            |metric| {
15623                // The two chains feed the walker: 4 + 3 = 7. Each `if`
15624                // predicate is additionally a bare truthy test of a
15625                // command substitution — `{[expr {…}]}` is structurally
15626                // `if {[somecmd]}`, which counts 1 exactly as `if {$a}`
15627                // does — so 7 + 2 = 9 (#1180). Written without the
15628                // redundant `[expr …]` wrapper, `if {$a || $b}` scores
15629                // 2, matching C++'s `if (a || b)`.
15630                assert_eq!(metric.abc.conditions_sum(), 9);
15631                insta::assert_json_snapshot!(metric.abc);
15632            },
15633        );
15634    }
15635
15636    #[cfg(feature = "tcl")]
15637    #[test]
15638    fn tcl_while_conditions() {
15639        // Tcl has no `do { ... } while(cond);` — `while {…} {…}` is
15640        // the standard loop. The walker fires on `&&` / `||` tokens
15641        // inside the `expr` predicate.
15642        check_metrics::<TclParser>(
15643            "proc f {a b} {\n\
15644                 while {[expr {$a || $b}]} { break }    \n\
15645                 while {[expr {$a && $b}]} { break }    \n\
15646             }",
15647            "foo.tcl",
15648            |metric| {
15649                // 2 + 2 from the chains, plus one bare truthy test per
15650                // `while` predicate — see `tcl_if_multiple_conditions`
15651                // (#1180).
15652                assert_eq!(metric.abc.conditions_sum(), 6);
15653                insta::assert_json_snapshot!(metric.abc);
15654            },
15655        );
15656    }
15657
15658    #[cfg(feature = "tcl")]
15659    #[test]
15660    fn tcl_short_circuit_with_boolean_literal_operand() {
15661        // `$a && 1` reports 2 conditions: a VariableSubstitution
15662        // operand plus a Number-literal operand. Confirms `Number`
15663        // is in the walker terminal set. `true` / `false` Tcl
15664        // keywords are not literal tokens in tree-sitter-tcl —
15665        // they're emitted as the operator-context word, which is
15666        // captured separately by the `Tcl::Boolean` kind for
15667        // dedicated `expr {true}` predicates but not as a `&&`
15668        // operand at this iteration; using a numeric literal keeps
15669        // the assertion grammar-stable.
15670        check_metrics::<TclParser>(
15671            "proc f {a} { return [expr {$a && 1}] }\n",
15672            "foo.tcl",
15673            |metric| {
15674                assert_eq!(metric.abc.conditions_sum(), 2);
15675                insta::assert_json_snapshot!(metric.abc);
15676            },
15677        );
15678    }
15679
15680    #[cfg(feature = "tcl")]
15681    #[test]
15682    fn tcl_complex_function_abc() {
15683        // Mixed program covering every category. Tcl's grammar
15684        // re-parses braced content that looks command-shaped as a
15685        // nested `command` node, which inflates the branch count
15686        // relative to a naive read of the source — see breakdown.
15687        check_metrics::<TclParser>(
15688            "proc run {n} {\n\
15689                 set total 0\n\
15690                 for {set i 0} {$i < $n} {incr i} {\n\
15691                     if {$i % 2 == 0} {\n\
15692                         do_work $i\n\
15693                     } else {\n\
15694                         incr total $i\n\
15695                     }\n\
15696                 }\n\
15697                 puts \"done\"\n\
15698                 return $total\n\
15699             }",
15700            "foo.tcl",
15701            |metric| {
15702                // Assignments: `set total 0` (1), `set i 0` (1),
15703                // `incr i` (1), `incr total $i` (1) → A = 4.
15704                // Branches: the outer `for …` is one `command` node;
15705                // the `{$i < $n}` predicate ALSO re-parses as a
15706                // `command` node (tree-sitter-tcl treats braced
15707                // predicates as nested commands at the pinned
15708                // grammar version); plus `do_work $i`, `puts
15709                // "done"`, and `return $total`. The for-loop body's
15710                // `incr` and `incr total $i` are assignment commands
15711                // and don't add branches. Total B = 5.
15712                // Conditions: `==` (1) and `else` (1) → C = 2. The
15713                // `<` inside `{$i < $n}` is NOT `Tcl::LT`: because
15714                // that predicate re-parses as a `command`, the `<`
15715                // is emitted as `simple_word`. Only `<` inside a
15716                // real `expr` production becomes `Tcl::LT`.
15717                assert_eq!(metric.abc.assignments_sum(), 4);
15718                assert_eq!(metric.abc.branches_sum(), 5);
15719                assert_eq!(metric.abc.conditions_sum(), 2);
15720                insta::assert_json_snapshot!(metric.abc);
15721            },
15722        );
15723    }
15724
15725    /// The dedicated `set name value` production counts as one assignment.
15726    #[cfg(feature = "irules")]
15727    #[test]
15728    fn irules_abc_set_assignment() {
15729        check_metrics::<IrulesParser>("when X {\n    set x 1\n}\n", "foo.irule", |metric| {
15730            assert_eq!(metric.abc.assignments_sum(), 1);
15731            assert_eq!(metric.abc.branches_sum(), 0);
15732            assert_eq!(metric.abc.conditions_sum(), 0);
15733        });
15734    }
15735
15736    /// Mutator commands (`incr` / `append` / `lappend`) count as
15737    /// assignments, not branches — iRules has no assignment operators, so
15738    /// mutation is always a command invocation.
15739    #[cfg(feature = "irules")]
15740    #[test]
15741    fn irules_abc_mutator_commands() {
15742        check_metrics::<IrulesParser>(
15743            "when X {\n    incr x\n    append s \"y\"\n    lappend l 1\n}\n",
15744            "foo.irule",
15745            |metric| {
15746                assert_eq!(metric.abc.assignments_sum(), 3);
15747                assert_eq!(metric.abc.branches_sum(), 0);
15748            },
15749        );
15750    }
15751
15752    /// `::incr` is `incr` through the global namespace. iRules resolves
15753    /// the leading word in `irules_command_is_assignment` rather than
15754    /// through `tcl_command_name`, so this pins the strip on that second
15755    /// path (#1381 review).
15756    #[cfg(feature = "irules")]
15757    #[test]
15758    fn irules_abc_qualified_mutator_commands() {
15759        check_metrics::<IrulesParser>(
15760            "when X {\n    ::incr x\n    ::append s \"y\"\n    ::lappend l 1\n}\n",
15761            "foo.irule",
15762            |metric| {
15763                assert_eq!(metric.abc.assignments_sum(), 3);
15764                assert_eq!(metric.abc.branches_sum(), 0);
15765            },
15766        );
15767    }
15768
15769    /// Control: `ns::incr` is a proc in `ns`, so it stays a branch.
15770    #[cfg(feature = "irules")]
15771    #[test]
15772    fn irules_abc_namespaced_mutator_command_stays_a_branch() {
15773        check_metrics::<IrulesParser>("when X {\n    ns::incr x\n}\n", "foo.irule", |metric| {
15774            assert_eq!(metric.abc.assignments_sum(), 0);
15775            assert_eq!(metric.abc.branches_sum(), 1);
15776        });
15777    }
15778
15779    /// Generic (non-mutator) commands count as branches.
15780    #[cfg(feature = "irules")]
15781    #[test]
15782    fn irules_abc_branch_commands() {
15783        check_metrics::<IrulesParser>(
15784            "when X {\n    log local0. hi\n    pool p1\n}\n",
15785            "foo.irule",
15786            |metric| {
15787                assert_eq!(metric.abc.assignments_sum(), 0);
15788                assert_eq!(metric.abc.branches_sum(), 2);
15789                assert_eq!(metric.abc.conditions_sum(), 0);
15790            },
15791        );
15792    }
15793
15794    /// A numeric comparison (`==`) is one condition; the `log` inside the
15795    /// `if` body is one branch.
15796    #[cfg(feature = "irules")]
15797    #[test]
15798    fn irules_abc_comparison_condition() {
15799        check_metrics::<IrulesParser>(
15800            "when X {\n    if { $a == 1 } { log local0. hi }\n}\n",
15801            "foo.irule",
15802            |metric| {
15803                assert_eq!(metric.abc.branches_sum(), 1);
15804                assert_eq!(metric.abc.conditions_sum(), 1);
15805            },
15806        );
15807    }
15808
15809    /// A word-form string comparator (`contains`) is a condition just like
15810    /// `==` — iRules-specific (Tcl has only `eq`/`ne`/`in`/`ni`). If
15811    /// `contains` were dropped from the condition set this would report 0.
15812    #[cfg(feature = "irules")]
15813    #[test]
15814    fn irules_abc_string_op_condition() {
15815        check_metrics::<IrulesParser>(
15816            "when X {\n    if { $a contains \"x\" } { log local0. hi }\n}\n",
15817            "foo.irule",
15818            |metric| {
15819                assert_eq!(metric.abc.branches_sum(), 1);
15820                assert_eq!(metric.abc.conditions_sum(), 1);
15821            },
15822        );
15823    }
15824
15825    /// Each `elseif` / `else` clause is one condition; the three `set`s are
15826    /// assignments. The leading `if` is not itself a condition.
15827    #[cfg(feature = "irules")]
15828    #[test]
15829    fn irules_abc_elseif_else_conditions() {
15830        check_metrics::<IrulesParser>(
15831            "when X {\n    if { $a } { set r 1 } elseif { $b } { set r 2 } else { set r 3 }\n}\n",
15832            "foo.irule",
15833            |metric| {
15834                assert_eq!(metric.abc.assignments_sum(), 3);
15835                // The `elseif` and `else` clauses are one condition each,
15836                // as before; #1180 adds the two bare truthy predicates
15837                // (`{ $a }`, `{ $b }`). C++'s
15838                // `if(a){} else if(b){} else {}` also scores 4.
15839                assert_eq!(metric.abc.conditions_sum(), 4);
15840            },
15841        );
15842    }
15843
15844    /// A ternary contributes its own condition plus the `>` comparison in
15845    /// its test: conditions 2; the `set` is one assignment.
15846    #[cfg(feature = "irules")]
15847    #[test]
15848    fn irules_abc_ternary_condition() {
15849        check_metrics::<IrulesParser>(
15850            "when X {\n    set y [expr { $a > 0 ? 1 : 0 }]\n}\n",
15851            "foo.irule",
15852            |metric| {
15853                assert_eq!(metric.abc.assignments_sum(), 1);
15854                assert_eq!(metric.abc.conditions_sum(), 2);
15855            },
15856        );
15857    }
15858
15859    /// Fitzpatrick Rule 9: the short-circuit `&&` is not itself a condition,
15860    /// but each negated bare operand (`!$a`, `!$b`) in the chain is. Guards
15861    /// the `irules_count_condition` chain-operand walk — conditions 2.
15862    #[cfg(feature = "irules")]
15863    #[test]
15864    fn irules_abc_negated_operands_in_chain() {
15865        check_metrics::<IrulesParser>(
15866            "when X {\n    if { !$a && !$b } { log local0. hi }\n}\n",
15867            "foo.irule",
15868            |metric| {
15869                assert_eq!(metric.abc.branches_sum(), 1);
15870                assert_eq!(metric.abc.conditions_sum(), 2);
15871            },
15872        );
15873    }
15874
15875    /// A bare-truthy `if {$a}` predicate is one condition (#1180).
15876    ///
15877    /// It carries no comparison, ternary or short-circuit operator, so
15878    /// before the Phase 2B slot routing landed nothing invoked the
15879    /// unary-conditional walker and the whole predicate scored 0 — this
15880    /// test previously pinned that absence, and the metrics book's ABC
15881    /// deviation table said so. Now the `if` node routes its `expr`
15882    /// predicate and the count matches C++'s `if (a)`, which is also 1.
15883    /// The `log` command remains the single branch.
15884    #[cfg(feature = "irules")]
15885    #[test]
15886    fn irules_abc_bare_truthy_counts_one_condition() {
15887        check_metrics::<IrulesParser>(
15888            "when X {\n    if { $a } { log local0. hi }\n}\n",
15889            "foo.irule",
15890            |metric| {
15891                assert_eq!(metric.abc.branches_sum(), 1);
15892                assert_eq!(metric.abc.conditions_sum(), 1);
15893            },
15894        );
15895    }
15896
15897    /// The negated form of the same predicate, which was *also* 0 before
15898    /// #1180: `!$a` reached the walker but no parent seeded boolean
15899    /// context, so the terminal operand was never counted. Distinct from
15900    /// `irules_abc_negated_operands_in_chain`, whose `&&` supplied the
15901    /// seed the bare form lacked.
15902    #[cfg(feature = "irules")]
15903    #[test]
15904    fn irules_abc_negated_bare_truthy_counts_one_condition() {
15905        check_metrics::<IrulesParser>(
15906            "when X {\n    if { !$a } { log local0. hi }\n}\n",
15907            "foo.irule",
15908            |metric| {
15909                assert_eq!(metric.abc.branches_sum(), 1);
15910                assert_eq!(metric.abc.conditions_sum(), 1);
15911            },
15912        );
15913    }
15914
15915    /// The ternary's three operand slots, located relative to the `?`
15916    /// and `:` tokens because the grammar exposes no fields (#1180).
15917    ///
15918    /// `$a ? !$b : !$c` is four: the `ternary_expr` node, the bare
15919    /// truthy condition, and one per negated branch — the same value
15920    /// Java, C#, Groovy, the C family, the JS family, PHP, Perl, Ruby
15921    /// and Python report for the identical expression.
15922    #[cfg(feature = "irules")]
15923    #[test]
15924    fn irules_abc_ternary_routes_its_operand_slots() {
15925        check_metrics::<IrulesParser>(
15926            "when X {\n    set y [expr { $a ? !$b : !$c }]\n}\n",
15927            "foo.irule",
15928            |metric| {
15929                assert_eq!(metric.abc.conditions_sum(), 4);
15930            },
15931        );
15932    }
15933
15934    /// The #1161 control: a ternary whose condition is a *comparison*
15935    /// must not move. The `>` already supplied its condition and the
15936    /// branches are unnegated, so routing the slots adds nothing.
15937    #[cfg(feature = "irules")]
15938    #[test]
15939    fn irules_abc_comparison_ternary_is_unchanged_by_slot_routing() {
15940        check_metrics::<IrulesParser>(
15941            "when X {\n    set y [expr { $a > 0 ? 1 : 0 }]\n}\n",
15942            "foo.irule",
15943            |metric| {
15944                assert_eq!(metric.abc.conditions_sum(), 2);
15945            },
15946        );
15947    }
15948
15949    /// A parenthesised ternary condition scores the same as a bare one.
15950    ///
15951    /// The grammar inlines `( … )` as anonymous children of
15952    /// `ternary_expr` rather than wrapping them in a node, so a
15953    /// fixed-index reading of the slots would shift right by one and
15954    /// mis-assign every operand. This is the input that discriminates
15955    /// the token-relative location the fix uses.
15956    #[cfg(feature = "irules")]
15957    #[test]
15958    fn irules_abc_parenthesised_ternary_condition_matches_the_bare_form() {
15959        // `check_metrics` takes a bare `fn`, so it cannot carry the
15960        // first measurement into the second comparison; `metrics_verbatim`
15961        // returns a value instead.
15962        let conditions = |source: &str| {
15963            crate::test_support::metrics_verbatim(
15964                crate::LANG::Irules,
15965                source.as_bytes(),
15966                crate::MetricsOptions::default(),
15967            )
15968            .abc
15969            .conditions_sum()
15970        };
15971        let bare = conditions("when X {\n    set y [expr { $a ? !$b : !$c }]\n}\n");
15972        assert_eq!(bare, 4, "the bare form is the documented reference value");
15973        assert_eq!(
15974            conditions("when X {\n    set y [expr { ($a) ? !$b : !$c }]\n}\n"),
15975            bare,
15976            "parenthesising the condition must not change the count"
15977        );
15978    }
15979
15980    // #1454's five-language sweep of #1422's C# rule: a pattern-match
15981    // guard is a condition *slot*, so every spelling of one contributes
15982    // exactly one condition, and the guard is a cyclomatic decision the
15983    // arm it guards does not already pay for.
15984    //
15985    // Each fixture is the §11 triple — an operator guard (`> 5`, whose
15986    // comparison token already counted before the fix), a call guard,
15987    // and a bare-identifier guard (the two that counted nothing) —
15988    // plus a parenthesised guard, which is the only member that
15989    // exercises the boolean-context seed, plus an unguarded control
15990    // that must stay where it was. A single-spelling fixture cannot
15991    // show the slot works: before the fix the operator member already
15992    // read the right number.
15993    //
15994    // The controls are load-bearing twice over. `none` pins that the
15995    // slot fires on the guard and not on the arm — a `+1` on every arm
15996    // would satisfy every guarded row and break this one — and the
15997    // helper (`is_even` / `isEven`) pins that the call-guard row is
15998    // scoring its guard rather than its call, since a call is an ABC
15999    // *branch* everywhere and a condition nowhere.
16000    //
16001    // `assert_members_score` rather than `assert_every_member_scores`:
16002    // the guarded members and their control sit at different values,
16003    // which is the comparison these tests exist to make.
16004
16005    #[cfg(feature = "rust")]
16006    #[test]
16007    fn rust_match_guard_scores_one_condition_however_spelled() {
16008        let src = "fn is_even(n: i32) -> bool {
16009                n == 0
16010            }
16011            fn tok(x: i32) -> i32 {
16012                match x { n if n > 5 => 1, _ => 0 }
16013            }
16014            fn call(x: i32) -> i32 {
16015                match x { n if is_even(n) => 1, _ => 0 }
16016            }
16017            fn bare(x: i32, b: bool) -> i32 {
16018                match x { _ if b => 1, _ => 0 }
16019            }
16020            fn paren(x: i32, b: bool) -> i32 {
16021                match x { n if (b) => n, _ => 0 }
16022            }
16023            fn none(x: i32) -> i32 {
16024                match x { 1 => 1, _ => 0 }
16025            }";
16026        assert_fixture_spells::<RustParser>(
16027            src,
16028            "foo.rs",
16029            &[
16030                // The guard's own `if` keyword, one per guarded member.
16031                // It is also how Rust cyclomatic already saw the guard,
16032                // which is why only the ABC half moved here.
16033                (Rust::If as u16, 4, "match guards"),
16034                (Rust::GT as u16, 1, "`tok`'s `>`"),
16035                (Rust::CallExpression as u16, 1, "the `is_even` call"),
16036                (
16037                    Rust::ParenthesizedExpression as u16,
16038                    1,
16039                    "`paren`'s parenthesised guard",
16040                ),
16041                // Two arms per member: the guarded one and its bare `_`.
16042                (Rust::MatchPattern as u16, 10, "match arms"),
16043            ],
16044        );
16045        check_func_space::<RustParser, _>(src, "foo.rs", |space| {
16046            assert_members_score(
16047                &space,
16048                &[
16049                    ("is_even", 1, 1),
16050                    ("tok", 2, 3),
16051                    // Was 1 — the guard spelling nothing counted.
16052                    ("call", 2, 3),
16053                    ("bare", 2, 3),
16054                    ("paren", 2, 3),
16055                    ("none", 1, 2),
16056                ],
16057            );
16058        });
16059    }
16060
16061    #[cfg(feature = "java")]
16062    #[test]
16063    fn java_pattern_switch_guard_scores_one_condition_however_spelled() {
16064        let src = "class T {
16065                static boolean isEven(int i) { return i == 0; }
16066                int tok(Object o) { return switch (o) { case Integer i when i > 5 -> 1; default -> 0; }; }
16067                int call(Object o) { return switch (o) { case Integer i when isEven(i) -> 1; default -> 0; }; }
16068                int bare(Object o, boolean b) { return switch (o) { case Integer i when b -> 1; default -> 0; }; }
16069                int paren(Object o, boolean b) { return switch (o) { case Integer i when (b) -> 1; default -> 0; }; }
16070                int none(Object o) { return switch (o) { case Integer i -> 1; default -> 0; }; }
16071            }";
16072        assert_fixture_spells::<JavaParser>(
16073            src,
16074            "foo.java",
16075            &[
16076                (Java::Guard as u16, 4, "`when` guards"),
16077                (Java::GT as u16, 1, "`tok`'s `>`"),
16078                (Java::MethodInvocation as u16, 1, "the `isEven` call"),
16079                // Five `switch (o)` subjects plus `paren`'s `when (b)`.
16080                // Java wraps a switch subject in the same kind, so this
16081                // count moves if either is edited out.
16082                (
16083                    Java::ParenthesizedExpression as u16,
16084                    6,
16085                    "the switch subjects and `paren`'s guard",
16086                ),
16087            ],
16088        );
16089        check_func_space::<JavaParser, _>(src, "foo.java", |space| {
16090            assert_members_score(
16091                &space.spaces[0],
16092                &[
16093                    ("isEven", 1, 1),
16094                    // Was 2 conditions against a *flat* cyclomatic of 2:
16095                    // Java had neither half of the rule, so the guard
16096                    // was invisible to cyclomatic and visible to ABC
16097                    // only through whatever operator it spelled.
16098                    ("tok", 2, 3),
16099                    ("call", 2, 3),
16100                    ("bare", 2, 3),
16101                    ("paren", 2, 3),
16102                    ("none", 1, 2),
16103                ],
16104            );
16105        });
16106    }
16107
16108    #[cfg(feature = "python")]
16109    #[test]
16110    fn python_case_guard_scores_one_condition_however_spelled() {
16111        let src = "def is_even(n):
16112    return n == 0
16113def tok(x):
16114    match x:
16115        case n if n > 5:
16116            return 1
16117        case _:
16118            return 0
16119def call(x):
16120    match x:
16121        case n if is_even(x):
16122            return 1
16123        case _:
16124            return 0
16125def bare(x, b):
16126    match x:
16127        case _ if b:
16128            return 1
16129        case _:
16130            return 0
16131def paren(x, b):
16132    match x:
16133        case n if (b):
16134            return n
16135        case _:
16136            return 0
16137def none(x):
16138    match x:
16139        case 1:
16140            return 1
16141        case _:
16142            return 0
16143";
16144        assert_fixture_spells::<PythonParser>(
16145            src,
16146            "foo.py",
16147            &[
16148                // The `case` guard clause. A comprehension filter is the
16149                // same kind in a different role, and the `guard` field
16150                // is what keeps it out of the slot — there is none in
16151                // this fixture, and `python_comprehension_if_clause_is_not_a_case_guard`
16152                // is where that separation is pinned.
16153                (Python::IfClause as u16, 4, "`case` guards"),
16154                (
16155                    Python::ComparisonOperator as u16,
16156                    2,
16157                    "`tok`'s `>` and `is_even`'s `==`",
16158                ),
16159                (Python::Call as u16, 1, "the `is_even` call"),
16160                (
16161                    Python::ParenthesizedExpression as u16,
16162                    1,
16163                    "`paren`'s parenthesised guard",
16164                ),
16165            ],
16166        );
16167        check_func_space::<PythonParser, _>(src, "foo.py", |space| {
16168            assert_members_score(
16169                &space,
16170                &[
16171                    ("is_even", 1, 1),
16172                    ("tok", 2, 3),
16173                    ("call", 2, 3),
16174                    ("bare", 2, 3),
16175                    ("paren", 2, 3),
16176                    ("none", 1, 2),
16177                ],
16178            );
16179        });
16180    }
16181
16182    // A comprehension's `if` filter is an `if_clause` too, and it is not
16183    // a `case` guard: the slot reads `case_clause`'s `guard` field, so
16184    // the two cannot be confused by construction. Python cyclomatic does
16185    // count the filter (through the same `If` keyword token it counts a
16186    // guard by), and ABC does not — a pre-existing divergence this
16187    // change deliberately leaves where it found it. The test is here so
16188    // that a later `IfClause` arm added without the field read fails
16189    // loudly rather than moving comprehensions silently.
16190    #[cfg(feature = "python")]
16191    #[test]
16192    fn python_comprehension_if_clause_is_not_a_case_guard() {
16193        let src = "def m(xs):
16194    return [x for x in xs if x]
16195";
16196        assert_fixture_spells::<PythonParser>(
16197            src,
16198            "foo.py",
16199            &[(Python::IfClause as u16, 1, "the comprehension filter")],
16200        );
16201        check_func_space::<PythonParser, _>(src, "foo.py", |space| {
16202            assert_members_score(&space, &[("m", 0, 3)]);
16203        });
16204    }
16205
16206    #[cfg(feature = "ruby")]
16207    #[test]
16208    fn ruby_in_clause_guard_scores_one_condition_however_spelled() {
16209        let src = "def is_even(x)
16210  x == 0
16211end
16212def tok(x)
16213  case x
16214  in [n] if n > 5 then 1
16215  in _ then 0
16216  end
16217end
16218def call(x)
16219  case x
16220  in [n] if n.even? then 1
16221  in _ then 0
16222  end
16223end
16224def bare(x, b)
16225  case x
16226  in [n] if b then 1
16227  in _ then 0
16228  end
16229end
16230def unguard(x, b)
16231  case x
16232  in [n] unless b then 1
16233  in _ then 0
16234  end
16235end
16236def paren(x, b)
16237  case x
16238  in [n] if (b) then 1
16239  in _ then 0
16240  end
16241end
16242def none(x)
16243  case x
16244  in [n] then 1
16245  in _ then 0
16246  end
16247end
16248";
16249        assert_fixture_spells::<RubyParser>(
16250            src,
16251            "foo.rb",
16252            &[
16253                (Ruby::IfGuard as u16, 4, "`if` guards"),
16254                (Ruby::UnlessGuard as u16, 1, "the `unless` guard"),
16255                (Ruby::GT as u16, 1, "`tok`'s `>`"),
16256                (
16257                    Ruby::ParenthesizedStatements as u16,
16258                    1,
16259                    "`paren`'s parenthesised guard",
16260                ),
16261            ],
16262        );
16263        // `Ruby::Guard` (210) is the hidden `_guard` supertype: the two
16264        // dispatchers list it beside the concrete kinds so a grammar
16265        // that starts emitting it keeps working, and this pins that it
16266        // does not emit it today — otherwise the defensive arm is
16267        // indistinguishable from a dead one
16268        // (`.claude/rules/grammar-dispatch.md` §2).
16269        let parser = RubyParser::new(
16270            src.as_bytes().to_vec(),
16271            &std::path::PathBuf::from("foo.rb"),
16272            None,
16273        );
16274        assert!(
16275            !ast_has_kind_id(&parser, Ruby::Guard as u16),
16276            "`_guard` stopped being hidden — the defensive arms now fire"
16277        );
16278        check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
16279            assert_members_score(
16280                &space,
16281                &[
16282                    ("is_even", 1, 1),
16283                    // Was 2 / 2: Ruby had neither half either, and the
16284                    // arm's own condition supplied the 2 that made the
16285                    // operator spelling look correct.
16286                    ("tok", 2, 3),
16287                    ("call", 2, 3),
16288                    ("bare", 2, 3),
16289                    ("unguard", 2, 3),
16290                    ("paren", 2, 3),
16291                    ("none", 1, 2),
16292                ],
16293            );
16294        });
16295    }
16296
16297    // Elixir is the inverse of the four above: ABC counted the `when`
16298    // token from the start and cyclomatic had no arm at all, so a guard
16299    // read as a condition with no decision behind it. #1454 adds the
16300    // decision, which is why `tok` / `call` / `bare` move on the
16301    // cyclomatic axis here and on the ABC axis everywhere else.
16302    //
16303    // Elixir was also the one language of the six that #1454 left
16304    // *without* the slot. Its `when` arm added a flat one on top of
16305    // whatever the guard's sub-structure already scored, so `tok` came
16306    // back 3 against `call` and `bare`'s 2 — the spelling-dependence the
16307    // batch exists to remove, and a `.claude/rules/grammar-dispatch.md`
16308    // §5 double count with the `>` token arm. A whole-branch review of
16309    // the batch found it; all three rows now read 2, level with the
16310    // sibling fixtures above and with their own decision counts.
16311    #[cfg(feature = "elixir")]
16312    #[test]
16313    fn elixir_guard_is_a_decision_however_spelled() {
16314        let src = "defmodule T do
16315  def is_even(x) do
16316    x == 0
16317  end
16318  def tok(x) do
16319    case x do
16320      n when n > 5 -> 1
16321      _ -> 0
16322    end
16323  end
16324  def call(x) do
16325    case x do
16326      n when is_integer(n) -> 1
16327      _ -> 0
16328    end
16329  end
16330  def bare(x, b) do
16331    case x do
16332      _n when b -> 1
16333      _ -> 0
16334    end
16335  end
16336  def none(x) do
16337    case x do
16338      1 -> 1
16339      _ -> 0
16340    end
16341  end
16342end
16343";
16344        assert_fixture_spells::<ElixirParser>(
16345            src,
16346            "foo.ex",
16347            &[
16348                (Elixir::When as u16, 3, "`when` guards"),
16349                (Elixir::GT as u16, 1, "`tok`'s `>`"),
16350            ],
16351        );
16352        check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
16353            assert_members_score(
16354                &space.spaces[0],
16355                &[
16356                    ("is_even", 1, 1),
16357                    // All three guarded members were cyclomatic 2 —
16358                    // level with `none` — before the decision arm, and
16359                    // `tok` was abc 3 before the slot.
16360                    ("tok", 2, 3),
16361                    ("call", 2, 3),
16362                    ("bare", 2, 3),
16363                    ("none", 1, 2),
16364                ],
16365            );
16366        });
16367    }
16368
16369    // A statement modifier's `condition` is the same boolean slot as the
16370    // block form's, so `g if (b)` must score what `if (b) then g end`
16371    // does. The modifier kinds were missing from the peel's seed, so a
16372    // parenthesised operand unwrapped into a context only a negation
16373    // could mark boolean and scored 0 (#1521). Every member here is one
16374    // decision, so each reads `conditions == cyclomatic - 1`; `bare` and
16375    // `block` are the analogues the modifier rows must match.
16376    #[cfg(feature = "ruby")]
16377    #[test]
16378    fn ruby_parenthesised_modifier_condition_scores_one() {
16379        let src = "def bare(b)
16380  g if b
16381end
16382def block(b)
16383  if (b) then g end
16384end
16385def if1(b)
16386  g if (b)
16387end
16388def if2(b)
16389  g if ((b))
16390end
16391def unless1(b)
16392  g unless (b)
16393end
16394def unless2(b)
16395  g unless ((b))
16396end
16397def while1(b)
16398  g while (b)
16399end
16400def while2(b)
16401  g while ((b))
16402end
16403def until1(b)
16404  g until (b)
16405end
16406def until2(b)
16407  g until ((b))
16408end
16409";
16410        assert_fixture_spells::<RubyParser>(
16411            src,
16412            "foo.rb",
16413            &[
16414                (Ruby::IfModifier as u16, 3, "`if` modifiers"),
16415                (Ruby::UnlessModifier as u16, 2, "`unless` modifiers"),
16416                (Ruby::WhileModifier as u16, 2, "`while` modifiers"),
16417                (Ruby::UntilModifier as u16, 2, "`until` modifiers"),
16418                (
16419                    Ruby::ParenthesizedStatements as u16,
16420                    13,
16421                    "one layer per `(b)`, two per `((b))`",
16422                ),
16423            ],
16424        );
16425        check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
16426            assert_members_score(
16427                &space,
16428                &[
16429                    ("bare", 1, 2),
16430                    ("block", 1, 2),
16431                    ("if1", 1, 2),
16432                    ("if2", 1, 2),
16433                    ("unless1", 1, 2),
16434                    ("unless2", 1, 2),
16435                    ("while1", 1, 2),
16436                    ("while2", 1, 2),
16437                    ("until1", 1, 2),
16438                    ("until2", 1, 2),
16439                ],
16440            );
16441        });
16442    }
16443
16444    // The slot's whole claim, on every spelling the Elixir grammar gives
16445    // a guard — and on `assert_every_member_scores`, which asserts §8
16446    // parity where the fixture above cannot (its unguarded control and
16447    // its bare-comparison member legitimately sit off their decision
16448    // counts, which is why that one takes a per-member table).
16449    //
16450    // Nine members, nine routes to the same 2:
16451    //
16452    //   * `tok` / `eq` — the guard is an operator application, so the
16453    //     slot adds nothing and the `>` / `==` token arm supplies the
16454    //     one. These were 3 before the slot.
16455    //   * `call` / `bare` — no operator counts a `Call` or an
16456    //     `Identifier`, so the slot supplies the one directly.
16457    //   * `paren` / `negated` — `block` and `unary_operator` are
16458    //     wrappers, peeled by `elixir_wrapper_operand` before the slot
16459    //     asks whether an operator already paid. `negated` covers the
16460    //     keyword `not`, which the peel once did not recognise as a
16461    //     negation.
16462    //   * `membership` / `nonmembership` — `in` / `not in` are
16463    //     relational operators, given the by-use arm #1461 gave the
16464    //     other five languages. Without it these two would score zero,
16465    //     since the slot correctly declines an operator application.
16466    //
16467    // A repeated guard (`when a when b`) is deliberately absent: it is
16468    // an or-chain and scores one per alternative, so it belongs with
16469    // `elixir_repeated_guard_matches_the_or_chain` below rather than in
16470    // a table whose every row reads 2.
16471    //
16472    // §11: each bullet is an independent path, and no other member can
16473    // stand in for it — deleting the `in` arm leaves `membership` and
16474    // `nonmembership` alone failing, deleting the `not` recognition
16475    // leaves `negated` alone failing.
16476    #[cfg(feature = "elixir")]
16477    #[test]
16478    fn elixir_guard_scores_one_however_spelled() {
16479        let src = "defmodule T do
16480  def tok(x) do
16481    case x do
16482      n when n > 5 -> 1
16483      _ -> 0
16484    end
16485  end
16486  def eq(x) do
16487    case x do
16488      n when n == 5 -> 1
16489      _ -> 0
16490    end
16491  end
16492  def call(x) do
16493    case x do
16494      n when is_integer(n) -> 1
16495      _ -> 0
16496    end
16497  end
16498  def bare(x, b) do
16499    case x do
16500      _n when b -> 1
16501      _ -> 0
16502    end
16503  end
16504  def paren(x, b) do
16505    case x do
16506      _n when (b) -> 1
16507      _ -> 0
16508    end
16509  end
16510  def negated(x, b) do
16511    case x do
16512      _n when not b -> 1
16513      _ -> 0
16514    end
16515  end
16516  def membership(x) do
16517    case x do
16518      n when n in [1, 2] -> 1
16519      _ -> 0
16520    end
16521  end
16522  def nonmembership(x) do
16523    case x do
16524      n when n not in [1, 2] -> 1
16525      _ -> 0
16526    end
16527  end
16528end
16529";
16530        assert_fixture_spells::<ElixirParser>(
16531            src,
16532            "foo.ex",
16533            &[
16534                (Elixir::When as u16, 8, "one `when` guard per member"),
16535                (Elixir::GT as u16, 1, "`tok`'s `>`"),
16536                (Elixir::EQEQ as u16, 1, "`eq`'s `==`"),
16537                (Elixir::Block as u16, 1, "`paren`'s parenthesised guard"),
16538                (Elixir::Not as u16, 1, "`negated`'s `not`"),
16539                (Elixir::In as u16, 1, "`membership`'s `in`"),
16540                (Elixir::Notin as u16, 1, "`nonmembership`'s `not in`"),
16541            ],
16542        );
16543        check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
16544            assert_every_member_scores(
16545                &space.spaces[0],
16546                8,
16547                2,
16548                "one for the `case` arm and one for the guard, however the guard is spelled",
16549            );
16550        });
16551    }
16552
16553    // Elixir's repeated guard is an or-chain, not a longer spelling of
16554    // one guard: each `when` expression is tried in turn and evaluation
16555    // moves to the next when the previous one is false *or raises*. So
16556    // `when a when b` carries the alternatives `when a or b` does, and
16557    // the two spellings have to score alike.
16558    //
16559    // They did not. The nesting is right-associative
16560    // (`head when (a when (b when c))`), only the outermost `when` sat
16561    // on the anchor `elixir_when_is_guard` tests, and the `Abc` slot
16562    // peeled to the last alternative and scored that one — so a
16563    // repeated guard read *identically to a single guard* on both axes
16564    // while the `or` form read one higher per alternative.
16565    //
16566    // Asserted as an equality against the `or` spelling rather than
16567    // against separate literals, so neither row can drift alone; the
16568    // literals are here too, because a pair of equal wrong numbers
16569    // would satisfy the equality on its own.
16570    //
16571    // Both anchors, because the climb has to reach each: a definition
16572    // `Call`'s `arguments` (`repeated` / `ored`) and a `stab_clause`'s
16573    // `left` (`clause` / `clause_or`). `single` is the control the
16574    // repeated rows must sit *above*, which is the comparison that
16575    // failed before.
16576    #[cfg(feature = "elixir")]
16577    #[test]
16578    fn elixir_repeated_guard_matches_the_or_chain() {
16579        let src = "defmodule T do
16580  def repeated(n) when is_integer(n) when is_float(n) when is_atom(n) do
16581    n
16582  end
16583  def ored(n) when is_integer(n) or is_float(n) or is_atom(n) do
16584    n
16585  end
16586  def single(n) when is_integer(n) do
16587    n
16588  end
16589  def clause(x) do
16590    case x do
16591      n when is_integer(n) when is_float(n) -> 1
16592      _ -> 0
16593    end
16594  end
16595  def clause_or(x) do
16596    case x do
16597      n when is_integer(n) or is_float(n) -> 1
16598      _ -> 0
16599    end
16600  end
16601end
16602";
16603        assert_fixture_spells::<ElixirParser>(
16604            src,
16605            "foo.ex",
16606            &[
16607                (
16608                    Elixir::When as u16,
16609                    8,
16610                    "three repeated, one `or`-spelled, one single, two \
16611                     repeated and one `or`-spelled in a clause",
16612                ),
16613                (Elixir::Or as u16, 3, "the two `or` spellings' operators"),
16614            ],
16615        );
16616        check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
16617            // Positional: a `def` whose head carries a guard parses its
16618            // name out of a `binary_operator` rather than a plain
16619            // `Call`, so `repeated`, `ored` and `single` all come back
16620            // `<anonymous>` (the same naming gap
16621            // `elixir_typespec_when_is_not_a_guard` reads around).
16622            let members: Vec<(u64, u64)> = space.spaces[0]
16623                .spaces
16624                .iter()
16625                .map(|m| {
16626                    (
16627                        m.metrics.abc.conditions(),
16628                        m.metrics.cyclomatic.cyclomatic(),
16629                    )
16630                })
16631                .collect();
16632            assert_eq!(
16633                members,
16634                vec![(3, 4), (3, 4), (1, 2), (3, 4), (3, 4)],
16635                "a repeated guard scores one alternative per `when`; the \
16636                 single guard is the control one alternative lower"
16637            );
16638            assert_eq!(
16639                members[0], members[1],
16640                "`when a when b when c` is the or-chain `when a or b or c`"
16641            );
16642            assert_eq!(
16643                members[3], members[4],
16644                "and the same holds on a `stab_clause` anchor"
16645            );
16646        });
16647    }
16648
16649    // The gate that makes the Elixir arm safe. Elixir has no dedicated
16650    // guard production, and a typespec's binding clause spells the same
16651    // `when` token — so an ungated arm would have made type syntax a
16652    // decision. It was already an ABC condition against no decision
16653    // anywhere, which this removes.
16654    //
16655    // Both members carry the same `@spec`; only `guarded` carries a real
16656    // head guard, so the difference between the two rows is the guard
16657    // and nothing else.
16658    #[cfg(feature = "elixir")]
16659    #[test]
16660    fn elixir_typespec_when_is_not_a_guard() {
16661        let src = "defmodule T do
16662  @spec plain(a) :: a when a: integer
16663  def plain(x) do
16664    x
16665  end
16666  @spec guarded(a) :: a when a: integer
16667  def guarded(x) when is_integer(x) do
16668    x
16669  end
16670end
16671";
16672        assert_fixture_spells::<ElixirParser>(
16673            src,
16674            "foo.ex",
16675            &[(
16676                Elixir::When as u16,
16677                3,
16678                "two typespec `when`s and one head guard",
16679            )],
16680        );
16681        check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
16682            let module = &space.spaces[0];
16683            // Positional rather than by name: a `def` whose head carries
16684            // a guard parses its name out of a `binary_operator` instead
16685            // of a plain `Call` target, and the space comes back
16686            // `<anonymous>`. That naming gap predates this change and is
16687            // why `assert_members_score` cannot serve here.
16688            let members: Vec<(u64, u64)> = module
16689                .spaces
16690                .iter()
16691                .map(|m| {
16692                    (
16693                        m.metrics.abc.conditions(),
16694                        m.metrics.cyclomatic.cyclomatic(),
16695                    )
16696                })
16697                .collect();
16698            assert_eq!(
16699                members,
16700                vec![(0, 1), (1, 2)],
16701                "`plain` scores nothing; `guarded` scores its head guard \
16702                 in both metrics"
16703            );
16704            // The typespecs sit in the module body, outside either
16705            // member, so their (non-)contribution has to be read off
16706            // the container. Was 2 before the gate: one per `@spec`.
16707            assert_eq!(
16708                module.metrics.abc.conditions(),
16709                0,
16710                "a typespec `when` is type syntax, not a guard"
16711            );
16712        });
16713    }
16714
16715    // The other half of that gate's allowlist. `elixir_when_is_guard`
16716    // accepts a definition `Call`'s `arguments` anchor when the call's
16717    // keyword is a method macro *or* one of `defguard` / `defguardp`,
16718    // and the second disjunct is its own path: `elixir_is_method_macro`
16719    // spells only `def` / `defp` / `defmacro` / `defmacrop`, so a
16720    // `defguard` head reaches the anchor test solely through the
16721    // `matches!`. Nothing in the suite spelled `defguard` before this,
16722    // which left the disjunct scored by no fixture at all.
16723    //
16724    // `defguard` is where a guard is most obviously a decision — the
16725    // macro exists to name one — so an ungated head would have scored
16726    // the construct zero on both axes while the `def f(x) when g`
16727    // spelling it expands into scores one.
16728    //
16729    // Read off the *module*, not off a member: `defguard` is not a
16730    // method macro, so it opens no function space and its guard lands
16731    // in the container's body. `plain` is the control that pins the
16732    // module's rows to the two guards and nothing else, and the guard
16733    // bodies are calls (`is_integer/1`, `is_atom/1`) rather than
16734    // comparisons so neither can supply a condition of its own.
16735    #[cfg(feature = "elixir")]
16736    #[test]
16737    fn elixir_defguard_head_is_a_guard() {
16738        let src = "defmodule T do
16739  defguard is_int(x) when is_integer(x)
16740  defguardp is_at(x) when is_atom(x)
16741  def plain(x) do
16742    x
16743  end
16744end
16745";
16746        assert_fixture_spells::<ElixirParser>(
16747            src,
16748            "foo.ex",
16749            &[(
16750                Elixir::When as u16,
16751                2,
16752                "one per `defguard` / `defguardp` head",
16753            )],
16754        );
16755        check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
16756            let module = &space.spaces[0];
16757            assert_eq!(
16758                (
16759                    module.metrics.abc.conditions(),
16760                    module.metrics.cyclomatic.cyclomatic()
16761                ),
16762                (2, 3),
16763                "each of `defguard` and `defguardp` carries one guard, on \
16764                 both axes (cyclomatic counts from its base of 1)"
16765            );
16766            let members: Vec<(u64, u64)> = module
16767                .spaces
16768                .iter()
16769                .map(|m| {
16770                    (
16771                        m.metrics.abc.conditions(),
16772                        m.metrics.cyclomatic.cyclomatic(),
16773                    )
16774                })
16775                .collect();
16776            assert_eq!(
16777                members,
16778                vec![(0, 1)],
16779                "`defguard` opens no function space, so `plain` is the \
16780                 module's only member and scores nothing"
16781            );
16782        });
16783    }
16784
16785    // #1461's structural half: a relational operator scores by *use*,
16786    // a value-bearing operand scores in a boolean *slot*.
16787    //
16788    // Until this, the relational constructs a grammar spells as their
16789    // own production — C#'s two `is` tests, Java's and Groovy's
16790    // `instanceof`, Groovy's `in`, Kotlin's `is` / `in`, Ruby's
16791    // one-line `in` — reached `stats.conditions` only through
16792    // `<lang>_bool_terminal_kinds!()`, which every walker consults
16793    // inside a boolean slot and nowhere else. The comparison token
16794    // beside each of them carried no such gate, so one language scored
16795    // two spellings of the same relational test differently by
16796    // position:
16797    //
16798    //     val b = a == c        // 1 condition
16799    //     val b = a is String   // 0 conditions
16800    //
16801    // Fitzpatrick Rule 5 scores a relational operator wherever it is
16802    // written, so the asymmetry was in the mechanism. Each construct
16803    // now has an unconditional arm in its language's `compute` and has
16804    // left the terminal set; listing it in both would score it twice
16805    // (`.claude/rules/grammar-dispatch.md` §5).
16806    //
16807    // Every fixture below carries the same four shapes, because they
16808    // are structurally independent paths and covering one says nothing
16809    // about the others (§11):
16810    //
16811    // - `out*` — the construct with no boolean slot above it. This is
16812    //   the case the change exists for, and the one no prior fixture
16813    //   covered: measured against the pre-change binary, every `out*`
16814    //   member here scored **0**.
16815    // - `outCtl` — the same member spelling `==`. It read 1 before and
16816    //   after, so it is the anchor that makes `out*`'s 1 a claim about
16817    //   the construct rather than about the member shape
16818    //   (`.claude/rules/testing.md`, "Perturb the fixture as well as
16819    //   the production line").
16820    // - `in*` / `inAnd` — the predicate slot and the `&&`-chain walker,
16821    //   the two paths that already owned the count. They must be
16822    //   *unchanged*; a §5 double count surfaces here as a 2 against the
16823    //   control's 1, which is how #1459 caught the Kotlin `as?`
16824    //   collision.
16825    // - `forIn` (Kotlin / Groovy / Ruby) — the `for … in …` header,
16826    //   which spells the same keyword from a different production and
16827    //   must stay at 0. It is the reason those three count the node and
16828    //   not the token.
16829
16830    #[cfg(feature = "csharp")]
16831    #[test]
16832    fn csharp_is_tests_score_outside_a_boolean_slot() {
16833        let src = "class A {
16834                bool outIs(object x) => x is int;
16835                bool outPat(object x) => x is null;
16836                bool outCtl(int x) => x == 1;
16837                int inIs(object x) { if (x is int) { return 1; } return 0; }
16838                int inAnd(object x, bool b) { if (x is int && b) { return 1; } return 0; }
16839            }";
16840        assert_csharp_fixture_spells(
16841            src,
16842            &[
16843                (Csharp::IsExpression as u16, 3, "the bare `is` type tests"),
16844                (
16845                    Csharp::IsPatternExpression as u16,
16846                    1,
16847                    "`outPat`'s null pattern",
16848                ),
16849                (Csharp::EQEQ as u16, 1, "`outCtl`'s comparison"),
16850            ],
16851        );
16852        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
16853            assert_members_score(
16854                &space.spaces[0],
16855                &[
16856                    // Both were 0, against `outCtl`'s 1 on the same
16857                    // expression-bodied shape.
16858                    ("outIs", 1, 1),
16859                    ("outPat", 1, 1),
16860                    ("outCtl", 1, 1),
16861                    // Unchanged: the slot no longer counts the test, the
16862                    // new arm does, and the total stays put.
16863                    ("inIs", 1, 2),
16864                    ("inAnd", 2, 3),
16865                ],
16866            );
16867        });
16868    }
16869
16870    #[cfg(feature = "java")]
16871    #[test]
16872    fn java_instanceof_scores_outside_a_boolean_slot() {
16873        let src = "class A {
16874                boolean outOf(Object x) { return x instanceof String; }
16875                boolean outCtl(int x) { return x == 1; }
16876                int inOf(Object x) { if (x instanceof String) { return 1; } return 0; }
16877                int inAnd(Object x, boolean b) { if (x instanceof String && b) { return 1; } return 0; }
16878            }";
16879        assert_fixture_spells::<JavaParser>(
16880            src,
16881            "foo.java",
16882            &[
16883                (
16884                    Java::InstanceofExpression as u16,
16885                    3,
16886                    "the `instanceof` tests",
16887                ),
16888                (Java::EQEQ as u16, 1, "`outCtl`'s comparison"),
16889            ],
16890        );
16891        check_func_space::<JavaParser, _>(src, "foo.java", |space| {
16892            assert_members_score(
16893                &space.spaces[0],
16894                &[
16895                    // Was 0, against `outCtl`'s 1 on the same
16896                    // `return`-a-predicate shape.
16897                    ("outOf", 1, 1),
16898                    ("outCtl", 1, 1),
16899                    ("inOf", 1, 2),
16900                    ("inAnd", 2, 3),
16901                ],
16902            );
16903        });
16904    }
16905
16906    // Groovy carries item 4 as well as the structural pass. `<=>`
16907    // yields -1 / 0 / 1 rather than a boolean, which is why it never
16908    // belonged in the terminal set — that set holds operands — but it
16909    // is a relational operator, and `LTEQGT` is already a condition
16910    // token in Ruby, PHP, C++ and Mozcpp. Groovy was the outlier at 0.
16911    #[cfg(feature = "groovy")]
16912    #[test]
16913    fn groovy_relational_productions_score_outside_a_boolean_slot() {
16914        let src = "class A {
16915                def outIs(x) { def b = x instanceof String; return b }
16916                def outIn(x, l) { def b = x in l; return b }
16917                def outShip(a, c) { def r = a <=> c; return r }
16918                def outCtl(x) { def b = x == 1; return b }
16919                def inIs(x) { if (x instanceof String) { return 1 }; return 0 }
16920                def inIn(x, l) { if (x in l) { return 1 }; return 0 }
16921                def forIn(l) { for (q in l) { }; return 0 }
16922            }";
16923        assert_fixture_spells::<GroovyParser>(
16924            src,
16925            "foo.groovy",
16926            &[
16927                (
16928                    Groovy::InstanceofExpression as u16,
16929                    2,
16930                    "the `instanceof` tests",
16931                ),
16932                (Groovy::MembershipExpression as u16, 2, "the `in` tests"),
16933                (Groovy::SpaceshipExpression as u16, 1, "`outShip`'s `<=>`"),
16934                (
16935                    Groovy::ForInStatement as u16,
16936                    1,
16937                    "`forIn`'s loop header, which spells the same `in`",
16938                ),
16939                (Groovy::EQEQ as u16, 1, "`outCtl`'s comparison"),
16940            ],
16941        );
16942        check_func_space::<GroovyParser, _>(src, "foo.groovy", |space| {
16943            assert_members_score(
16944                &space.spaces[0],
16945                &[
16946                    // All three were 0, against `outCtl`'s 1 on the
16947                    // identical declare-and-return shape.
16948                    ("outIs", 1, 1),
16949                    ("outIn", 1, 1),
16950                    ("outShip", 1, 1),
16951                    ("outCtl", 1, 1),
16952                    ("inIs", 1, 2),
16953                    ("inIn", 1, 2),
16954                    // The loop header is not a membership test and
16955                    // stays at 0 — counting the `in` *token* would have
16956                    // scored it.
16957                    ("forIn", 0, 2),
16958                ],
16959            );
16960        });
16961    }
16962
16963    #[cfg(feature = "kotlin")]
16964    #[test]
16965    fn kotlin_is_and_in_score_outside_a_boolean_slot() {
16966        let src = "class K {
16967                fun outIs(a: Any): Boolean { val b = a is String; return b }
16968                fun outIn(a: Int): Boolean { val b = a in 1..2; return b }
16969                fun outCtl(a: Int): Boolean { val b = a == 1; return b }
16970                fun inIs(a: Any): Int { if (a is String) { return 1 }; return 0 }
16971                fun forIn(l: List<Int>): Int { for (q in l) { }; return 0 }
16972            }";
16973        assert_kotlin_class_members(
16974            src,
16975            &[
16976                (Kotlin::IsExpression as u16, 2, "the `is` tests"),
16977                (Kotlin::InExpression as u16, 1, "`outIn`'s `in` test"),
16978                (
16979                    Kotlin::ForStatement as u16,
16980                    1,
16981                    "`forIn`'s loop header, which spells the same `in`",
16982                ),
16983                (Kotlin::EQEQ as u16, 1, "`outCtl`'s comparison"),
16984            ],
16985            &[
16986                // Both were 0, against `outCtl`'s 1 on the identical
16987                // `val`-and-return shape.
16988                ("outIs", 1, 1),
16989                ("outIn", 1, 1),
16990                ("outCtl", 1, 1),
16991                ("inIs", 1, 2),
16992                ("forIn", 0, 2),
16993            ],
16994        );
16995    }
16996
16997    #[cfg(feature = "ruby")]
16998    #[test]
16999    fn ruby_test_pattern_scores_outside_a_boolean_slot() {
17000        let src = "def out_in(a)
17001  b = a in Integer
17002  b
17003end
17004def out_ctl(a)
17005  b = a == 1
17006  b
17007end
17008def in_in(a)
17009  if a in Integer
17010    return 1
17011  end
17012  0
17013end
17014def for_in(l)
17015  for q in l do end
17016  0
17017end
17018";
17019        assert_fixture_spells::<RubyParser>(
17020            src,
17021            "foo.rb",
17022            &[
17023                (Ruby::TestPattern as u16, 2, "the one-line `in` tests"),
17024                (
17025                    Ruby::For as u16,
17026                    1,
17027                    "`for_in`'s loop header, which spells the same `in`",
17028                ),
17029                (Ruby::EQEQ as u16, 1, "`out_ctl`'s comparison"),
17030            ],
17031        );
17032        check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
17033            assert_members_score(
17034                &space,
17035                &[
17036                    // Was 0, against `out_ctl`'s 1 on the identical
17037                    // assign-and-return shape.
17038                    ("out_in", 1, 1),
17039                    ("out_ctl", 1, 1),
17040                    ("in_in", 1, 2),
17041                    ("for_in", 0, 2),
17042                ],
17043            );
17044        });
17045    }
17046
17047    // #1461 item 1. The walrus is an *operand*, not an operator, so it
17048    // joined the former `python_bool_terminal_kinds!()` and stays slot-scoped —
17049    // `b = (n := g())` outside a predicate is a binding, not a
17050    // decision. Inside one the slot tests `g()`'s truth, and
17051    // `if (n := g()):` scored 0 where the `if g():` it is a
17052    // refactoring of scored 1.
17053    //
17054    // `assignments` is asserted beside `conditions` because the walrus
17055    // is the one construct in the survey that pays on two axes, and
17056    // that is deliberate rather than a §5 double count: ABC's axes are
17057    // independent measurements of the same source, and `if (n := g()):`
17058    // genuinely both binds a name and decides a branch. Without this
17059    // row a later reader has no way to tell the intent from an
17060    // oversight.
17061    #[cfg(feature = "python")]
17062    #[test]
17063    fn python_walrus_is_a_unary_condition() {
17064        let src = "def in_slot(g):
17065    if (n := g()):
17066        return 1
17067    return 0
17068
17069def chain(g, b):
17070    return b and (n := g())
17071
17072def ctrl(g):
17073    if g():
17074        return 1
17075    return 0
17076";
17077        assert_fixture_spells::<PythonParser>(
17078            src,
17079            "foo.py",
17080            &[(Python::NamedExpression as u16, 2, "the walrus bindings")],
17081        );
17082        check_func_space::<PythonParser, _>(src, "foo.py", |space| {
17083            assert_members_score(
17084                &space,
17085                &[
17086                    // Was 0, against `ctrl`'s 1 for the same predicate
17087                    // without the binding.
17088                    ("in_slot", 1, 2),
17089                    // The `and`-chain walker is the second, independent
17090                    // path (§11): was 1, the bare `b` operand alone.
17091                    ("chain", 2, 2),
17092                    ("ctrl", 1, 2),
17093                ],
17094            );
17095            for (name, assignments) in [("in_slot", 1u64), ("chain", 1), ("ctrl", 0)] {
17096                assert_eq!(
17097                    child_space(&space, name).metrics.abc.assignments(),
17098                    assignments,
17099                    "{name}: the walrus also pays on the A axis, on purpose"
17100                );
17101            }
17102        });
17103    }
17104
17105    // #1461 item 2. A macro in a boolean slot expands to a boolean
17106    // expression, and `if matches!(x, Some(_))` scored 0 against a
17107    // cyclomatic decision of 1.
17108    //
17109    // `cfg_slot` is in the fixture because the arm's *breadth* is the
17110    // decision, not a side effect: these sets discriminate on slot and
17111    // never on return type, so `cfg!` counts exactly as `matches!`
17112    // does. It is also the construct the corpus actually moves —
17113    // serde's one changed snapshot is `if cfg!(no_underscore_consts)`,
17114    // not a `matches!` — so asserting only `matches!` would leave the
17115    // measured case uncovered.
17116    #[cfg(feature = "rust")]
17117    #[test]
17118    fn rust_macro_invocation_is_a_unary_condition() {
17119        let src = "fn in_slot(x: Option<u8>) -> u8 { if matches!(x, Some(_)) { 1 } else { 0 } }
17120fn cfg_slot() -> u8 { if cfg!(unix) { 1 } else { 0 } }
17121fn chain(x: Option<u8>, b: bool) -> bool { b && matches!(x, Some(_)) }
17122fn ctrl(x: Option<u8>) -> u8 { if x.is_some() { 1 } else { 0 } }
17123";
17124        assert_fixture_spells::<RustParser>(
17125            src,
17126            "foo.rs",
17127            &[(
17128                Rust::MacroInvocation as u16,
17129                3,
17130                "the `matches!` / `cfg!` predicates",
17131            )],
17132        );
17133        check_func_space::<RustParser, _>(src, "foo.rs", |space| {
17134            assert_members_score(
17135                &space,
17136                &[
17137                    // Each was 1 — the `else` alone — against `ctrl`'s
17138                    // 2 for the same shape with a method call in the
17139                    // predicate.
17140                    ("in_slot", 2, 2),
17141                    ("cfg_slot", 2, 2),
17142                    // The `&&`-chain walker, the second independent
17143                    // path (§11): was 1, the bare `b` operand alone.
17144                    ("chain", 2, 2),
17145                    ("ctrl", 2, 2),
17146                ],
17147            );
17148        });
17149    }
17150}
17151
17152/// A comment inside a ternary must not change its ABC conditions
17153/// (#1181).
17154///
17155/// Two opposite defects, one cause: tree-sitter counts a comment among a
17156/// node's children, so it is the operand's previous sibling *and* it
17157/// shifts every positional index.
17158///
17159/// * Languages whose seed asked "is my previous sibling `?` or `:`"
17160///   (C family, PHP, Perl, JS family) read the comment as "not a
17161///   ternary token", flipped the boolean-context seed on for a *branch*
17162///   slot, and **over**-counted: `a ? /*n*/ (b) : c` scored 3 where
17163///   `a ? (b) : c` scores 2.
17164/// * Languages whose branch walk read `child(2)` / `child(4)` (Java,
17165///   C#, Groovy) landed on the comment instead of the operand, never
17166///   inspected it, and **under**-counted: `a ? /*n*/ !b : c` scored 2
17167///   where `a ? !b : c` scores 3.
17168///
17169/// Both slots are now addressed by grammar field. The parenthesised
17170/// operand is the only input that discriminates the first defect and
17171/// the negated operand the only one that discriminates the second —
17172/// existing ternary fixtures use neither.
17173// Gated on the union of the features [`ternary_comment_invariance::cases`]
17174// has a row for, so a build enabling none of them — `--no-default-features
17175// --features go` — drops both tests rather than tripping their
17176// `checked > 0` guards (`.claude/rules/testing.md`, #1286). Both tests
17177// read the same table, so one gate on the module is exact for each.
17178#[cfg(test)]
17179#[cfg(any(
17180    feature = "c",
17181    feature = "cpp",
17182    feature = "csharp",
17183    feature = "groovy",
17184    feature = "java",
17185    feature = "javascript",
17186    feature = "mozcpp",
17187    feature = "mozjs",
17188    feature = "objc",
17189    feature = "perl",
17190    feature = "php",
17191    feature = "typescript"
17192))]
17193mod ternary_comment_invariance {
17194    use crate::test_support::metrics_verbatim;
17195    use crate::{LANG, MetricsOptions};
17196
17197    fn conditions(lang: LANG, source: &str) -> u64 {
17198        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
17199            .abc
17200            .conditions_sum()
17201    }
17202
17203    /// `(base, with_comment)` for a parenthesised and a negated branch
17204    /// operand, per language.
17205    fn cases(lang: LANG) -> Option<[(String, String); 2]> {
17206        // `{}` marks the consequence slot; `/*n*/` the inserted comment.
17207        let (template, paren, negated, comment) = match lang {
17208            LANG::Cpp | LANG::C | LANG::Objc | LANG::Mozcpp => {
17209                ("int f(){ int x = a ? {} : c; }", "(b)", "!b", "/*n*/ ")
17210            }
17211            LANG::Java => (
17212                "class K{ void f(){ int x = a ? {} : c; } }",
17213                "(b)",
17214                "!b",
17215                "/*n*/ ",
17216            ),
17217            LANG::Csharp => (
17218                "class K{ void f(){ var x = a ? {} : c; } }",
17219                "(b)",
17220                "!b",
17221                "/*n*/ ",
17222            ),
17223            LANG::Groovy => ("def f(){ def x = a ? {} : c }", "(b)", "!b", "/*n*/ "),
17224            LANG::Javascript | LANG::Typescript | LANG::Tsx | LANG::Mozjs => {
17225                ("function f(){ var x = a ? {} : c; }", "(b)", "!b", "/*n*/ ")
17226            }
17227            LANG::Php => (
17228                "<?php function f(){ $x = $a ? {} : $c; }",
17229                "($b)",
17230                "!$b",
17231                "/*n*/ ",
17232            ),
17233            // Perl has no block comment: `#` runs to end of line, so the
17234            // comment must carry its own newline.
17235            LANG::Perl => ("sub f { my $x = $a ? {} : $c; }", "($b)", "!$b", "# n\n "),
17236            _ => return None,
17237        };
17238        let build = |operand: &str, with_comment: bool| {
17239            let slot = if with_comment {
17240                format!("{comment}{operand}")
17241            } else {
17242                operand.to_owned()
17243            };
17244            template.replace("{}", &slot)
17245        };
17246        Some([
17247            (build(paren, false), build(paren, true)),
17248            (build(negated, false), build(negated, true)),
17249        ])
17250    }
17251
17252    #[test]
17253    fn a_comment_before_a_branch_operand_changes_nothing() {
17254        let mut checked = 0;
17255        for lang in LANG::into_enum_iter() {
17256            if !lang.is_enabled() {
17257                continue;
17258            }
17259            let Some(pairs) = cases(lang) else { continue };
17260            checked += 1;
17261            for (base, commented) in pairs {
17262                assert_eq!(
17263                    conditions(lang, &commented),
17264                    conditions(lang, &base),
17265                    "{lang:?}: a comment changed the ABC conditions of a ternary\n  \
17266                     without: {base}\n  with:    {commented}"
17267                );
17268            }
17269        }
17270        assert!(
17271            checked > 0,
17272            "no ternary language enabled; this test asserted nothing"
17273        );
17274    }
17275
17276    /// The absolute values the invariance test compares against, so a
17277    /// regression that moved *both* sides equally still fails.
17278    ///
17279    /// expected: `a ? (b) : c` counts the `?` marker plus the condition
17280    /// `a` in boolean context = 2. Negating the consequence adds one
17281    /// more, since `!b` establishes boolean content for that slot = 3.
17282    #[test]
17283    fn the_baseline_values_are_two_and_three() {
17284        let mut checked = 0;
17285        for lang in LANG::into_enum_iter() {
17286            if !lang.is_enabled() {
17287                continue;
17288            }
17289            let Some([(paren, _), (negated, _)]) = cases(lang) else {
17290                continue;
17291            };
17292            assert_eq!(
17293                conditions(lang, &paren),
17294                2,
17295                "{lang:?}: parenthesised branch"
17296            );
17297            assert_eq!(conditions(lang, &negated), 3, "{lang:?}: negated branch");
17298            checked += 1;
17299        }
17300        assert!(
17301            checked > 0,
17302            "no ternary language enabled; this test asserted nothing"
17303        );
17304    }
17305}
17306
17307/// A keyword negation must score like its symbolic twin (#1182).
17308///
17309/// `not b` and `!b` are the same negation — they differ in precedence,
17310/// not in meaning, and ABC counts the negation rather than the parse.
17311/// Ruby and Perl tested only the `!` token, so `if not b` scored 0
17312/// against `if !b`'s 1, and a `not` ternary scored 2 against the `!`
17313/// form's 4.
17314///
17315/// Lua and Elixir were checked in the same sweep and were already
17316/// correct: Lua's only negation keyword *is* `not` and it was the token
17317/// being tested, and Elixir reaches the same count by another path.
17318/// They are exercised here so a future edit cannot regress them
17319/// silently. Python counts `not` through its own dispatcher arm and has
17320/// no `!` spelling to compare against.
17321// Gated on the union of every row below — the three `pairs` languages
17322// plus Lua, whose test is separate — so a build enabling none of them
17323// drops the module instead of failing its guards
17324// (`.claude/rules/testing.md`, #1286 / #1411). The three tests do *not*
17325// share a row set, so each carries its own narrower gate: a Lua-only
17326// build must not be asked to run the Ruby/Perl baselines.
17327#[cfg(test)]
17328#[cfg(any(
17329    feature = "elixir",
17330    feature = "lua",
17331    feature = "perl",
17332    feature = "ruby"
17333))]
17334mod keyword_negation_parity {
17335    use crate::test_support::metrics_verbatim;
17336    use crate::{LANG, MetricsOptions};
17337
17338    fn conditions(lang: LANG, source: &str) -> u64 {
17339        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
17340            .abc
17341            .conditions_sum()
17342    }
17343
17344    /// `(bang_form, keyword_form)` pairs that must score identically.
17345    #[cfg(any(feature = "elixir", feature = "perl", feature = "ruby"))]
17346    fn pairs(lang: LANG) -> Option<Vec<(String, String)>> {
17347        let build =
17348            |t: &str| -> (String, String) { (t.replace("{NOT}", "!"), t.replace("{NOT}", "not ")) };
17349        let templates: &[&str] = match lang {
17350            LANG::Ruby => &[
17351                "def f(b)\n  if {NOT}b\n    1\n  end\nend\n",
17352                "def f(a, b, c)\n  x = a ? ({NOT}b) : ({NOT}c)\nend\n",
17353                "def f(a, b, c)\n  x = a ? b : ({NOT}c)\nend\n",
17354            ],
17355            LANG::Perl => &[
17356                "sub f { if ({NOT}$b) { 1; } }",
17357                "sub f { my $x = $a ? ({NOT}$b) : ({NOT}$c); }",
17358                "sub f { my $x = $a ? $b : ({NOT}$c); }",
17359            ],
17360            // Already correct before #1182; pinned so they stay that way.
17361            LANG::Elixir => &["def f(b) do\n  if {NOT}b do\n    1\n  end\nend\n"],
17362            _ => return None,
17363        };
17364        Some(templates.iter().map(|t| build(t)).collect())
17365    }
17366
17367    #[test]
17368    #[cfg(any(feature = "elixir", feature = "perl", feature = "ruby"))]
17369    fn the_not_keyword_scores_like_bang() {
17370        let mut checked = 0;
17371        for lang in LANG::into_enum_iter() {
17372            if !lang.is_enabled() {
17373                continue;
17374            }
17375            let Some(pairs) = pairs(lang) else { continue };
17376            checked += 1;
17377            for (bang, keyword) in pairs {
17378                assert_eq!(
17379                    conditions(lang, &keyword),
17380                    conditions(lang, &bang),
17381                    "{lang:?}: `not` and `!` scored differently\n  bang:    {bang}\n  keyword: {keyword}"
17382                );
17383            }
17384        }
17385        assert!(
17386            checked > 0,
17387            "no language enabled; this test asserted nothing"
17388        );
17389    }
17390
17391    /// The absolute values, so a regression that moved both spellings
17392    /// equally still fails.
17393    ///
17394    /// expected: `if !b` is one condition — the negated bare operand.
17395    /// `a ? (!b) : (!c)` is four: the `?` marker, the condition `a` in
17396    /// boolean context, and one per negated branch operand.
17397    #[test]
17398    #[cfg(any(feature = "perl", feature = "ruby"))]
17399    fn the_baseline_values_are_one_and_four() {
17400        let mut checked = 0;
17401        for (lang, guard, ternary) in [
17402            (
17403                LANG::Ruby,
17404                "def f(b)\n  if not b\n    1\n  end\nend\n",
17405                "def f(a, b, c)\n  x = a ? (not b) : (not c)\nend\n",
17406            ),
17407            (
17408                LANG::Perl,
17409                "sub f { if (not $b) { 1; } }",
17410                "sub f { my $x = $a ? (not $b) : (not $c); }",
17411            ),
17412        ] {
17413            if !lang.is_enabled() {
17414                continue;
17415            }
17416            assert_eq!(conditions(lang, guard), 1, "{lang:?}: `if not b`");
17417            assert_eq!(conditions(lang, ternary), 4, "{lang:?}: `not` ternary");
17418            checked += 1;
17419        }
17420        assert!(
17421            checked > 0,
17422            "no language enabled; this test asserted nothing"
17423        );
17424    }
17425
17426    /// Lua's only negation keyword is `not`, so it has no `!` twin to
17427    /// compare against — its guard is that the keyword counts at all.
17428    #[test]
17429    #[cfg(feature = "lua")]
17430    fn lua_counts_its_only_negation_keyword() {
17431        // Was a silent `if !is_enabled() { return; }`, which made the
17432        // whole test a no-op rather than absent whenever the runtime
17433        // check and the feature disagreed. The `cfg` above is the
17434        // absence half; this is the loud residual half (#1286).
17435        assert!(
17436            LANG::Lua.is_enabled(),
17437            "compiled under `feature = \"lua\"` but `LANG::Lua` reports disabled; \
17438             this test asserted nothing"
17439        );
17440        assert_eq!(
17441            conditions(
17442                LANG::Lua,
17443                "function f(b)\n  if not b then return 1 end\nend\n"
17444            ),
17445            1
17446        );
17447    }
17448}
17449
17450/// A numeric literal used as a bare `&&` / `||` operand is a Fitzpatrick
17451/// Rule 9 unary condition in every truthy-valued language (#1379).
17452///
17453/// Ruby's terminal-bool set named `integer` and none of `float` /
17454/// `rational` / `complex`, Elixir's named `integer` and not `float`, and
17455/// Perl's named no numeric kind at all — so `a && 1.0` scored one
17456/// condition where `a && 1` scored two, and in Perl even `$a && 1`
17457/// scored one. Python had both kinds since #772 and is the control the
17458/// other three were brought level with.
17459///
17460/// #1410 closed the six sets #1379 deferred: PHP, Groovy, and the
17461/// name-keyed C / C++ / Mozcpp / Objective-C set, where the omission
17462/// was visible *within* one language — `if (true)` scored one condition
17463/// and `if (1)` none, because `"true"` was in the set and
17464/// `number_literal` was not.
17465///
17466/// The headline claim of each case is a *comparison*: a numeric operand
17467/// must score exactly what an identifier operand scores in the same slot.
17468/// That is what discriminates the defect — with the kind missing from the
17469/// set the numeric form drops while the identifier form does not — and it
17470/// gives a failure message naming both spellings.
17471/// `every_operand_scores_its_recorded_values` then pins the absolute
17472/// numbers, so a regression moving *both* sides equally still fails, and
17473/// carries the cross-metric anchor `.claude/rules/grammar-dispatch.md` §8
17474/// asks for.
17475///
17476/// Two slots per language, because the sets feed two independent walker
17477/// paths (grammar-dispatch §11) and Perl's defect showed in both: the
17478/// operands of a `&&` chain, and the predicate of an `if`. A fixture of
17479/// only the first leaves `perl_count_condition` and `ruby_count_condition`
17480/// untested. Since #1520 Ruby's predicate pays for any operand no other
17481/// arm counts, terminal or not, so for Ruby only the chain half can tell
17482/// a kind missing from the set; the predicate half pins the slot itself.
17483///
17484/// The recorded cyclomatic figure is the same 3 for both slots — one file
17485/// space, one function space, one decision — while conditions differ (2
17486/// for the chain, 1 for the predicate, since ABC scores an `if` through
17487/// its predicate rather than the keyword). So §8's `conditions ==
17488/// cyclomatic - 1` identity is chain-specific arithmetic, not a law; what
17489/// generalises is that cyclomatic must not move when only the operand
17490/// spelling does, which is what makes a `conditions` move unambiguously
17491/// ABC's.
17492///
17493/// `for_each_case` guards the two ways this table could decay into
17494/// asserting nothing: an emptied `numerics` slice, and a template that
17495/// loses its `{}` slot (which would make every comparison `x == x`).
17496/// Both were reachable in the first draft and neither failed a test.
17497#[cfg(test)]
17498#[cfg(any(
17499    feature = "ruby",
17500    feature = "elixir",
17501    feature = "perl",
17502    feature = "python",
17503    feature = "lua",
17504    feature = "javascript",
17505    feature = "php",
17506    feature = "groovy",
17507    feature = "c",
17508    feature = "cpp",
17509    feature = "mozcpp",
17510    feature = "objc"
17511))]
17512mod numeric_bool_operands {
17513    use crate::test_support::metrics_verbatim;
17514    use crate::{LANG, MetricsOptions};
17515
17516    /// One fixture shape: a source template with a `{}` operand slot, and
17517    /// the `abc.conditions_sum` / `cyclomatic_sum` every spelling of that
17518    /// operand must produce.
17519    type Slot = (&'static str, u64, u64);
17520
17521    /// A language's two slots, its identifier baseline operand, the
17522    /// truthy literal operands that must score the same, and how many of
17523    /// those there should be. Every row but the C family's lists numeric
17524    /// literals only; that one adds `char_literal`, which is a second
17525    /// integral-literal kind rather than a second numeric spelling.
17526    ///
17527    /// The count is not bookkeeping. `for_each_case` counts *languages*,
17528    /// so trimming a row's operand list back to `&["1"]` — which is
17529    /// exactly the pre-#1379 fixture — left the whole module green when
17530    /// measured. Pinning the length makes that a deliberate two-line
17531    /// edit instead of a silent one.
17532    type Case = ([Slot; 2], &'static str, &'static [&'static str], usize);
17533
17534    fn conditions(lang: LANG, source: &str) -> u64 {
17535        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
17536            .abc
17537            .conditions_sum()
17538    }
17539
17540    fn cyclomatic_sum(lang: LANG, source: &str) -> u64 {
17541        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
17542            .cyclomatic
17543            .cyclomatic_sum()
17544    }
17545
17546    /// `([chain_slot, condition_slot], identifier, numerics)` per language.
17547    ///
17548    /// `{}` is the right-hand operand of a two-operand short-circuit chain
17549    /// in the first slot and the whole `if` predicate in the second, so
17550    /// every fixture differs from its own baseline in exactly one token.
17551    ///
17552    /// Each row lists **one spelling per numeric kind the grammar emits**,
17553    /// which is the unit that matters: #1379's misses were sibling rules
17554    /// under a shared supertype, not aliases of one rule.
17555    ///
17556    /// - Ruby: `integer` / `float` / `rational` / `complex`. `1r` parses
17557    ///   to `rational(integer)`, `2i` to `complex(integer)` and `1ri` to
17558    ///   `complex(rational(integer))` — verified by `bca dump` — and the
17559    ///   walker cannot descend into any wrapper, so listing the wrapper
17560    ///   is the only way each scores at all. Radix prefixes and `_`
17561    ///   separators fold into `integer`.
17562    /// - Perl: all five members of the hidden `_numeric_literals` choice.
17563    ///   `0xff` and `1.5e10` are here because the first cut of #1379
17564    ///   named only `integer` and `floating_point` and left them scoring
17565    ///   1 against an identifier's 2. `017` is *not* a sixth row — it
17566    ///   lexes as `integer`, and the `octal` kind is unreachable.
17567    /// - Elixir: `integer` / `float` / `char`. `?a` is the codepoint 97,
17568    ///   a numeric literal wearing a sigil.
17569    /// - PHP: `integer` / `float`. Every radix prefix, `_` separator and
17570    ///   exponent folds into those two, and the `float` *type* keyword
17571    ///   `Float2` is a third id rendering to the same name that must
17572    ///   stay out of the set (#1410).
17573    /// - Groovy: one consolidated `number_literal`, type suffixes
17574    ///   (`1L`, `1.5f`, `1G`) included.
17575    /// - C, C++, Mozcpp and Objective-C: one consolidated
17576    ///   `number_literal`, plus `char_literal` — not a numeric spelling
17577    ///   but a second integral-literal *kind*, since `'c'` has type
17578    ///   `int` in C and `char` in C++ and is truthy for the same reason.
17579    ///   All four share one name-keyed set and so run the same fixture,
17580    ///   which is the only coverage `Mozcpp` can have: it owns no file
17581    ///   extension, and `metrics_verbatim` is the one entry point that
17582    ///   reaches a `LANG` without going through one.
17583    ///
17584    /// Python, Lua and JavaScript were already correct and ride along as
17585    /// controls — they are what the first three were measured against,
17586    /// and a future edit that breaks them fails here too. Each folds
17587    /// every radix into one `Number` / `integer` kind, checked by
17588    /// measurement.
17589    fn cases(lang: LANG) -> Option<Case> {
17590        Some(match lang {
17591            LANG::Ruby => (
17592                [
17593                    ("def f(a)\n  a && {}\nend\n", 2, 3),
17594                    ("def f\n  if {}\n    1\n  end\nend\n", 1, 3),
17595                ],
17596                "b",
17597                &["1", "1.0", "1r", "2i", "1ri"],
17598                5,
17599            ),
17600            LANG::Elixir => (
17601                [
17602                    ("def f(a) do\n  a && {}\nend\n", 2, 3),
17603                    ("def f() do\n  if {} do\n    1\n  end\nend\n", 1, 3),
17604                ],
17605                "b",
17606                &["1", "1.0", "?a"],
17607                3,
17608            ),
17609            LANG::Perl => (
17610                [
17611                    ("sub f {\n  my $x = $a && {};\n}\n", 2, 3),
17612                    ("sub f {\n  if ({}) { 1; }\n}\n", 1, 3),
17613                ],
17614                "$b",
17615                &["1", "1.0", "1.5e10", "0xff"],
17616                4,
17617            ),
17618            LANG::Python => (
17619                [
17620                    ("def f(a):\n    return a and {}\n", 2, 3),
17621                    ("def f():\n    if {}:\n        return 1\n", 1, 3),
17622                ],
17623                "b",
17624                &["1", "1.0", "0xff", "1j"],
17625                4,
17626            ),
17627            LANG::Lua => (
17628                [
17629                    ("function f(a)\n  return a and {}\nend\n", 2, 3),
17630                    ("function f()\n  if {} then return 1 end\nend\n", 1, 3),
17631                ],
17632                "b",
17633                &["1", "1.0", "0xff"],
17634                3,
17635            ),
17636            LANG::Javascript => (
17637                [
17638                    ("function f(a) {\n  return a && {};\n}\n", 2, 3),
17639                    ("function f() {\n  if ({}) { return 1; }\n}\n", 1, 3),
17640                ],
17641                "b",
17642                &["1", "1.0", "0xff"],
17643                3,
17644            ),
17645            LANG::Php => (
17646                [
17647                    ("<?php\nfunction f($a) {\n  return $a && {};\n}\n", 2, 3),
17648                    ("<?php\nfunction f() {\n  if ({}) { return 1; }\n}\n", 1, 3),
17649                ],
17650                "$b",
17651                &["1", "1.0"],
17652                2,
17653            ),
17654            LANG::Groovy => (
17655                [
17656                    ("def f(a) {\n  return a && {}\n}\n", 2, 3),
17657                    ("def f() {\n  if ({}) { return 1 }\n}\n", 1, 3),
17658                ],
17659                "b",
17660                &["1"],
17661                1,
17662            ),
17663            LANG::C | LANG::Cpp | LANG::Mozcpp | LANG::Objc => (
17664                [
17665                    ("int f(int a) {\n  return a && {};\n}\n", 2, 3),
17666                    ("int f() {\n  if ({}) { return 1; }\n}\n", 1, 3),
17667                ],
17668                "b",
17669                &["1", "'c'"],
17670                2,
17671            ),
17672            _ => return None,
17673        })
17674    }
17675
17676    /// Runs `check` once per enabled language that has a case, having
17677    /// first established that the case can still assert something.
17678    ///
17679    /// Three guards, all of which had to be added after the first draft
17680    /// shipped without them and a measured perturbation of each left the
17681    /// whole module green:
17682    ///
17683    /// - **`checked > 0`** is the non-vacuity half of the rule in
17684    ///   `.claude/rules/testing.md`. The `#[cfg(any(feature = …))]` on
17685    ///   this module makes the tests *absent* when no truthy-valued
17686    ///   language is compiled in; this catches the residual case where
17687    ///   the runtime `is_enabled()` check stops agreeing with the feature
17688    ///   it compiled under.
17689    /// - **the `numerics` list is non-empty and keeps its recorded
17690    ///   length** — `checked` counts *languages*, so trimming a row's
17691    ///   numeric list back to `&["1"]` (the pre-#1379 fixture) or
17692    ///   emptying it left every test passing when measured, with that
17693    ///   language's coverage deleted. Two assertions, because the
17694    ///   length check passes an emptied row whose count was zeroed with
17695    ///   it.
17696    /// - **every template keeps its `{}`** — without the slot,
17697    ///   `str::replace` is a no-op, baseline and candidate are computed
17698    ///   from the same string, and the comparison degenerates to
17699    ///   `x == x`.
17700    ///
17701    /// Sharing one driver means no test can lose any of the three.
17702    fn for_each_case(check: impl Fn(LANG, Case)) {
17703        let mut checked = 0;
17704        for lang in LANG::into_enum_iter() {
17705            if !lang.is_enabled() {
17706                continue;
17707            }
17708            let Some(case @ (slots, _, numerics, expected_kinds)) = cases(lang) else {
17709                continue;
17710            };
17711            // Emptying a row is the half the length check below cannot
17712            // see: `&[]` against an `expected_kinds` of 0 agrees with
17713            // itself, the inner loops run zero times, and `checked` —
17714            // which counts languages, not spellings — still reads
17715            // satisfied.
17716            assert!(
17717                !numerics.is_empty(),
17718                "{lang:?}: the numeric-operand list is empty; this language asserted nothing"
17719            );
17720            assert_eq!(
17721                numerics.len(),
17722                expected_kinds,
17723                "{lang:?}: the numeric-operand list no longer covers one spelling \
17724                 per grammar numeric kind"
17725            );
17726            for (template, _, _) in slots {
17727                assert!(
17728                    template.contains("{}"),
17729                    "{lang:?}: template lost its `{{}}` operand slot: {template}"
17730                );
17731            }
17732            check(lang, case);
17733            checked += 1;
17734        }
17735        assert!(
17736            checked > 0,
17737            "no truthy-valued language enabled; this test asserted nothing"
17738        );
17739    }
17740
17741    #[test]
17742    fn a_numeric_operand_scores_like_an_identifier_operand() {
17743        for_each_case(|lang, (slots, identifier, numerics, _)| {
17744            for (template, _, _) in slots {
17745                let baseline = conditions(lang, &template.replace("{}", identifier));
17746                for numeric in numerics {
17747                    let source = template.replace("{}", numeric);
17748                    let scored = conditions(lang, &source);
17749                    assert_eq!(
17750                        scored, baseline,
17751                        "{lang:?}: `{numeric}` scored {scored} unary conditions against \
17752                         `{identifier}`'s {baseline}\n  source: {source}"
17753                    );
17754                }
17755            }
17756        });
17757    }
17758
17759    /// The absolute anchor under the comparison above: every operand
17760    /// spelling must produce the slot's recorded `conditions`, and must
17761    /// leave `cyclomatic` alone. The second half is what rules out a
17762    /// regression that moved both metrics together.
17763    #[test]
17764    fn every_operand_scores_its_recorded_values() {
17765        for_each_case(|lang, (slots, identifier, numerics, _)| {
17766            for (template, expected_conditions, expected_cyclomatic) in slots {
17767                for operand in std::iter::once(identifier).chain(numerics.iter().copied()) {
17768                    let source = template.replace("{}", operand);
17769                    assert_eq!(
17770                        conditions(lang, &source),
17771                        expected_conditions,
17772                        "{lang:?}: `{operand}` conditions\n  source: {source}"
17773                    );
17774                    assert_eq!(
17775                        cyclomatic_sum(lang, &source),
17776                        expected_cyclomatic,
17777                        "{lang:?}: `{operand}` cyclomatic_sum\n  source: {source}"
17778                    );
17779                }
17780            }
17781        });
17782    }
17783}
17784
17785/// A relational construct with its own grammar production must score
17786/// like an identifier in the same boolean slot (#1449, #1461).
17787///
17788/// Every language here spells at least one boolean test as a dedicated
17789/// node rather than as a `binary_expression`, so no comparison-token
17790/// arm ever sees it. A kind missing from the former per-language
17791/// terminal-kind sets scored **zero**, silently, and zero is
17792/// indistinguishable from a legitimate zero — which is why the headline
17793/// claim of each case is a *comparison* against an identifier control
17794/// in the identical slot rather than an absolute number.
17795///
17796/// The constructs, and the route each fix took:
17797///
17798/// - **Groovy** `a in l` / `a !in l` (`membership_expression`), counted
17799///   as a node by `groovy_count_token_condition`. The other four —
17800///   `a === b` / `a !== b` (`identity_expression`) and `s =~ /p/` /
17801///   `s ==~ /p/` (`regex_find_expression`, `regex_match_expression`) —
17802///   are counted there as operator tokens, so all five score outside a
17803///   boolean slot as `==` does, and `groovy_condition_scores_itself`
17804///   lists each so the slot holding one does not pay again; a construct
17805///   takes exactly one of the two routes, never both
17806///   (`.claude/rules/grammar-dispatch.md` §5).
17807/// - **Perl** `/^#/` (`pattern_matcher`) and `m{^#}`
17808///   (`pattern_matcher_m`), the two spellings of a match against the
17809///   implicit `$_`. They are sibling rules, not aliases, so both are
17810///   covered — a fixture carrying only the first would have left the
17811///   `m{}` half at zero and read as covered. Since #1467 they take the
17812///   other route: a gated arm in `PerlCode::compute` counts them by use,
17813///   and they have left the former `perl_bool_terminal_kinds!()`.
17814/// - **Ruby** `a in Integer` (`test_pattern`), the one-line pattern
17815///   test. Decided against `match_pattern` (`expr => pat`), which
17816///   raises rather than yielding a boolean.
17817/// - **Objective-C** `@available(iOS 13.0, *)` and its
17818///   `__builtin_available` synonym (`available_expression`), the runtime
17819///   OS-version check. Not relational, but boolean by definition and
17820///   invisible to every comparison-token arm, so it measured short in
17821///   exactly the same way (#1457). Its entry landed in the *name*-keyed
17822///   former `cpp_bool_terminal_kinds!()` that C, C++ and Mozcpp shared, which is
17823///   why `the_c_family_grammars_do_not_emit_available_expression` below
17824///   pins the inertness the other three rely on.
17825///
17826/// Rust's `matches!(…)` measures short in the same way, and is
17827/// deliberately **not** here: its fix is `macro_invocation`, which also
17828/// catches `cfg!` / `dbg!` / `todo!`, and the one corpus snapshot it
17829/// moves is a `cfg!` rather than a `matches!`. That breadth is what
17830/// #1461 item 2 exists to decide, so it is left to that issue rather
17831/// than settled by inclusion here.
17832///
17833/// Two slots per language, because they are two independent walker
17834/// paths (§11) and every construct above measured short in **both**: the
17835/// operands of a `&&` chain, and the predicate of an `if`. A fixture of
17836/// only one leaves the other untested.
17837///
17838/// Each row also carries the construct's negated spelling, which reaches
17839/// the construct only through the language's value peel: the `!…` /
17840/// `(…)` wrapper is peeled before `count_boolean_slot` asks
17841/// `<lang>_condition_scores_itself` whether the operand already paid.
17842/// Without a negated row the peel is the path no fixture here exercises.
17843///
17844/// Since #1526 no language here has a terminal-kind set: the chain
17845/// operands and the slots both score through the shared
17846/// `count_boolean_slot` rule, and Perl's rows score through the by-use
17847/// arm (#1467).
17848///
17849/// The `cyclomatic` half of `every_construct_scores_its_recorded_values`
17850/// is what rules out a regression that moved both metrics together:
17851/// cyclomatic must not move when only the operand spelling does, which
17852/// is what makes a `conditions` move unambiguously ABC's. The recorded
17853/// figures differ per slot (Rust's chain slot carries the extra `&&`
17854/// decision), so each slot pins its own pair rather than sharing one.
17855// Gated on the union of the three languages with rows, so a build
17856// enabling none of them drops the module instead of failing its
17857// guards (`.claude/rules/testing.md`, #1286 / #1411).
17858#[cfg(test)]
17859#[cfg(any(
17860    feature = "groovy",
17861    feature = "perl",
17862    feature = "ruby",
17863    feature = "objc"
17864))]
17865mod own_production_bool_constructs {
17866    use crate::test_support::metrics_verbatim;
17867    use crate::{LANG, MetricsOptions};
17868
17869    /// One fixture shape: a source template with a `{}` slot for the
17870    /// whole boolean expression, and the `abc.conditions_sum` /
17871    /// `cyclomatic_sum` every spelling in that slot must produce.
17872    type Slot = (&'static str, u64, u64);
17873
17874    /// A language's two slots, its identifier control, the constructs
17875    /// that must score the same, and how many of those there should be.
17876    ///
17877    /// The count is not bookkeeping. `for_each_case` counts *languages*,
17878    /// so trimming Groovy's list back to a single construct — or
17879    /// dropping Perl's `m{}` half, which is a separate grammar rule from
17880    /// its `//` half — leaves the module green with that coverage
17881    /// deleted. Pinning the length makes such a trim a deliberate
17882    /// two-line edit instead of a silent one.
17883    type Case = ([Slot; 2], &'static str, &'static [&'static str], usize);
17884
17885    fn conditions(lang: LANG, source: &str) -> u64 {
17886        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
17887            .abc
17888            .conditions_sum()
17889    }
17890
17891    fn cyclomatic_sum(lang: LANG, source: &str) -> u64 {
17892        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
17893            .cyclomatic
17894            .cyclomatic_sum()
17895    }
17896
17897    /// `([chain_slot, predicate_slot], identifier, constructs)` per
17898    /// language.
17899    ///
17900    /// `{}` is the left-hand operand of a two-operand short-circuit
17901    /// chain in the first slot and the whole `if` predicate in the
17902    /// second, so every fixture differs from its own control in exactly
17903    /// the construct under test.
17904    ///
17905    /// Ruby's chain slot parenthesises the slot because `in` binds
17906    /// looser than `&&` there; the control is parenthesised identically,
17907    /// so the parens cancel out of the comparison and the peel unwraps
17908    /// them for both sides alike.
17909    fn cases(lang: LANG) -> Option<Case> {
17910        Some(match lang {
17911            LANG::Groovy => (
17912                [
17913                    ("def f(a, b, l, s) {\n  return {} && b\n}\n", 2, 3),
17914                    ("def f(a, b, l, s) {\n  if ({}) { return 1 }\n}\n", 1, 3),
17915                ],
17916                "b",
17917                &[
17918                    "a in l",
17919                    "a !in l",
17920                    "a === b",
17921                    "a !== b",
17922                    "s =~ /p/",
17923                    "s ==~ /p/",
17924                    "!(a in l)",
17925                    // The indexing / navigation kinds added by #1466.
17926                    // These two are the cyclomatic-neutral ones, so
17927                    // they belong in this module's same-as-the-control
17928                    // shape. The three *null-safe* spellings (`a?.b`,
17929                    // `a??.b`, `l?[0]`) each short-circuit on a null
17930                    // receiver and so add a cyclomatic decision, which
17931                    // a row whose contract is "the control's
17932                    // cyclomatic" cannot express; they get their own
17933                    // test,
17934                    // `groovy_safe_navigation_closes_the_two_below_gap`,
17935                    // which anchors them on that axis explicitly.
17936                    "l[0]",
17937                    "a.@b",
17938                ],
17939                9,
17940            ),
17941            LANG::Perl => (
17942                [
17943                    ("sub f {\n  my $x = {} && $b;\n}\n", 2, 3),
17944                    ("sub f {\n  if ({}) { 1; }\n}\n", 1, 3),
17945                ],
17946                "$a",
17947                &["/^#/", "m{^#}", "!/^#/"],
17948                3,
17949            ),
17950            LANG::Ruby => (
17951                [
17952                    ("def f(a)\n  ({}) && b\nend\n", 2, 3),
17953                    ("def f(a)\n  if {}\n    1\n  end\nend\n", 1, 3),
17954                ],
17955                "a",
17956                &["a in Integer", "!(a in Integer)"],
17957                2,
17958            ),
17959            // Objective-C's `@available` is not relational like the
17960            // constructs above, but it reaches the terminal set by the
17961            // route Groovy's `in` takes: a dedicated `available_expression`
17962            // production whose value is a boolean, seen by no
17963            // comparison-token arm (#1457). Both spellings are listed
17964            // because they are alternatives of one rule's leading token
17965            // rather than two rules — so unlike Perl's `m{}` above they
17966            // cannot drift apart, and the second row is here to keep that
17967            // claim measured rather than assumed.
17968            LANG::Objc => (
17969                [
17970                    ("int f(int a) {\n  return {} && b;\n}\n", 2, 3),
17971                    ("int f() {\n  if ({}) { return 1; }\n}\n", 1, 3),
17972                ],
17973                "a",
17974                &[
17975                    "@available(iOS 13.0, *)",
17976                    "__builtin_available(macOS 10.15, *)",
17977                    "!@available(iOS 13.0, *)",
17978                ],
17979                3,
17980            ),
17981            _ => return None,
17982        })
17983    }
17984
17985    /// Runs `check` once per enabled language that has a case, having
17986    /// first established that the case can still assert something.
17987    ///
17988    /// The four guards mirror `numeric_bool_operands::for_each_case`,
17989    /// where each was added only after a measured perturbation of it
17990    /// left that module green: `checked > 0` for the runtime half of
17991    /// the feature gate, a non-empty construct list and its recorded
17992    /// count so a trimmed row cannot vanish silently, and the `{}` slot
17993    /// so `str::replace` does not degenerate into comparing a string
17994    /// with itself.
17995    fn for_each_case(check: impl Fn(LANG, Case)) {
17996        let mut checked = 0;
17997        for lang in LANG::into_enum_iter() {
17998            if !lang.is_enabled() {
17999                continue;
18000            }
18001            let Some(case @ (slots, _, constructs, expected_constructs)) = cases(lang) else {
18002                continue;
18003            };
18004            // Both halves are needed, and the second does not imply the
18005            // first. The length check alone only asserts that the list
18006            // and its recorded count *agree*, so emptying a row and
18007            // setting its count to 0 — the shape a careless "the test
18008            // failed, fix the number" edit takes — deleted that
18009            // language's coverage with both tests still green when
18010            // measured.
18011            assert!(
18012                !constructs.is_empty(),
18013                "{lang:?}: the construct list is empty; this language asserted nothing"
18014            );
18015            assert_eq!(
18016                constructs.len(),
18017                expected_constructs,
18018                "{lang:?}: the construct list no longer covers every own-production \
18019                 boolean spelling this language was fixed for"
18020            );
18021            for (template, _, _) in slots {
18022                assert!(
18023                    template.contains("{}"),
18024                    "{lang:?}: template lost its `{{}}` slot: {template}"
18025                );
18026            }
18027            check(lang, case);
18028            checked += 1;
18029        }
18030        assert!(
18031            checked > 0,
18032            "no language with an own-production boolean construct enabled; \
18033             this test asserted nothing"
18034        );
18035    }
18036
18037    #[test]
18038    fn an_own_production_construct_scores_like_an_identifier() {
18039        for_each_case(|lang, (slots, identifier, constructs, _)| {
18040            for (template, _, _) in slots {
18041                let baseline = conditions(lang, &template.replace("{}", identifier));
18042                for construct in constructs {
18043                    let source = template.replace("{}", construct);
18044                    let scored = conditions(lang, &source);
18045                    assert_eq!(
18046                        scored, baseline,
18047                        "{lang:?}: `{construct}` scored {scored} conditions against \
18048                         `{identifier}`'s {baseline}\n  source: {source}"
18049                    );
18050                }
18051            }
18052        });
18053    }
18054
18055    /// Groovy's three null-safe spellings, on the cyclomatic axis.
18056    ///
18057    /// They cannot ride the rows above, whose contract is "scores the
18058    /// control's `conditions` *and* the control's `cyclomatic`":
18059    /// the boolean-slot rule (`groovy_count_condition`) does not move cyclomatic, but
18060    /// `?.` (`QMARKDOT`), `??.` (`QMARKQMARKDOT`) and `?[`
18061    /// (`QMARKLBRACK`) are already cyclomatic decisions in their own
18062    /// right (`src/metrics/cyclomatic/groovy.rs`), so each spelling
18063    /// scores the control's conditions against the control's cyclomatic
18064    /// **plus one**. That is the whole reason this family was the worst
18065    /// case in #1466: before the fix ABC sat *two* below its own
18066    /// decision count on `if (a?.b)`, against one below for every other
18067    /// spelling.
18068    ///
18069    /// `l?[0]` joined the family in #1471. It sat in the neutral rows
18070    /// above until then, passing only because Groovy cyclomatic had no
18071    /// `?[` arm — a fixture that would have rejected the fix for the
18072    /// very gap it recorded.
18073    ///
18074    /// Asserting the offset rather than the bare conditions is what
18075    /// makes this a §5 double-count guard as well. If a later change
18076    /// added `QMARKDOT` to `groovy_count_token_condition`'s arm while
18077    /// the wrapper stayed in the terminal set, `conditions` would go to
18078    /// `control + 1` and this test — not a snapshot — would say so.
18079    #[test]
18080    #[cfg(feature = "groovy")]
18081    fn groovy_safe_navigation_closes_the_two_below_gap() {
18082        for (template, control_conditions, control_cyclomatic) in [
18083            ("def f(a, b, l) {\n  return {} && b\n}\n", 2, 3),
18084            ("def f(a, b, l) {\n  if ({}) { return 1 }\n}\n", 1, 3),
18085        ] {
18086            let control = template.replace("{}", "b");
18087            assert_eq!(conditions(LANG::Groovy, &control), control_conditions);
18088            assert_eq!(cyclomatic_sum(LANG::Groovy, &control), control_cyclomatic);
18089
18090            for spelling in ["a?.b", "a??.b", "l?[0]"] {
18091                let source = template.replace("{}", spelling);
18092                assert_eq!(
18093                    conditions(LANG::Groovy, &source),
18094                    control_conditions,
18095                    "`{spelling}` conditions\n  source: {source}"
18096                );
18097                assert_eq!(
18098                    cyclomatic_sum(LANG::Groovy, &source),
18099                    control_cyclomatic + 1,
18100                    "`{spelling}` cyclomatic_sum: the navigation operator's own \
18101                     decision has moved\n  source: {source}"
18102                );
18103            }
18104        }
18105    }
18106
18107    /// The `!` operand is read by field, so an `extra` cannot hide it.
18108    ///
18109    /// `groovy_wrapper_operand` took `child(1)` until #1466, which is
18110    /// the `!` operand only when nothing sits between the two. A comment
18111    /// is an `extra` and occupies that slot, so `if (! /*c*/ a)` scored
18112    /// zero conditions where `if (!a)` scored one — the positional-read
18113    /// class grammar-dispatch §3 is about, and the observable half of
18114    /// the peel rewrite.
18115    ///
18116    /// `parenthesized_expression` names nothing in node-types.json, so
18117    /// it kept the positional read, and the bug, until #1455 read it as
18118    /// the first operand that is not an extra.
18119    #[test]
18120    #[cfg(feature = "groovy")]
18121    fn groovy_negation_operand_survives_an_interposed_comment() {
18122        let template = "def f(a) {\n  if ({}) { return 1 }\n}\n";
18123
18124        assert_eq!(conditions(LANG::Groovy, &template.replace("{}", "!a")), 1);
18125        assert_eq!(
18126            conditions(LANG::Groovy, &template.replace("{}", "! /*c*/ a")),
18127            1,
18128            "the `!` operand is being read positionally again; a comment displaces it"
18129        );
18130
18131        assert_eq!(conditions(LANG::Groovy, &template.replace("{}", "(a)")), 1);
18132        assert_eq!(
18133            conditions(LANG::Groovy, &template.replace("{}", "( /*c*/ a)")),
18134            1,
18135            "the parenthesised operand is being read positionally again; a comment \
18136             displaces it"
18137        );
18138    }
18139
18140    /// The arithmetic unary operators are values, not wrappers.
18141    ///
18142    /// `groovy_count_condition` routed every `unary_expression` to the
18143    /// peel while the peel handled only the `!` spelling, so the arm
18144    /// claimed `~a` / `-a` / `+a` and dropped them
18145    /// (grammar-dispatch §7). Since #1526 the slot pays for any
18146    /// predicate no other arm counts — Groovy truth makes `if (-a)` a
18147    /// decision — so the three spellings score what `a` does. Outside a
18148    /// slot they are no negation, so a call argument scores nothing for
18149    /// them, while `!-a` scores its negation once.
18150    #[test]
18151    #[cfg(feature = "groovy")]
18152    fn groovy_arithmetic_unary_is_a_value_not_a_wrapper() {
18153        let template = "def f(a) {\n  if ({}) { return 1 }\n}\n";
18154        let call = "def f(a) {\n  g({})\n}\n";
18155
18156        assert_eq!(conditions(LANG::Groovy, &template.replace("{}", "a")), 1);
18157        for spelling in ["~a", "-a", "+a"] {
18158            assert_eq!(
18159                conditions(LANG::Groovy, &template.replace("{}", spelling)),
18160                1,
18161                "`{spelling}` in a slot pays as `a` does"
18162            );
18163            assert_eq!(
18164                conditions(LANG::Groovy, &call.replace("{}", spelling)),
18165                0,
18166                "`{spelling}` outside a slot is not a negation"
18167            );
18168            let negated = format!("!{spelling}");
18169            assert_eq!(
18170                conditions(LANG::Groovy, &call.replace("{}", &negated)),
18171                1,
18172                "`{negated}` outside a slot is one negation"
18173            );
18174        }
18175    }
18176
18177    /// The two Perl spellings must remain two distinct grammar kinds.
18178    ///
18179    /// The bare-match arm of `PerlCode::compute` lists `PatternMatcher`
18180    /// **and** `PatternMatcherM` on the stated grounds that `/^#/` and `m{^#}`
18181    /// are sibling rules rather than aliases of one. Nothing above pins
18182    /// that: if a grammar bump collapsed `m{}` onto `pattern_matcher`,
18183    /// both fixtures would keep scoring through the surviving entry,
18184    /// the `PatternMatcherM` arm would become dead code, and every test
18185    /// in this module would stay green — the silent-drift shape lesson
18186    /// 34 and grammar-dispatch §2 exist to catch.
18187    ///
18188    /// Asserting the ids differ is not enough on its own, since two
18189    /// distinct enum variants prove nothing about what the parser
18190    /// emits. So each spelling is parsed and required to produce its
18191    /// own kind and not the other's.
18192    #[test]
18193    #[cfg(feature = "perl")]
18194    fn the_two_perl_match_spellings_are_distinct_kinds() {
18195        use crate::{ParserTrait, Perl, PerlParser};
18196        use big_code_analysis_ast::test_support::ast_has_kind_id;
18197
18198        assert_ne!(
18199            Perl::PatternMatcher as u16,
18200            Perl::PatternMatcherM as u16,
18201            "the two spellings share one kind id; one bare-match arm entry is dead"
18202        );
18203
18204        for (src, present, absent) in [
18205            (
18206                "sub f { if (/^#/) { 1; } }",
18207                ("pattern_matcher", Perl::PatternMatcher as u16),
18208                ("pattern_matcher_m", Perl::PatternMatcherM as u16),
18209            ),
18210            (
18211                "sub f { if (m{^#}) { 1; } }",
18212                ("pattern_matcher_m", Perl::PatternMatcherM as u16),
18213                ("pattern_matcher", Perl::PatternMatcher as u16),
18214            ),
18215        ] {
18216            let parser = PerlParser::new(
18217                src.as_bytes().to_vec(),
18218                &std::path::PathBuf::from("f.pl"),
18219                None,
18220            );
18221            let (present_name, present_id) = present;
18222            let (absent_name, absent_id) = absent;
18223            assert!(
18224                ast_has_kind_id(&parser, present_id),
18225                "`{src}` no longer emits `{present_name}`"
18226            );
18227            assert!(
18228                !ast_has_kind_id(&parser, absent_id),
18229                "`{src}` now also emits `{absent_name}`; the two spellings have \
18230                 collapsed and one bare-match arm entry is dead"
18231            );
18232        }
18233    }
18234
18235    /// The absolute anchor under the comparison above: every spelling
18236    /// must produce the slot's recorded `conditions`, and must leave
18237    /// `cyclomatic` alone.
18238    #[test]
18239    fn every_construct_scores_its_recorded_values() {
18240        for_each_case(|lang, (slots, identifier, constructs, _)| {
18241            for (template, expected_conditions, expected_cyclomatic) in slots {
18242                for spelling in std::iter::once(identifier).chain(constructs.iter().copied()) {
18243                    let source = template.replace("{}", spelling);
18244                    assert_eq!(
18245                        conditions(lang, &source),
18246                        expected_conditions,
18247                        "{lang:?}: `{spelling}` conditions\n  source: {source}"
18248                    );
18249                    assert_eq!(
18250                        cyclomatic_sum(lang, &source),
18251                        expected_cyclomatic,
18252                        "{lang:?}: `{spelling}` cyclomatic_sum\n  source: {source}"
18253                    );
18254                }
18255            }
18256        });
18257    }
18258}
18259
18260/// A Perl statement modifier must score like the block form it is
18261/// shorthand for (#1464).
18262///
18263/// `return 1 if $x;` and `if ($x) { return 1; }` are the same decision
18264/// written two ways, and the modifier is the idiomatic Perl spelling —
18265/// `next unless $ok;`, `warn "..." if $debug;`. Perl's *cyclomatic*
18266/// dispatcher already counted all six modifier kinds, so this was a
18267/// straight ABC undercount against Perl's own decision count rather
18268/// than a policy disagreement between the two metrics.
18269///
18270/// Each pair is asserted equal **and** equal to a literal, so a
18271/// regression that zeroed both spellings still fails. The fixtures are
18272/// spell-anchored because the modifier is the only thing in them that
18273/// scores a condition: deleting `if $x` from the modifier fixture would
18274/// otherwise turn it into a silent copy of nothing at all
18275/// (`.claude/rules/testing.md`, "Perturb the fixture as well as the
18276/// production line").
18277#[cfg(test)]
18278#[cfg(feature = "perl")]
18279mod perl_statement_modifier_parity {
18280    use crate::test_support::{assert_perl_fixture_spells, metrics_verbatim};
18281    use crate::{LANG, MetricsOptions, Perl};
18282
18283    fn conditions(source: &str) -> u64 {
18284        metrics_verbatim(LANG::Perl, source.as_bytes(), MetricsOptions::default())
18285            .abc
18286            .conditions_sum()
18287    }
18288
18289    /// `(keyword, kind id, kind name)` — the four modifier keywords
18290    /// whose grammar slot is a `condition`. `for` / `foreach` is the
18291    /// fifth spelling and is covered by its own test below; `when` is
18292    /// the sixth and has no fixture at all, being unreachable from
18293    /// valid Perl (see `perl_walk_statement_modifier`).
18294    const CONDITION_MODIFIERS: [(&str, u16, &str); 4] = [
18295        ("if", Perl::IfSimpleStatement as u16, "if_simple_statement"),
18296        (
18297            "unless",
18298            Perl::UnlessSimpleStatement as u16,
18299            "unless_simple_statement",
18300        ),
18301        (
18302            "while",
18303            Perl::WhileSimpleStatement as u16,
18304            "while_simple_statement",
18305        ),
18306        (
18307            "until",
18308            Perl::UntilSimpleStatement as u16,
18309            "until_simple_statement",
18310        ),
18311    ];
18312
18313    #[test]
18314    fn a_modifier_scores_like_its_block_form() {
18315        assert!(
18316            LANG::Perl.is_enabled(),
18317            "compiled under `feature = \"perl\"` but `LANG::Perl` reports disabled; \
18318             this test asserted nothing"
18319        );
18320        for (keyword, kind, name) in CONDITION_MODIFIERS {
18321            let modifier = format!("sub f {{ my $x = shift; return 1 {keyword} $x; }}");
18322            let block = format!("sub f {{ my $x = shift; {keyword} ($x) {{ return 1; }} }}");
18323            assert_perl_fixture_spells(&modifier, &[(kind, 1, name)]);
18324            assert_eq!(
18325                conditions(&modifier),
18326                conditions(&block),
18327                "`{keyword}` modifier and block form disagree\n  modifier: {modifier}\n  block:    {block}"
18328            );
18329            assert_eq!(
18330                conditions(&modifier),
18331                1,
18332                "`{keyword}` modifier: the bare scalar predicate is one condition"
18333            );
18334        }
18335    }
18336
18337    /// The modifier's `condition` field goes through the shared
18338    /// condition classifier, not a flat `+1`, so a compound predicate
18339    /// keeps its sub-structure — three shapes the block form already
18340    /// scores above and below one.
18341    #[test]
18342    fn a_modifier_condition_keeps_its_substructure() {
18343        for (predicate, expected) in [("!$x", 1), ("($x)", 1), ("$x && $x", 2), ("$x > 2", 1)] {
18344            let modifier = format!("sub f {{ my $x = shift; return 1 if {predicate}; }}");
18345            let block = format!("sub f {{ my $x = shift; if ({predicate}) {{ return 1; }} }}");
18346            assert_perl_fixture_spells(
18347                &modifier,
18348                &[(Perl::IfSimpleStatement as u16, 1, "if_simple_statement")],
18349            );
18350            assert_eq!(
18351                conditions(&modifier),
18352                expected,
18353                "`if {predicate}` modifier\n  source: {modifier}"
18354            );
18355            assert_eq!(
18356                conditions(&block),
18357                expected,
18358                "`if ({predicate})` block form\n  source: {block}"
18359            );
18360        }
18361    }
18362
18363    /// The `for` / `foreach` modifier iterates a list and is not a
18364    /// boolean test — the grammar names its slot `list`, not
18365    /// `condition`. Pinned against the `if` modifier so the zero reads
18366    /// as a decision rather than as an arm nothing reaches.
18367    ///
18368    /// Measured: simply *adding* `ForSimpleStatement` to the dispatch
18369    /// arm does not fail this test, because the walker reads the slot by
18370    /// field name and `for_simple_statement` exposes no `condition` —
18371    /// the exclusion is structural, not a listed omission. What it does
18372    /// catch is a rework that reaches the slot some other way (a child
18373    /// index, the `list` field) or a grammar that renames the field.
18374    #[test]
18375    fn the_for_modifier_is_not_a_condition() {
18376        for keyword in ["for", "foreach"] {
18377            let source = format!("sub f {{ my $x = shift; print 5 {keyword} @$x; }}");
18378            assert_perl_fixture_spells(
18379                &source,
18380                &[(Perl::ForSimpleStatement as u16, 1, "for_simple_statement")],
18381            );
18382            let abc =
18383                metrics_verbatim(LANG::Perl, source.as_bytes(), MetricsOptions::default()).abc;
18384            assert_eq!(abc.conditions_sum(), 0, "`{keyword}` modifier: {source}");
18385            // The expected value *is* the default, so the zero above
18386            // proves nothing on its own — a fixture that stopped
18387            // parsing would satisfy it too. `my $x = shift` is the
18388            // file's one assignment, and `shift` and `print 5` its two
18389            // branches; all three must survive.
18390            assert_eq!(
18391                abc.assignments_sum(),
18392                1,
18393                "`{keyword}`: fixture stopped scoring"
18394            );
18395            assert_eq!(
18396                abc.branches_sum(),
18397                2,
18398                "`{keyword}`: fixture stopped scoring"
18399            );
18400        }
18401        assert_eq!(
18402            conditions("sub f { my $x = shift; return 1 if $x; }"),
18403            1,
18404            "control: the `if` modifier still counts, so the zero above is a \
18405             policy decision and not a dead arm"
18406        );
18407    }
18408}
18409
18410/// A non-numeric literal in a boolean operand slot must score like an
18411/// identifier in the same slot (#1462).
18412///
18413/// The sibling module above pins the *numeric* half, closed by #1410.
18414/// The non-numeric literals were never swept and were missing from
18415/// every set the numerics were added to — same mechanism, same silent
18416/// zero, same file. Measured before the fix, every row below scored
18417/// exactly one condition short of its identifier control, in **both**
18418/// slots: `x || "default"` — the language's commonest truthy-default
18419/// idiom — reported 1 against `x || y`'s 2.
18420///
18421/// Scope is the eleven truthy-valued sets. #1462 shipped eight of them
18422/// and called that complete; a whole-branch review of the batch found
18423/// Perl, Ruby and Elixir in neither the swept list nor the
18424/// compile-error exemption, each already carrying `False` / `Nil`, and
18425/// each reproducing the gap exactly. They were swept on the same terms
18426/// and are rows here. Tcl and iRules are truthy too and needed nothing:
18427/// their `quoted_word` / `braced_word_simple` / `number` kinds already
18428/// cover every literal an `expr {…}` operand can hold, so they have no
18429/// row rather than a vacuous one.
18430///
18431/// C#, Java, Kotlin, Rust and Go name no literal kind at all and stay
18432/// that way: a bare literal in a boolean slot is a compile error there,
18433/// so there is nothing to count. The C family is integer-truthy, and
18434/// #1462 deferred it for its corpus exposure. #1526 then closed its gap
18435/// without naming a kind, by making every slot pay for an occupant no
18436/// other arm counts; its rows here (#1469) pin that answer, because a
18437/// kind `cpp_condition_scores_itself` wrongly claimed would drop a slot
18438/// exactly as the missing terminal kind once did. Its `true`, `false`
18439/// and `null` rows are the exception to "measured short": the former
18440/// set already named them, and they stay so the pin covers every kind.
18441///
18442/// Three things each row pins that a conditions comparison alone
18443/// cannot:
18444///
18445/// - **The `kind_id` each spelling parses to.** This is the whole bug
18446///   class (`.claude/rules/grammar-dispatch.md` §1): the grammars here
18447///   emit `string` under two ids in JavaScript, Mozjs and Tsx and under
18448///   three in Tsx counting the type keyword, and listing the wrong one
18449///   compiles, runs, and returns the unfixed number. Asserting the id
18450///   turns that measurement into a standing claim rather than a note in
18451///   an issue. It is also the fixture anchor `.claude/rules/testing.md`
18452///   asks for — editing `"s"` to `1` in a row now fails by name instead
18453///   of quietly turning that row into a copy of the numeric module.
18454/// - **`cyclomatic` does not move.** Only the operand spelling changes
18455///   between a row and its control, so a `conditions` difference is
18456///   unambiguously ABC's.
18457/// - **Both walker paths.** The sets feed two structurally independent
18458///   consumers (§11) — the operands of a `&&` / `and` chain, and the
18459///   predicate of an `if` — and every row measured short in both. A
18460///   fixture of only one leaves the other path invisible.
18461///
18462/// The JS-family rows go through `js_literals!` rather than four hand-
18463/// written arrays, because the four languages differ in exactly one
18464/// place — which `string` alias an operand slot carries — and making
18465/// that the macro's only argument states the asymmetry instead of
18466/// leaving a reader to diff four near-identical lists for it.
18467#[cfg(test)]
18468#[cfg(any(
18469    feature = "javascript",
18470    feature = "mozjs",
18471    feature = "typescript",
18472    feature = "python",
18473    feature = "lua",
18474    feature = "php",
18475    feature = "groovy",
18476    feature = "perl",
18477    feature = "ruby",
18478    feature = "elixir",
18479    feature = "c",
18480    feature = "cpp",
18481    feature = "mozcpp",
18482    feature = "objc"
18483))]
18484mod literal_bool_operands {
18485    use crate::test_support::{assert_fixture_spells, metrics_verbatim};
18486    use crate::{LANG, MetricsOptions};
18487
18488    /// One fixture shape: a source template with a `{}` operand slot,
18489    /// and the `abc.conditions_sum` / `cyclomatic_sum` every spelling of
18490    /// that operand must produce.
18491    type Slot = (&'static str, u64, u64);
18492
18493    /// One literal operand: its spelling, and the `kind_id` the grammar
18494    /// must emit for it.
18495    type Literal = (&'static str, u16);
18496
18497    /// A language's two slots, its identifier baseline operand, the
18498    /// literal operands that must score the same, and how many of those
18499    /// there should be.
18500    ///
18501    /// The count is not bookkeeping. `for_each_case` counts *languages*,
18502    /// so trimming a row's literal list — the pre-#1462 state is the
18503    /// empty list — would leave the whole module green.
18504    type Case = ([Slot; 2], &'static str, &'static [Literal], usize);
18505
18506    /// The seven non-numeric literal kinds of a JS-family grammar, with
18507    /// the `string` alias an operand slot carries passed in: `String2`
18508    /// for JavaScript, Mozjs and Tsx, `String` for TypeScript. That one
18509    /// argument is the entire difference between the four rows, and it
18510    /// is the same split `Checker::is_string` makes (§7).
18511    macro_rules! js_literals {
18512        ($Lang:ident, $string:ident) => {
18513            &[
18514                ("\"s\"", crate::$Lang::$string as u16),
18515                ("`t`", crate::$Lang::TemplateString as u16),
18516                ("/re/", crate::$Lang::Regex as u16),
18517                ("null", crate::$Lang::Null as u16),
18518                ("undefined", crate::$Lang::Undefined as u16),
18519                ("{}", crate::$Lang::Object as u16),
18520                ("[]", crate::$Lang::Array as u16),
18521            ]
18522        };
18523    }
18524
18525    const JS_SLOTS: [Slot; 2] = [
18526        ("function f(a) {\n  return a && {};\n}\n", 2, 3),
18527        ("function f() {\n  if ({}) { return 1; }\n}\n", 1, 3),
18528    ];
18529
18530    /// The five non-numeric literal kinds every C-family grammar shares,
18531    /// then the grammar's own. The string and null spellings are passed
18532    /// in because Objective-C writes its strings `@"s"` and C++ its null
18533    /// `nullptr`; each still parses to the shared `string_literal` /
18534    /// `null` kind, so the row stays one spelling per kind.
18535    macro_rules! c_family_literals {
18536        ($Lang:ident, $string:literal, $null:literal $(, $extra:expr)* $(,)?) => {
18537            &[
18538                ($string, crate::$Lang::StringLiteral as u16),
18539                ("\"a\" \"b\"", crate::$Lang::ConcatenatedString as u16),
18540                ("true", crate::$Lang::True as u16),
18541                ("false", crate::$Lang::False as u16),
18542                ($null, crate::$Lang::Null as u16),
18543                $($extra,)*
18544            ]
18545        };
18546    }
18547
18548    const C_FAMILY_SLOTS: [Slot; 2] = [
18549        ("int f(int a) {\n  return a && {};\n}\n", 2, 3),
18550        ("int f() {\n  if ({}) { return 1; }\n}\n", 1, 3),
18551    ];
18552
18553    fn conditions(lang: LANG, source: &str) -> u64 {
18554        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
18555            .abc
18556            .conditions_sum()
18557    }
18558
18559    fn cyclomatic_sum(lang: LANG, source: &str) -> u64 {
18560        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
18561            .cyclomatic
18562            .cyclomatic_sum()
18563    }
18564
18565    /// Asserts the fixture still spells the construct under test, by
18566    /// parsing it with the language's own parser. Split out of `cases`
18567    /// because only the parser types are feature-gated; the `kind_id`
18568    /// enums are compiled unconditionally.
18569    fn assert_spells(lang: LANG, source: &str, kinds: &[(u16, usize, &str)]) {
18570        match lang {
18571            #[cfg(feature = "javascript")]
18572            LANG::Javascript => {
18573                assert_fixture_spells::<crate::JavascriptParser>(source, "f.js", kinds);
18574            }
18575            #[cfg(feature = "mozjs")]
18576            LANG::Mozjs => assert_fixture_spells::<crate::MozjsParser>(source, "f.jsm", kinds),
18577            #[cfg(feature = "typescript")]
18578            LANG::Typescript => {
18579                assert_fixture_spells::<crate::TypescriptParser>(source, "f.ts", kinds);
18580            }
18581            #[cfg(feature = "typescript")]
18582            LANG::Tsx => assert_fixture_spells::<crate::TsxParser>(source, "f.tsx", kinds),
18583            #[cfg(feature = "python")]
18584            LANG::Python => assert_fixture_spells::<crate::PythonParser>(source, "f.py", kinds),
18585            #[cfg(feature = "lua")]
18586            LANG::Lua => assert_fixture_spells::<crate::LuaParser>(source, "f.lua", kinds),
18587            #[cfg(feature = "php")]
18588            LANG::Php => assert_fixture_spells::<crate::PhpParser>(source, "f.php", kinds),
18589            #[cfg(feature = "groovy")]
18590            LANG::Groovy => {
18591                assert_fixture_spells::<crate::GroovyParser>(source, "f.groovy", kinds);
18592            }
18593            #[cfg(feature = "perl")]
18594            LANG::Perl => assert_fixture_spells::<crate::PerlParser>(source, "f.pl", kinds),
18595            #[cfg(feature = "ruby")]
18596            LANG::Ruby => assert_fixture_spells::<crate::RubyParser>(source, "f.rb", kinds),
18597            #[cfg(feature = "elixir")]
18598            LANG::Elixir => assert_fixture_spells::<crate::ElixirParser>(source, "f.ex", kinds),
18599            #[cfg(feature = "c")]
18600            LANG::C => assert_fixture_spells::<crate::CParser>(source, "f.c", kinds),
18601            #[cfg(feature = "cpp")]
18602            LANG::Cpp => assert_fixture_spells::<crate::CppParser>(source, "f.cpp", kinds),
18603            #[cfg(feature = "mozcpp")]
18604            LANG::Mozcpp => assert_fixture_spells::<crate::MozcppParser>(source, "f.cpp", kinds),
18605            #[cfg(feature = "objc")]
18606            LANG::Objc => assert_fixture_spells::<crate::ObjcParser>(source, "f.m", kinds),
18607            other => panic!("{other:?} has a case row but no parser arm"),
18608        }
18609    }
18610
18611    /// `([chain_slot, condition_slot], identifier, literals)` per
18612    /// language, one spelling per literal *kind* the grammar emits.
18613    ///
18614    /// - JS family: `string`, `template_string`, `regex`, `null`,
18615    ///   `undefined`, `object`, `array`. TypeScript's `string` / `object`
18616    ///   type keywords (`a: string`) are different ids rendering to the
18617    ///   same names and are deliberately not in the sets, so a row that
18618    ///   accidentally named one would fail the id anchor here.
18619    /// - Python: `string` covers `'s'`, `"""s"""`, `f'x'` and `b'x'`,
18620    ///   while the implicit-join `'a' 'b'` is the separate
18621    ///   `concatenated_string` rule — the sibling-rule-under-a-supertype
18622    ///   shape #1379's Perl numerals were, which an alias sweep cannot
18623    ///   see. Then `none`, the four collection displays, and `ellipsis`.
18624    /// - Lua: one `string` kind for `"s"`, `'s'` and `[[s]]`, plus
18625    ///   `table_constructor`. Lua's is the strongest case of the eight:
18626    ///   everything but `false` and `nil` is truthy, which is why
18627    ///   `cond and "a" or "b"` is the language's ternary.
18628    /// - PHP: `string` (single-quoted) and `encapsed_string`
18629    ///   (interpolating) are separate rules, as are `heredoc` and
18630    ///   `nowdoc`; then `array_creation_expression`, `null`, the
18631    ///   backtick `shell_command_expression`, and `cast_expression` —
18632    ///   the second finding of #1462, PHP being the only set in the
18633    ///   Java / C# / Groovy / PHP group that named no cast kind.
18634    /// - Groovy: `string_literal` (which also covers the slashy `/re/`),
18635    ///   `null_literal`, `list_literal`, `map_literal`.
18636    /// - Perl: the four string productions the grammar keeps separate
18637    ///   (`'s'`, `q()`, `"s"`, `qq()`), the two command substitutions
18638    ///   (`qx()`, backticks), the `qw()` / `[…]` / `{…}` collection
18639    ///   literals, `qr//`, and `special_literal` (`__FILE__`).
18640    ///   `heredoc_initializer` is the twelfth kind and cannot live in
18641    ///   this table — a Perl heredoc body follows the *statement*, not
18642    ///   the operand — so it has its own test below.
18643    /// - Ruby: `string` (covering `"s"`, `'s'`, `%q()`, `%Q()`),
18644    ///   `chained_string` (the adjacent-literal join `"a" "b"`, a
18645    ///   sibling rule an alias sweep cannot see), `subshell`, the four
18646    ///   collection literals `array` / `hash` / `%w[]` / `%i[]`,
18647    ///   `regex`, the one-character `?a`, and both symbol productions.
18648    ///   `heredoc_beginning` is the twelfth and shares the Perl heredoc
18649    ///   test for the same reason.
18650    /// - Elixir: `string` (one kind for `"s"` and the `"""` heredoc),
18651    ///   `charlist`, `sigil` (one kind for `~r//`, `~s()`, `~w()`),
18652    ///   `quoted_atom` — a separate production from `atom`, so
18653    ///   `:"q a"` scored zero while `:atom` scored one — and the four
18654    ///   collection literals `list` / `tuple` / `map` / `bitstring`.
18655    /// - C family: `string_literal` (one kind for `"s"`, `L"s"`, `u8"s"`
18656    ///   and Objective-C's `@"s"`), `concatenated_string`, `true`,
18657    ///   `false` and `null` (`NULL`, and C++'s `nullptr`, which the
18658    ///   grammar wraps in a `null` node). C++ adds `raw_string_literal`
18659    ///   and `user_defined_literal` (`"s"_x`, and `1.0_km` around a
18660    ///   number); Objective-C adds its boxed forms — `array_literal`,
18661    ///   `dictionary_literal`, `at_expression` (`@1`, `@YES`, `@(b)`,
18662    ///   `@protocol(P)`), `selector_expression` and `encode_expression`.
18663    ///   `char_literal` is numeric and pinned in the sibling module.
18664    ///   Objective-C's `nil`, `YES` and `NO` parse as plain identifiers,
18665    ///   so they need no row of their own.
18666    ///
18667    /// Shapes that measured short and are not rows here, none of them a
18668    /// literal: JavaScript's `this` and Groovy's
18669    /// `object_creation_expression` (recorded in #1462); Perl's
18670    /// `s///` and `tr///` (operations on `$_` evaluating to a count),
18671    /// `anonymous_function` and `array_dereference`; Ruby's `lambda`;
18672    /// and Elixir's `anonymous_function` and `&f/1` capture. Every one
18673    /// pays its slot since #1526 (Elixir's since #1527), and
18674    /// `slot_pays_tests` pins them against an identifier control (#1475).
18675    fn cases(lang: LANG) -> Option<Case> {
18676        Some(match lang {
18677            LANG::Javascript => (JS_SLOTS, "b", js_literals!(Javascript, String2), 7),
18678            LANG::Mozjs => (JS_SLOTS, "b", js_literals!(Mozjs, String2), 7),
18679            LANG::Typescript => (JS_SLOTS, "b", js_literals!(Typescript, String), 7),
18680            LANG::Tsx => (JS_SLOTS, "b", js_literals!(Tsx, String2), 7),
18681            LANG::Python => (
18682                [
18683                    ("def f(a):\n    return a and {}\n", 2, 3),
18684                    ("def f():\n    if {}:\n        return 1\n", 1, 3),
18685                ],
18686                "b",
18687                &[
18688                    ("'s'", crate::Python::String as u16),
18689                    ("'a' 'b'", crate::Python::ConcatenatedString as u16),
18690                    ("None", crate::Python::None as u16),
18691                    ("[]", crate::Python::List as u16),
18692                    ("{1}", crate::Python::Set as u16),
18693                    ("()", crate::Python::Tuple as u16),
18694                    ("{}", crate::Python::Dictionary as u16),
18695                    ("...", crate::Python::Ellipsis as u16),
18696                ],
18697                8,
18698            ),
18699            LANG::Lua => (
18700                [
18701                    ("function f(a)\n  return a and {}\nend\n", 2, 3),
18702                    ("function f()\n  if {} then return 1 end\nend\n", 1, 3),
18703                ],
18704                "b",
18705                &[
18706                    ("\"s\"", crate::Lua::String as u16),
18707                    ("{}", crate::Lua::TableConstructor as u16),
18708                ],
18709                2,
18710            ),
18711            LANG::Php => (
18712                [
18713                    ("<?php\nfunction f($a) {\n  return $a && {};\n}\n", 2, 3),
18714                    ("<?php\nfunction f() {\n  if ({}) { return 1; }\n}\n", 1, 3),
18715                ],
18716                "$b",
18717                &[
18718                    ("'s'", crate::Php::String as u16),
18719                    ("\"x$b\"", crate::Php::EncapsedString as u16),
18720                    ("<<<EOT\ns\nEOT", crate::Php::Heredoc as u16),
18721                    ("<<<'EOT'\ns\nEOT", crate::Php::Nowdoc as u16),
18722                    ("[]", crate::Php::ArrayCreationExpression as u16),
18723                    ("null", crate::Php::Null as u16),
18724                    ("`ls`", crate::Php::ShellCommandExpression as u16),
18725                    ("(bool)$b", crate::Php::CastExpression as u16),
18726                ],
18727                8,
18728            ),
18729            LANG::Groovy => (
18730                [
18731                    ("def f(a) {\n  return a && {}\n}\n", 2, 3),
18732                    ("def f() {\n  if ({}) { return 1 }\n}\n", 1, 3),
18733                ],
18734                "b",
18735                &[
18736                    ("\"s\"", crate::Groovy::StringLiteral as u16),
18737                    ("null", crate::Groovy::NullLiteral as u16),
18738                    ("[]", crate::Groovy::ListLiteral as u16),
18739                    ("[:]", crate::Groovy::MapLiteral as u16),
18740                ],
18741                4,
18742            ),
18743            LANG::Perl => (
18744                [
18745                    (
18746                        "sub f {\n  my ($a, $b) = @_;\n  if ($a && {}) { print 1; }\n}\n",
18747                        2,
18748                        4,
18749                    ),
18750                    (
18751                        "sub f {\n  my ($a, $b) = @_;\n  if ({}) { print 1; }\n}\n",
18752                        1,
18753                        3,
18754                    ),
18755                ],
18756                "$b",
18757                &[
18758                    ("'s'", crate::Perl::StringSingleQuoted as u16),
18759                    ("q(s)", crate::Perl::StringQQuoted as u16),
18760                    ("\"s\"", crate::Perl::StringDoubleQuoted as u16),
18761                    ("qq(s)", crate::Perl::StringQqQuoted as u16),
18762                    ("qx(ls)", crate::Perl::CommandQxQuoted as u16),
18763                    ("`ls`", crate::Perl::BacktickQuoted as u16),
18764                    ("qw(a b)", crate::Perl::WordListQw as u16),
18765                    ("[1, 2]", crate::Perl::ArrayRef as u16),
18766                    ("{ a => 1 }", crate::Perl::HashRef as u16),
18767                    ("qr/re/", crate::Perl::RegexPatternQr as u16),
18768                    ("__FILE__", crate::Perl::SpecialLiteral as u16),
18769                ],
18770                11,
18771            ),
18772            LANG::Ruby => (
18773                [
18774                    ("def f(a, b)\n  if a && {} then 1 else 0 end\nend\n", 3, 4),
18775                    ("def f(a, b)\n  if {} then 1 else 0 end\nend\n", 2, 3),
18776                ],
18777                "b",
18778                &[
18779                    ("\"s\"", crate::Ruby::String as u16),
18780                    ("\"a\" \"b\"", crate::Ruby::ChainedString as u16),
18781                    ("`ls`", crate::Ruby::Subshell as u16),
18782                    ("[1, 2]", crate::Ruby::Array as u16),
18783                    ("{ a: 1 }", crate::Ruby::Hash as u16),
18784                    ("%w[a b]", crate::Ruby::StringArray as u16),
18785                    ("%i[a b]", crate::Ruby::SymbolArray as u16),
18786                    ("/re/", crate::Ruby::Regex as u16),
18787                    ("?a", crate::Ruby::Character as u16),
18788                    (":sym", crate::Ruby::SimpleSymbol as u16),
18789                    (":\"sym\"", crate::Ruby::DelimitedSymbol as u16),
18790                ],
18791                11,
18792            ),
18793            LANG::Elixir => (
18794                [
18795                    (
18796                        "defmodule M do\n  def f(a, b) do\n    if a && {} do\n      a\n    end\n  end\nend\n",
18797                        2,
18798                        5,
18799                    ),
18800                    (
18801                        "defmodule M do\n  def f(a, b) do\n    if {} do\n      a\n    end\n  end\nend\n",
18802                        1,
18803                        4,
18804                    ),
18805                ],
18806                "b",
18807                &[
18808                    ("\"s\"", crate::Elixir::String as u16),
18809                    ("'c'", crate::Elixir::Charlist as u16),
18810                    ("~r/re/", crate::Elixir::Sigil as u16),
18811                    (":\"q a\"", crate::Elixir::QuotedAtom as u16),
18812                    ("[1, 2]", crate::Elixir::List as u16),
18813                    ("{1, 2}", crate::Elixir::Tuple as u16),
18814                    ("%{a: 1}", crate::Elixir::Map as u16),
18815                    ("<<1>>", crate::Elixir::Bitstring as u16),
18816                ],
18817                8,
18818            ),
18819            LANG::C => (
18820                C_FAMILY_SLOTS,
18821                "b",
18822                c_family_literals!(C, "\"s\"", "NULL"),
18823                5,
18824            ),
18825            LANG::Cpp => (
18826                C_FAMILY_SLOTS,
18827                "b",
18828                c_family_literals!(
18829                    Cpp,
18830                    "\"s\"",
18831                    "nullptr",
18832                    ("R\"(x)\"", crate::Cpp::RawStringLiteral as u16),
18833                    ("\"s\"_x", crate::Cpp::UserDefinedLiteral as u16),
18834                ),
18835                7,
18836            ),
18837            LANG::Mozcpp => (
18838                C_FAMILY_SLOTS,
18839                "b",
18840                c_family_literals!(
18841                    Mozcpp,
18842                    "\"s\"",
18843                    "nullptr",
18844                    ("R\"(x)\"", crate::Mozcpp::RawStringLiteral as u16),
18845                    ("\"s\"_x", crate::Mozcpp::UserDefinedLiteral as u16),
18846                ),
18847                7,
18848            ),
18849            LANG::Objc => (
18850                C_FAMILY_SLOTS,
18851                "b",
18852                c_family_literals!(
18853                    Objc,
18854                    "@\"s\"",
18855                    "NULL",
18856                    ("@[]", crate::Objc::ArrayLiteral as u16),
18857                    ("@{}", crate::Objc::DictionaryLiteral as u16),
18858                    ("@1", crate::Objc::AtExpression as u16),
18859                    ("@selector(x)", crate::Objc::SelectorExpression as u16),
18860                    ("@encode(int)", crate::Objc::EncodeExpression as u16),
18861                ),
18862                10,
18863            ),
18864            _ => return None,
18865        })
18866    }
18867
18868    /// Runs `check` once per enabled language that has a case, having
18869    /// first established that the case can still assert something. The
18870    /// four guards are the sibling module's, for the same four ways
18871    /// this table could decay into asserting nothing: no language
18872    /// enabled, an emptied literal list, a list that no longer covers
18873    /// every kind, and a template that lost its `{}` slot (which makes
18874    /// every comparison `x == x`).
18875    fn for_each_case(check: impl Fn(LANG, Case)) {
18876        let mut checked = 0;
18877        for lang in LANG::into_enum_iter() {
18878            if !lang.is_enabled() {
18879                continue;
18880            }
18881            let Some(case @ (slots, _, literals, expected_kinds)) = cases(lang) else {
18882                continue;
18883            };
18884            // The length check does not imply this one: it asserts only
18885            // that the list and its recorded count *agree*, which an
18886            // emptied row with `expected_kinds` set to 0 satisfies —
18887            // the shape a "the test failed, fix the number" edit takes.
18888            // `checked` counts languages, so that row still increments
18889            // it and the non-vacuity guard below reads satisfied.
18890            assert!(
18891                !literals.is_empty(),
18892                "{lang:?}: the literal-operand list is empty; this language asserted nothing"
18893            );
18894            assert_eq!(
18895                literals.len(),
18896                expected_kinds,
18897                "{lang:?}: the literal-operand list no longer covers one spelling \
18898                 per grammar literal kind"
18899            );
18900            for (template, _, _) in slots {
18901                assert!(
18902                    template.contains("{}"),
18903                    "{lang:?}: template lost its `{{}}` operand slot: {template}"
18904                );
18905            }
18906            check(lang, case);
18907            checked += 1;
18908        }
18909        assert!(
18910            checked > 0,
18911            "no truthy-valued language enabled; this test asserted nothing"
18912        );
18913    }
18914
18915    #[test]
18916    fn a_literal_operand_scores_like_an_identifier_operand() {
18917        for_each_case(|lang, (slots, identifier, literals, _)| {
18918            for (template, _, _) in slots {
18919                let baseline = conditions(lang, &template.replace("{}", identifier));
18920                for (literal, _) in literals {
18921                    let source = template.replace("{}", literal);
18922                    let scored = conditions(lang, &source);
18923                    assert_eq!(
18924                        scored, baseline,
18925                        "{lang:?}: `{literal}` scored {scored} unary conditions against \
18926                         `{identifier}`'s {baseline}\n  source: {source}"
18927                    );
18928                }
18929            }
18930        });
18931    }
18932
18933    /// The absolute anchor under the comparison above: every operand
18934    /// spelling must produce the slot's recorded `conditions`, and must
18935    /// leave `cyclomatic` alone.
18936    #[test]
18937    fn every_literal_operand_scores_its_recorded_values() {
18938        for_each_case(|lang, (slots, identifier, literals, _)| {
18939            for (template, expected_conditions, expected_cyclomatic) in slots {
18940                let operands = std::iter::once(identifier)
18941                    .chain(literals.iter().map(|(spelling, _)| *spelling));
18942                for operand in operands {
18943                    let source = template.replace("{}", operand);
18944                    assert_eq!(
18945                        conditions(lang, &source),
18946                        expected_conditions,
18947                        "{lang:?}: `{operand}` conditions\n  source: {source}"
18948                    );
18949                    assert_eq!(
18950                        cyclomatic_sum(lang, &source),
18951                        expected_cyclomatic,
18952                        "{lang:?}: `{operand}` cyclomatic_sum\n  source: {source}"
18953                    );
18954                }
18955            }
18956        });
18957    }
18958
18959    /// The twelfth Perl and Ruby literal kind, which the table above
18960    /// cannot hold: in both languages a heredoc body follows the
18961    /// *statement*, so the operand slot carries only the introducer and
18962    /// the rest of the literal lands on later lines. Substituting one
18963    /// into a single-line template produces an unterminated heredoc and
18964    /// an `ERROR` parse, not a measurement.
18965    ///
18966    /// That shape is also why exactly one kind is listed per language
18967    /// (§5): `heredoc_initializer` / `heredoc_beginning` is the node in
18968    /// the operand slot, while `heredoc_body_statement` /
18969    /// `heredoc_body` is a sibling the walker never reaches. Listing
18970    /// both would score one literal twice.
18971    #[cfg(any(feature = "perl", feature = "ruby"))]
18972    #[test]
18973    fn a_heredoc_operand_scores_like_an_identifier_operand() {
18974        #[cfg(feature = "perl")]
18975        {
18976            let heredoc = "sub f {\n  my ($a, $b) = @_;\n  if ($a && <<\"EOT\") { print 1; }\nhello\nEOT\n}\n";
18977            assert_spells(
18978                LANG::Perl,
18979                heredoc,
18980                &[(crate::Perl::HeredocInitializer as u16, 1, "<<\"EOT\"")],
18981            );
18982            assert_eq!(
18983                conditions(LANG::Perl, heredoc),
18984                conditions(
18985                    LANG::Perl,
18986                    "sub f {\n  my ($a, $b) = @_;\n  if ($a && $b) { print 1; }\n}\n"
18987                ),
18988                "Perl heredoc operand"
18989            );
18990        }
18991        #[cfg(feature = "ruby")]
18992        {
18993            let heredoc =
18994                "def f(a, b)\n  if a && <<~TXT then 1 else 0 end\n    hello\n  TXT\nend\n";
18995            assert_spells(
18996                LANG::Ruby,
18997                heredoc,
18998                &[(crate::Ruby::HeredocBeginning as u16, 1, "<<~TXT")],
18999            );
19000            assert_eq!(
19001                conditions(LANG::Ruby, heredoc),
19002                conditions(
19003                    LANG::Ruby,
19004                    "def f(a, b)\n  if a && b then 1 else 0 end\nend\n"
19005                ),
19006                "Ruby heredoc operand"
19007            );
19008        }
19009    }
19010
19011    /// Every spelling must parse to the `kind_id` its row names — the
19012    /// §1 alias claim, which is the one thing a conditions comparison
19013    /// cannot check: a row naming the wrong alias of a multi-id kind
19014    /// would simply keep reporting the unfixed number.
19015    #[test]
19016    fn every_literal_spelling_parses_to_the_kind_its_row_names() {
19017        for_each_case(|lang, (slots, _, literals, _)| {
19018            for (template, _, _) in slots {
19019                for (literal, kind) in literals {
19020                    let source = template.replace("{}", literal);
19021                    assert_spells(lang, &source, &[(*kind, 1, literal)]);
19022                }
19023            }
19024        });
19025    }
19026}
19027
19028/// The condition slot and the wrapper peel agree on which kinds are
19029/// wrappers, in each language whose slot used to restate the list
19030/// (#1470).
19031///
19032/// `<lang>_count_condition` decided which slot expressions to hand the
19033/// peel, and the former `<lang>_inspect_container` which it could descend.
19034/// Spelled as two lists they drifted three times — Kotlin (#1459),
19035/// Groovy (#1466) and C# (#1463) — each time scoring a valid predicate
19036/// zero. The six languages here now ask `<lang>_wrapper_operand`
19037/// instead, which removed the duplication without moving a number: the
19038/// wrapper matrix measured identical before and after, so a revert
19039/// cannot fail these tests. What they pin is the *answer* both halves
19040/// must keep giving:
19041///
19042/// - **Routed.** Every wrapper the peel accepts scores its slot once.
19043///   A slot that stops asking the peel scores these zero.
19044/// - **Declined.** A unary the peel does not accept (`-b`, or a valid
19045///   non-wrapper such as `b & b`) stops the peel: a slot pays for it
19046///   once (#1526; Go's terminal-kind slot still scores `-b` nothing),
19047///   and outside a slot it scores nothing, so a peel that starts
19048///   accepting arithmetic operators by accident is caught here rather
19049///   than in a corpus snapshot.
19050/// - **Proves boolean.** A negation counts its operand outside any
19051///   boolean slot and a parenthesis does not — the second half of the
19052///   peel's answer, which the slot never sees.
19053///
19054/// Each template's `@` is the operand under test.
19055#[cfg(test)]
19056#[cfg(any(
19057    feature = "rust",
19058    feature = "go",
19059    feature = "java",
19060    feature = "python",
19061    feature = "ruby",
19062    feature = "lua"
19063))]
19064mod wrapper_peel_routing {
19065    use crate::test_support::metrics_verbatim;
19066    use crate::{LANG, MetricsOptions};
19067
19068    fn conditions(lang: LANG, source: &str) -> u64 {
19069        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
19070            .abc
19071            .conditions_sum()
19072    }
19073
19074    /// Asserts `conditions` for every `(operand, expected)` row of one
19075    /// template.
19076    fn assert_operands(lang: LANG, template: &str, rows: &[(&str, u64)]) {
19077        assert!(template.contains('@'), "template has no operand slot");
19078        for &(operand, expected) in rows {
19079            let source = template.replace('@', operand);
19080            assert_eq!(
19081                conditions(lang, &source),
19082                expected,
19083                "`{operand}` in {lang:?}\n  source: {source}"
19084            );
19085        }
19086    }
19087
19088    /// Rust's slot pays for any predicate no other arm counted (#1526),
19089    /// so the declined half reads the valid non-wrapper `b & b`: the
19090    /// peel stops on it, the slot pays for it, and the call argument
19091    /// below does not.
19092    #[test]
19093    #[cfg(feature = "rust")]
19094    fn rust_condition_slot_routes_what_the_peel_accepts() {
19095        assert_operands(
19096            LANG::Rust,
19097            "fn f(b: bool) { if @ {} }\n",
19098            &[
19099                ("b", 1),
19100                ("(b)", 1),
19101                ("!b", 1),
19102                ("!(b)", 1),
19103                ("b & b", 1),
19104                ("(b & b)", 1),
19105                ("!(b & b)", 1),
19106            ],
19107        );
19108        assert_operands(
19109            LANG::Rust,
19110            "fn f(b: bool) { g(@); }\n",
19111            &[("!b", 1), ("(b)", 0), ("b & b", 0), ("!(b & b)", 1)],
19112        );
19113    }
19114
19115    #[test]
19116    #[cfg(feature = "go")]
19117    fn go_condition_slot_routes_what_the_peel_accepts() {
19118        assert_operands(
19119            LANG::Go,
19120            "package p\nfunc f(b bool) { if @ {} }\n",
19121            &[
19122                ("b", 1),
19123                ("(b)", 1),
19124                ("!b", 1),
19125                ("!(b)", 1),
19126                ("-b", 0),
19127                ("(-b)", 0),
19128            ],
19129        );
19130        assert_operands(
19131            LANG::Go,
19132            "package p\nfunc f(b bool) { g(@) }\n",
19133            &[("!b", 1), ("(b)", 0), ("-b", 0)],
19134        );
19135    }
19136
19137    /// The ternary condition is a slot like the `if` one; the `?` itself
19138    /// is the base 1. Java's slot pays for any predicate no other arm
19139    /// counted (#1526), so the declined half reads the valid non-wrapper
19140    /// `b & b`: the peel stops on it, the slot pays for it, and the call
19141    /// argument below does not.
19142    #[test]
19143    #[cfg(feature = "java")]
19144    fn java_condition_slot_routes_what_the_peel_accepts() {
19145        assert_operands(
19146            LANG::Java,
19147            "class A { int f(boolean b) { return @ ? 1 : 2; } }\n",
19148            &[
19149                ("b", 2),
19150                ("(b)", 2),
19151                ("!b", 2),
19152                ("!(b)", 2),
19153                ("b & b", 2),
19154                ("(b & b)", 2),
19155            ],
19156        );
19157        assert_operands(
19158            LANG::Java,
19159            "class A { void f(boolean b) { g(@); } }\n",
19160            &[("!b", 1), ("(b)", 0), ("b & b", 0), ("!(b & b)", 1)],
19161        );
19162    }
19163
19164    /// Python peels parentheses and the walrus only: `not` is counted by
19165    /// its own `NotOperator` arm, so `python_wrapper_operand` carries no
19166    /// proves-boolean flag and there is no outside-a-slot half to pin.
19167    /// Python is truthy-valued, so the slot pays for any predicate no
19168    /// other arm counted, `-b` included (#1526), as Ruby's does.
19169    #[test]
19170    #[cfg(feature = "python")]
19171    fn python_condition_slot_routes_what_the_peel_accepts() {
19172        assert_operands(
19173            LANG::Python,
19174            "def f(b):\n    if @:\n        pass\n",
19175            &[("b", 1), ("(b)", 1), ("((b))", 1), ("-b", 1), ("(-b)", 1)],
19176        );
19177    }
19178
19179    /// Ruby has no counted call-argument slot, so the outside-a-slot
19180    /// half reads a ternary *branch*, which is type-free: only a
19181    /// negation there proves its operand boolean. That branch is also
19182    /// where the declined half lives for Ruby: its condition slot pays
19183    /// for any predicate no other arm counted, `-b` included (#1520), so
19184    /// `if -b` scores 1 rather than 0.
19185    #[test]
19186    #[cfg(feature = "ruby")]
19187    fn ruby_condition_slot_routes_what_the_peel_accepts() {
19188        assert_operands(
19189            LANG::Ruby,
19190            "def f(b)\n  if @ then 1 end\nend\n",
19191            &[
19192                ("b", 1),
19193                ("(b)", 1),
19194                ("!b", 1),
19195                ("not b", 1),
19196                ("!(b)", 1),
19197                ("-b", 1),
19198                ("(-b)", 1),
19199            ],
19200        );
19201        assert_operands(
19202            LANG::Ruby,
19203            "def f(b, c)\n  x = c ? 1 : @\nend\n",
19204            &[
19205                ("!b", 3),
19206                ("not b", 3),
19207                ("(b)", 2),
19208                ("-b", 2),
19209                // A negated branch is a slot whatever it negates (#1529).
19210                ("!self", 3),
19211                ("not -b", 3),
19212                ("!(b > 1)", 3),
19213            ],
19214        );
19215        // A statement sequence evaluates to its last statement, so that is
19216        // the operand the slot reads: `(b; b > 1)` is the comparison,
19217        // already counted by its own `>`, and `(b > 1; b)` is the bare `b`,
19218        // which the slot counts beside that `>`. Reading the first
19219        // statement inverted both. The subject-less `when` goes through
19220        // the same peel.
19221        let seqs: &[(&str, u64)] = &[("(b; b > 1)", 1), ("(b > 1; b)", 2)];
19222        assert_operands(LANG::Ruby, "def f(b)\n  if @ then 1 end\nend\n", seqs);
19223        assert_operands(
19224            LANG::Ruby,
19225            "def f(b)\n  case\n  when @ then 1\n  end\nend\n",
19226            seqs,
19227        );
19228    }
19229
19230    /// A subject-less `when` whose pattern is a chain adds nothing of its
19231    /// own: the Rule 9 walker scores every operand as a slot, so each
19232    /// chain already pays at least one (#1529). Before that the walker
19233    /// skipped an operand outside its terminal list, the clause had to
19234    /// pay for `-a && -b` (review of #1453), and a chain scored as many
19235    /// conditions as it had *listed* operands plus one at most. Each row
19236    /// now scores what its plain-identifier twin (`a && b`, `a || b ||
19237    /// c`) does. An assignment operand is its value (#1520), so
19238    /// `(a = 1) && (b = 2)` is `1 && 2`.
19239    #[test]
19240    #[cfg(feature = "ruby")]
19241    fn ruby_subjectless_when_chain_pays_per_operand() {
19242        assert_operands(
19243            LANG::Ruby,
19244            "def f(a, b, c)\n  case\n  when @ then 1\n  end\nend\n",
19245            &[
19246                ("a", 1),
19247                ("-a", 1),
19248                ("a && b", 2),
19249                ("-a && -b", 2),
19250                ("(a = 1) && (b = 2)", 2),
19251                ("a || b || c", 3),
19252                ("-a || -b || -c", 3),
19253                ("-a && b", 2),
19254                ("a > 1 && b", 2),
19255                ("a > 1 && -b", 2),
19256                ("a && (b || c)", 3),
19257                ("-a && (-b || c)", 3),
19258            ],
19259        );
19260    }
19261
19262    /// #1529: an operand of a Ruby `&&` / `||` / `and` / `or` chain is
19263    /// scored by the rule #1520 gave every condition slot — one, unless
19264    /// another arm already counts it — wherever the chain is written.
19265    /// The walker used to pay only for an operand that peeled down to a
19266    /// fixed list of terminal kinds, so every spelling in `UNLISTED`
19267    /// scored one below its plain-identifier twin `b` in every position
19268    /// below. `CONTROLS` must not move: `!b` and `(b)` already scored
19269    /// their twin, and a comparison, a pattern test, or a negation of one
19270    /// is counted by its own arm, so paying the operand as well would
19271    /// count that decision twice.
19272    #[test]
19273    #[cfg(feature = "ruby")]
19274    fn ruby_chain_operand_pays_what_no_other_arm_counted() {
19275        const UNLISTED: &[&str] = &[
19276            "self",
19277            "Foo::Bar",
19278            "-b",
19279            "(-b)",
19280            "defined?(b)",
19281            "b + 1",
19282            "!self",
19283            "not self",
19284            "(y ||= b)",
19285            "(y = self)",
19286            "begin self end",
19287        ];
19288        const CONTROLS: &[&str] = &["!b", "(b)", "c > 1", "!(c > 1)", "(c in Integer)"];
19289        // (template, conditions with `b` in the `@` operand)
19290        let positions: &[(&str, u64)] = &[
19291            ("def f(a, b, c)\n  g if a && @\nend\n", 2),
19292            ("def f(a, b, c)\n  g if (a && @)\nend\n", 2),
19293            ("def f(a, b, c)\n  x = a && @\nend\n", 2),
19294            ("def f(a, b, c)\n  return a || @\nend\n", 2),
19295            ("def f(a, b, c)\n  h(@ && a)\nend\n", 2),
19296            ("def f(a, b, c)\n  x = (a and @)\nend\n", 2),
19297            ("def f(a, b, c)\n  x = (a or @)\nend\n", 2),
19298            ("def f(a, b, c)\n  y ||= a && @\nend\n", 2),
19299            ("def f(a, b, c)\n  y &&= a || @\nend\n", 2),
19300            ("def f(a, b, c)\n  x = a && (c || @)\nend\n", 3),
19301            // A comment is a named child of the `binary` too, and must not
19302            // pay as a third operand.
19303            ("def f(a, b, c)\n  x = a && # c\n    @\nend\n", 2),
19304            ("def f(a, b, c)\n  while a && @ do g end\nend\n", 2),
19305            (
19306                "def f(a, b, c)\n  if a then 1 elsif a && @ then 2 end\nend\n",
19307                4,
19308            ),
19309            (
19310                "def f(a, b, c)\n  case\n  when a && @ then 1\n  end\nend\n",
19311                2,
19312            ),
19313        ];
19314        for &(template, twin) in positions {
19315            // The twin is measured, not only assumed, so a template that
19316            // stopped parsing as a chain fails here rather than agreeing
19317            // with every row at some other value.
19318            assert_operands(LANG::Ruby, template, &[("b", twin)]);
19319            let rows: Vec<_> = UNLISTED
19320                .iter()
19321                .chain(CONTROLS)
19322                .map(|&operand| (operand, twin))
19323                .collect();
19324            assert_operands(LANG::Ruby, template, &rows);
19325        }
19326    }
19327
19328    #[test]
19329    #[cfg(feature = "lua")]
19330    fn lua_condition_slot_routes_what_the_peel_accepts() {
19331        // Lua is truthy-valued, so the slot pays for any predicate no
19332        // other arm counted, `-b` and `#b` included (#1526).
19333        assert_operands(
19334            LANG::Lua,
19335            "function f(b)\n  if @ then end\nend\n",
19336            &[
19337                ("b", 1),
19338                ("(b)", 1),
19339                ("not b", 1),
19340                ("not (b)", 1),
19341                ("-b", 1),
19342                ("#b", 1),
19343                ("(-b)", 1),
19344            ],
19345        );
19346        assert_operands(
19347            LANG::Lua,
19348            "function f(b)\n  g(@)\nend\n",
19349            &[("not b", 1), ("(b)", 0), ("-b", 0)],
19350        );
19351    }
19352}
19353
19354/// A comment inside a condition or value slot changes nothing (#1455).
19355///
19356/// Every walker read its slots by fixed child index, and every peel
19357/// stepped into a `(…)` / negation with `child(1)` or the first named
19358/// child. tree-sitter counts a comment among a node's children and
19359/// marks it *named*, so `if (/*c*/ b)` handed the comment to the slot,
19360/// which declined it, and the condition scored zero. Each pair below is
19361/// one spelling with and without a comment; the two are asserted equal
19362/// to each other *and* to the expected literal, so a regression that
19363/// moved both sides alike — to zero, say — still fails.
19364///
19365/// The forms, per slot and per comment spelling of the language: a
19366/// comment before the operand, inside a nested parenthesis, before a
19367/// negation, between a negation and its operand, and — where the
19368/// language's syntax puts parentheses around the slot — before those
19369/// parentheses. Value slots (`return`, declarator, assignment, lambda
19370/// body) take the negated forms only, since a bare operand there is no
19371/// condition, and only the first comment spelling: a line comment after
19372/// `return` ends the statement in Go.
19373#[cfg(test)]
19374#[cfg(any(
19375    feature = "c",
19376    feature = "cpp",
19377    feature = "csharp",
19378    feature = "elixir",
19379    feature = "go",
19380    feature = "groovy",
19381    feature = "java",
19382    feature = "javascript",
19383    feature = "kotlin",
19384    feature = "lua",
19385    feature = "mozcpp",
19386    feature = "mozjs",
19387    feature = "objc",
19388    feature = "perl",
19389    feature = "php",
19390    feature = "python",
19391    feature = "ruby",
19392    feature = "rust",
19393    feature = "typescript"
19394))]
19395mod slot_comment_invariance {
19396    use crate::test_support::metrics_verbatim;
19397    use crate::{LANG, MetricsOptions};
19398
19399    fn conditions(lang: LANG, source: &str) -> u64 {
19400        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
19401            .abc
19402            .conditions_sum()
19403    }
19404
19405    /// How one language spells the slots under test.
19406    struct Spelling {
19407        /// Comment spellings, each carrying the whitespace — a newline
19408        /// for a line comment — the operand after it needs.
19409        comments: &'static [&'static str],
19410        operand: &'static str,
19411        /// `None` where the peel does not descend the negation: Python
19412        /// counts `not` through its own arm, and tree-sitter-python
19413        /// fails to parse the valid `(not # c⏎ b)` at the pinned grammar.
19414        negation: Option<&'static str>,
19415        /// The slot is written inside parentheses: either the syntax
19416        /// requires them (`if (…)`), or a line comment needs them to
19417        /// stay inside the expression (Python, Ruby, Elixir).
19418        parenthesised: bool,
19419        /// The parentheses belong to the slot's syntax, so a comment
19420        /// may also sit before them (`if /*c*/ (b)`).
19421        syntactic_parens: bool,
19422        /// `(template, expected)`; `@` marks a condition slot.
19423        conditions: &'static [(&'static str, u64)],
19424        /// Templates whose `@` is a value slot; a negation there scores 1.
19425        values: &'static [&'static str],
19426    }
19427
19428    const C_COMMENTS: &[&str] = &["/*c*/ ", "//c\n "];
19429
19430    const C_FAMILY: Spelling = Spelling {
19431        comments: C_COMMENTS,
19432        operand: "b",
19433        negation: Some("!"),
19434        parenthesised: true,
19435        syntactic_parens: true,
19436        conditions: &[
19437            ("int f(){ if @ { x(); } }", 1),
19438            ("int f(){ while @ { x(); } }", 1),
19439            ("int f(){ do { x(); } while @; }", 1),
19440        ],
19441        values: &["int f(){ return @; }"],
19442    };
19443
19444    const JS_FAMILY: Spelling = Spelling {
19445        comments: C_COMMENTS,
19446        operand: "b",
19447        negation: Some("!"),
19448        parenthesised: true,
19449        syntactic_parens: true,
19450        conditions: &[
19451            ("function f(){ if @ { x(); } }", 1),
19452            ("function f(){ while @ { x(); } }", 1),
19453            ("function f(){ do { x(); } while @; }", 1),
19454        ],
19455        values: &["function f(){ return @; }"],
19456    };
19457
19458    fn spelling(lang: LANG) -> Option<Spelling> {
19459        Some(match lang {
19460            LANG::C | LANG::Cpp | LANG::Mozcpp | LANG::Objc => C_FAMILY,
19461            LANG::Javascript | LANG::Mozjs | LANG::Typescript | LANG::Tsx => JS_FAMILY,
19462            LANG::Java => Spelling {
19463                conditions: &[
19464                    ("class K{ void f(){ if @ { x(); } } }", 1),
19465                    ("class K{ void f(){ while @ { x(); } } }", 1),
19466                    ("class K{ void f(){ do { x(); } while @; } }", 1),
19467                ],
19468                values: &[
19469                    "class K{ boolean f(){ return @; } }",
19470                    "class K{ void f(){ boolean y = @; } }",
19471                    "class K{ void f(){ y = @; } }",
19472                    "class K{ void f(){ Supplier<Boolean> s = () -> @; } }",
19473                ],
19474                ..C_FAMILY
19475            },
19476            LANG::Csharp => Spelling {
19477                conditions: &[
19478                    ("class K{ void f(){ if @ { x(); } } }", 1),
19479                    ("class K{ void f(){ while @ { x(); } } }", 1),
19480                    ("class K{ void f(){ do { x(); } while @; } }", 1),
19481                ],
19482                values: &[
19483                    "class K{ bool f(){ return @; } }",
19484                    "class K{ void f(){ var y = @; } }",
19485                    "class K{ void f(){ y = @; } }",
19486                    "class K{ void f(){ Func<bool> s = () => @; } }",
19487                ],
19488                ..C_FAMILY
19489            },
19490            LANG::Groovy => Spelling {
19491                conditions: &[
19492                    ("def f(){ if @ { x() } }", 1),
19493                    ("def f(){ while @ { x() } }", 1),
19494                    ("def f(){ do { x() } while @ }", 1),
19495                ],
19496                values: &[
19497                    "def f(){ return @ }",
19498                    "def f(){ def y = @ }",
19499                    "def f(){ y = @ }",
19500                ],
19501                ..C_FAMILY
19502            },
19503            LANG::Php => Spelling {
19504                comments: &["/*c*/ ", "//c\n ", "#c\n "],
19505                operand: "$b",
19506                conditions: &[
19507                    ("<?php function f(){ if @ { x(); } }", 1),
19508                    ("<?php function f(){ while @ { x(); } }", 1),
19509                    ("<?php function f(){ do { x(); } while @; }", 1),
19510                ],
19511                values: &["<?php function f(){ return @; }"],
19512                ..C_FAMILY
19513            },
19514            LANG::Kotlin => Spelling {
19515                conditions: &[
19516                    ("fun f(){ if @ { x() } }", 1),
19517                    ("fun f(){ while @ { x() } }", 1),
19518                    ("fun f(){ do { x() } while @ }", 1),
19519                ],
19520                values: &[],
19521                ..C_FAMILY
19522            },
19523            LANG::Perl => Spelling {
19524                comments: &["# c\n "],
19525                operand: "$b",
19526                conditions: &[
19527                    ("sub f { if @ { x(); } }", 1),
19528                    ("sub f { unless @ { x(); } }", 1),
19529                    ("sub f { while @ { x(); } }", 1),
19530                    ("sub f { until @ { x(); } }", 1),
19531                ],
19532                values: &["sub f { return @; }"],
19533                ..C_FAMILY
19534            },
19535            LANG::Rust => Spelling {
19536                parenthesised: false,
19537                syntactic_parens: false,
19538                conditions: &[
19539                    ("fn f(){ if @ { x(); } }", 1),
19540                    ("fn f(){ while @ { x(); } }", 1),
19541                ],
19542                values: &["fn f() -> bool { return @; }"],
19543                ..C_FAMILY
19544            },
19545            LANG::Go => Spelling {
19546                parenthesised: false,
19547                syntactic_parens: false,
19548                conditions: &[
19549                    ("package p\nfunc f(){ if @ { x() } }", 1),
19550                    ("package p\nfunc f(){ for @ { x() } }", 1),
19551                ],
19552                values: &["package p\nfunc f() bool { return @ }"],
19553                ..C_FAMILY
19554            },
19555            LANG::Lua => Spelling {
19556                comments: &["--[[c]] ", "--c\n "],
19557                negation: Some("not "),
19558                parenthesised: false,
19559                syntactic_parens: false,
19560                conditions: &[
19561                    ("function f() if @ then x() end end", 1),
19562                    ("function f() while @ do x() end end", 1),
19563                    ("function f() repeat x() until @ end", 1),
19564                ],
19565                values: &["function f() return @ end"],
19566                ..C_FAMILY
19567            },
19568            LANG::Python => Spelling {
19569                comments: &["# c\n "],
19570                negation: None,
19571                syntactic_parens: false,
19572                conditions: &[
19573                    ("def f():\n    if @:\n        x()\n", 1),
19574                    ("def f():\n    while @:\n        x()\n", 1),
19575                ],
19576                values: &[],
19577                ..C_FAMILY
19578            },
19579            LANG::Ruby => Spelling {
19580                comments: &["# c\n "],
19581                syntactic_parens: false,
19582                conditions: &[
19583                    ("def f\n  if @\n    x\n  end\nend\n", 1),
19584                    ("def f\n  while @\n    x\n  end\nend\n", 1),
19585                    ("def f\n  case\n  when @\n    x\n  end\nend\n", 1),
19586                ],
19587                values: &[],
19588                ..C_FAMILY
19589            },
19590            // Elixir's `if` slot scored its operand whatever the peel
19591            // returned, so the defect showed only where the peel decides:
19592            // an operand of a `&&` chain, which counts each operand.
19593            LANG::Elixir => Spelling {
19594                comments: &["# c\n "],
19595                syntactic_parens: false,
19596                conditions: &[
19597                    (
19598                        "defmodule M do\n def f do\n  if @ do\n   x()\n  end\n end\nend\n",
19599                        1,
19600                    ),
19601                    ("defmodule M do\n def f do\n  y = a && @\n end\nend\n", 2),
19602                ],
19603                values: &[],
19604                ..C_FAMILY
19605            },
19606            _ => return None,
19607        })
19608    }
19609
19610    /// `(without, with, expected)` for every form of every slot.
19611    fn cases(s: &Spelling) -> Vec<(String, String, u64)> {
19612        let slot = |x: String| {
19613            if s.parenthesised { format!("({x})") } else { x }
19614        };
19615        let b = s.operand;
19616        let mut out = Vec::new();
19617        for &(template, expected) in s.conditions {
19618            let fill = |x: String| template.replace('@', &x);
19619            for c in s.comments {
19620                let mut push = |plain: String, commented: String| {
19621                    out.push((fill(plain), fill(commented), expected));
19622                };
19623                push(slot(b.into()), slot(format!("{c}{b}")));
19624                push(slot(format!("({b})")), slot(format!("({c}{b})")));
19625                if let Some(n) = s.negation {
19626                    push(slot(format!("{n}{b}")), slot(format!("{c}{n}{b}")));
19627                    push(slot(format!("{n}{b}")), slot(format!("{n}{c}{b}")));
19628                }
19629                if s.syntactic_parens {
19630                    push(slot(b.into()), format!("{c}{}", slot(b.into())));
19631                }
19632            }
19633        }
19634        if let (Some(n), Some(c)) = (s.negation, s.comments.first()) {
19635            for template in s.values {
19636                let fill = |x: String| template.replace('@', &x);
19637                out.push((fill(format!("{n}{b}")), fill(format!("{c}{n}{b}")), 1));
19638                out.push((fill(format!("{n}{b}")), fill(format!("{n}{c}{b}")), 1));
19639            }
19640        }
19641        out
19642    }
19643
19644    #[test]
19645    fn a_comment_in_a_slot_changes_nothing() {
19646        let mut checked = 0;
19647        for lang in LANG::into_enum_iter().filter(LANG::is_enabled) {
19648            let Some(spelling) = spelling(lang) else {
19649                continue;
19650            };
19651            for (plain, commented, expected) in cases(&spelling) {
19652                let without = conditions(lang, &plain);
19653                assert_eq!(
19654                    conditions(lang, &commented),
19655                    without,
19656                    "{lang:?}: a comment changed the ABC conditions\n  \
19657                     without: {plain}\n  with:    {commented}"
19658                );
19659                assert_eq!(without, expected, "{lang:?}: {plain}");
19660                checked += 1;
19661            }
19662        }
19663        assert!(
19664            checked > 0,
19665            "no slot language enabled; this test asserted nothing"
19666        );
19667    }
19668}
19669
19670/// Slot occupants that no fixed index or first-named-child read finds
19671/// (#1455): a preamble before the slot, a comment *after* the operand
19672/// of a last-operand read, and the non-condition reads that were
19673/// positional too. Each row pairs a spelling with its plain twin.
19674#[cfg(test)]
19675#[cfg(any(
19676    feature = "cpp",
19677    feature = "elixir",
19678    feature = "mozcpp",
19679    feature = "perl",
19680    feature = "ruby",
19681    feature = "rust"
19682))]
19683mod slot_role_reads {
19684    use crate::test_support::metrics_verbatim;
19685    use crate::{LANG, MetricsOptions};
19686
19687    /// `(conditions, assignments)`.
19688    fn abc(lang: LANG, source: &str) -> (u64, u64) {
19689        let abc = metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default()).abc;
19690        (abc.conditions_sum(), abc.assignments_sum())
19691    }
19692
19693    /// `(language, plain, variant, (conditions, assignments))`.
19694    const ROWS: &[(LANG, &str, &str, (u64, u64))] = &[
19695        // A C++ `condition_clause` holds its condition in the `value`
19696        // field; an init-statement sits at child(1) before it.
19697        (
19698            LANG::Cpp,
19699            "int f(){ int y = g(); if (b) { x(); } }",
19700            "int f(){ if (int y = g(); b) { x(); } }",
19701            (1, 1),
19702        ),
19703        (
19704            LANG::Mozcpp,
19705            "int f(){ int y = g(); if (b) { x(); } }",
19706            "int f(){ if (int y = g(); b) { x(); } }",
19707            (1, 1),
19708        ),
19709        (
19710            LANG::Cpp,
19711            "int f(){ if constexpr (b) { x(); } }",
19712            "int f(){ if constexpr (/*c*/ b) { x(); } }",
19713            (1, 0),
19714        ),
19715        // A loop label is the `while` expression's first child.
19716        (
19717            LANG::Rust,
19718            "fn f(){ while b { x(); } }",
19719            "fn f(){ 'a: while b { x(); } }",
19720            (1, 0),
19721        ),
19722        // Perl's `(…)` evaluates to its last element, so the peel takes
19723        // the last operand — never a trailing comment.
19724        (
19725            LANG::Perl,
19726            "sub f { if ($b) { x(); } }",
19727            "sub f { if ($b # c\n) { x(); } }",
19728            (1, 0),
19729        ),
19730        (
19731            LANG::Perl,
19732            "sub f { x() if $b; }",
19733            "sub f { x() if $b # c\n; }",
19734            (1, 0),
19735        ),
19736        // `split`'s delimiter is its first *operand*; a comment before it
19737        // made the pattern read as an ordinary match.
19738        (
19739            LANG::Perl,
19740            "sub f { my @a = split(/,/, $s); }",
19741            "sub f { my @a = split( # sep\n /,/, $s); }",
19742            (0, 1),
19743        ),
19744        // A subject-less `when` scores its operand once; the comment
19745        // made the peel return it, so `x > 1` counted twice.
19746        (
19747            LANG::Ruby,
19748            "def f\n  case\n  when (x > 1)\n    y\n  end\nend\n",
19749            "def f\n  case\n  when ( # c\n x > 1)\n    y\n  end\nend\n",
19750            (1, 0),
19751        ),
19752        // Elixir's match operator, read at child(1), lost to a comment
19753        // before the `=` — an assignment rather than a condition.
19754        (
19755            LANG::Elixir,
19756            "defmodule M do\n def f do\n  a = 1\n end\nend\n",
19757            "defmodule M do\n def f do\n  a # c\n = 1\n end\nend\n",
19758            (0, 1),
19759        ),
19760    ];
19761
19762    #[test]
19763    fn a_slot_is_read_by_role() {
19764        let mut checked = 0;
19765        for &(lang, plain, variant, expected) in ROWS.iter().filter(|row| row.0.is_enabled()) {
19766            let without = abc(lang, plain);
19767            assert_eq!(
19768                abc(lang, variant),
19769                without,
19770                "{lang:?}: the variant scored differently\n  plain:   {plain}\n  variant: {variant}"
19771            );
19772            assert_eq!(without, expected, "{lang:?}: {plain}");
19773            checked += 1;
19774        }
19775        assert!(
19776            checked > 0,
19777            "no row language enabled; this test asserted nothing"
19778        );
19779    }
19780}