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: 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// A language's scorer for one operand: its `count_boolean_slot` or
531// `count_negated_operand` wrapper.
532pub(super) type OperandScorer = fn(&Node, &mut f64);
533
534// Fitzpatrick Rule 9 for a chain whose grammar names its two operands
535// `left` and `right`: each is scored by `score`. Read by field rather
536// than as every operand child, so a comment beside the operator — a
537// named child too — does not pay. The caller owns the gate deciding
538// that `chain` is a chain at all.
539pub(super) fn count_field_operands(chain: &Node, score: OperandScorer, conditions: &mut f64) {
540    for field in ["left", "right"] {
541        if let Some(operand) = chain.child_by_field_name(field) {
542            score(&operand, conditions);
543        }
544    }
545}
546
547// Each operand child of `list` — an argument list, a `return` value
548// list, a chain whose grammar names no operand field — scored by
549// `score`. `is_operand` keeps punctuation and comments out.
550pub(super) fn count_each_operand(list: &Node, score: OperandScorer, conditions: &mut f64) {
551    for operand in list.children().filter(is_operand) {
552        score(&operand, conditions);
553    }
554}
555
556// Default no-op `Abc` impls. Audited in #188; the matrix below
557// records the rationale for every entry so the no-op default is a
558// deliberate choice, not scaffolding leftover.
559//
560// Real defaults (the language has no construct ABC measures, so the
561// metric is genuinely 0):
562//   - PreprocCode, CcommentCode: no executable code (comments /
563//     preprocessor lines only).
564implement_metric_trait!(Abc, PreprocCode, CcommentCode);
565
566#[cfg(test)]
567#[allow(
568    clippy::float_cmp,
569    clippy::cast_precision_loss,
570    clippy::cast_possible_truncation,
571    clippy::cast_sign_loss,
572    clippy::similar_names,
573    clippy::doc_markdown,
574    clippy::needless_raw_string_hashes,
575    clippy::too_many_lines
576)]
577mod tests {
578    #[cfg(feature = "csharp")]
579    use crate::test_support::assert_csharp_fixture_spells;
580    use crate::test_support::{
581        assert_fixture_spells, ast_has_kind_id, check_func_space_only_shim,
582        check_metrics_only_shim, child_space, metrics_verbatim,
583    };
584    // Hand-written, not derived. A trait is used through its methods, so
585    // nothing in a test body names `ParserTrait` and
586    // `check-test-lang-gates.py` derives no gate for an import at all,
587    // so an ungated one stays compiled into builds that never call
588    // `<Lang>Parser::new`. These seven are the languages that do; a new
589    // `XParser::new` test in this module needs its feature added here,
590    // and the leg that lacks it says so as an unused import.
591    #[cfg(any(
592        feature = "csharp",
593        feature = "groovy",
594        feature = "kotlin",
595        feature = "php",
596        feature = "ruby",
597        feature = "rust",
598        feature = "typescript",
599    ))]
600    use crate::traits::ParserTrait;
601
602    use super::*;
603
604    check_metrics_only_shim!(check_metrics, Abc);
605    // Every `check_func_space` caller in this module is an ABC-versus-
606    // cyclomatic parity test (`abc.conditions() == cyclomatic() - 1` on
607    // the same space), so the func-space shim carries Cyclomatic too.
608    check_func_space_only_shim!(check_func_space, Abc, Cyclomatic);
609
610    /// `abc.conditions_sum()` of `src` under a walk restricted to ABC
611    /// and the metrics it resolves, for the cross-language parity tests
612    /// that compare one construct across sibling grammars.
613    /// `check_metrics` expands to a plain `fn` and so cannot close over
614    /// a reference value; this can. Restricted rather than
615    /// `MetricsOptions::default()` per `metrics_verbatim`'s own doc
616    /// (#1127).
617    #[cfg(any(
618        feature = "c",
619        feature = "cpp",
620        feature = "csharp",
621        feature = "go",
622        feature = "java",
623        feature = "javascript",
624        feature = "mozcpp",
625        feature = "mozjs",
626        feature = "objc",
627        feature = "perl",
628        feature = "typescript",
629    ))]
630    fn abc_conditions(lang: LANG, src: &str) -> u64 {
631        metrics_verbatim(
632            lang,
633            src.as_bytes(),
634            MetricsOptions::default().with_only(&[crate::Metric::Abc]),
635        )
636        .abc
637        .conditions_sum()
638    }
639
640    // Recurse to the innermost function space and assert the invariant
641    // named above there. `conditions()` is that one space's own count,
642    // so it pairs with `cyclomatic()` and never `cyclomatic_sum()`,
643    // which folds in a base of 1 per nested space.
644    #[cfg(any(
645        feature = "cpp",
646        feature = "csharp",
647        feature = "groovy",
648        feature = "java",
649        feature = "perl",
650        feature = "php",
651        feature = "typescript",
652    ))]
653    fn assert_deepest_conditions_match_cyclomatic(space: &crate::FuncSpace, expected: u64) {
654        let mut deepest = space;
655        while let Some(child) = deepest.spaces.last() {
656            deepest = child;
657        }
658        let decisions = deepest.metrics.cyclomatic.cyclomatic() - 1;
659        assert_eq!(decisions, expected);
660        assert_eq!(deepest.metrics.abc.conditions(), decisions);
661    }
662
663    // The sibling form of the helper above, for a fixture whose point is
664    // that several *different* spellings of one construct agree. The
665    // deepest-space walk cannot serve those: it follows `spaces.last()`
666    // and so inspects exactly one member, which is the shape #1383's
667    // over-count hid behind — a merged root total cannot tell a
668    // relational method scoring 2 from the constant control scoring 2.
669    #[cfg(any(feature = "csharp", feature = "elixir"))]
670    fn assert_every_member_scores(
671        container: &crate::FuncSpace,
672        members: usize,
673        expected: u64,
674        why: &str,
675    ) {
676        // The count is a parameter rather than a `!is_empty()` check so
677        // a caller cannot omit it: without it a fixture that lost every
678        // member but one still satisfies "every member scores N".
679        assert_eq!(
680            container.spaces.len(),
681            members,
682            "member count changed — the fixture moved, not the metric"
683        );
684        for member in &container.spaces {
685            let name = member.name.as_deref().unwrap_or("?");
686            assert_eq!(member.metrics.abc.conditions(), expected, "{name}: {why}");
687            assert_eq!(
688                member.metrics.abc.conditions(),
689                member.metrics.cyclomatic.cyclomatic() - 1,
690                "{name}: §8 parity with the cyclomatic decision count"
691            );
692        }
693    }
694
695    // The second sibling, for a fixture whose point is that a construct
696    // moves a member *off* the value its unguarded control scores.
697    // `assert_every_member_scores` cannot serve those: it takes one
698    // expected value for the whole container, and every #1422 guard
699    // fixture deliberately pairs guarded members with an unguarded
700    // control one lower — which is the comparison the test exists to
701    // make. Both tiers per member, because a guard is a decision in each
702    // and a fix that moved only one of them would satisfy neither claim
703    // on its own, and because a member may legitimately sit *above* its
704    // decision count — an `else` arm, or a comparison nested inside
705    // another comparison, is an ABC condition with no cyclomatic
706    // decision behind it (#1421).
707    #[cfg(any(
708        feature = "csharp",
709        feature = "elixir",
710        feature = "go",
711        feature = "groovy",
712        feature = "java",
713        feature = "kotlin",
714        feature = "perl",
715        feature = "python",
716        feature = "ruby",
717        feature = "rust",
718    ))]
719    #[track_caller]
720    fn assert_members_score(container: &crate::FuncSpace, expected: &[(&str, u64, u64)]) {
721        assert_eq!(
722            container.spaces.len(),
723            expected.len(),
724            "member count changed — the fixture moved, not the metric"
725        );
726        for (name, conditions, cyclomatic) in expected {
727            let member = child_space(container, name);
728            assert_eq!(
729                member.metrics.abc.conditions(),
730                *conditions,
731                "{name}: abc.conditions"
732            );
733            assert_eq!(
734                member.metrics.cyclomatic.cyclomatic(),
735                *cyclomatic,
736                "{name}: cyclomatic"
737            );
738        }
739    }
740
741    /// Regression for #227: a `Stats::default()` that never sees an
742    /// observation must not leak the `f64::MAX` sentinel for
743    /// `assignments_min`, `branches_min`, or `conditions_min`. All
744    /// three getters collapse the sentinel to `0.0` so JSON never
745    /// emits `1.7976931e308`.
746    #[test]
747    fn abc_empty_file_min_is_zero() {
748        let stats = Stats::default();
749        assert_eq!(stats.assignments_min(), 0);
750        assert_eq!(stats.branches_min(), 0);
751        assert_eq!(stats.conditions_min(), 0);
752    }
753
754    // The `EQ` arm of `java_count_token_assignment`: a plain `=` counts
755    // unless `java_eq_initializes_final_binding` finds it initialising a
756    // `final` binding, whether the declaration is a local or a field.
757    #[cfg(feature = "java")]
758    #[test]
759    fn java_eq_arm_counts_outside_final_declarations() {
760        check_metrics::<JavaParser>(
761            "class A { void m() { int x = 0; x = 1; x = 2; x = 3; } }",
762            "foo.java",
763            |metric| {
764                // `int x = 0;` is not `final`, so it counts like each
765                // `x = N;` that follows.
766                assert_eq!(metric.abc.assignments_sum(), 4);
767            },
768        );
769    }
770
771    #[cfg(feature = "java")]
772    #[test]
773    fn java_eq_arm_skips_final_initializers() {
774        check_metrics::<JavaParser>(
775            "class A {
776                final int X = 1;
777                @Deprecated private final int Y = 2;
778                void m() { final int Z = 3; }
779            }",
780            "foo.java",
781            |metric| {
782                // All three `=` tokens are `final` initializers — the
783                // second behind an annotation and an access modifier in
784                // the same `modifiers` node — so assignments are 0
785                // across all spaces.
786                assert_eq!(metric.abc.assignments_sum(), 0);
787            },
788        );
789    }
790
791    #[cfg(feature = "java")]
792    #[test]
793    fn java_final_initializer_does_not_suppress_the_assignments_inside_it() {
794        // The sentinel stack the structural predicate replaced stayed
795        // live from `final` to the next `;`, so every `=` nested in the
796        // initializer — a lambda body's, an array initializer's — was
797        // suppressed with the declarator's own. Java lambdas open no
798        // space, so nothing else separated them. Each row names the
799        // pre-fix value.
800        let cases = [
801            ("void m() { final Runnable r = () -> { x = 1; }; }", 1, 0),
802            (
803                "void m() { final Runnable r = () -> { x = 1; y = 2; }; }",
804                2,
805                1,
806            ),
807            ("void m() { final Runnable r = () -> x = 1; }", 1, 0),
808            ("void m() { final int[] a = { x = 1 }; }", 1, 0),
809            ("private final Runnable f = () -> { x = 1; };", 1, 0),
810            // The `;` that closed the leak is the reference: the same
811            // body after a `final` local counts as it always did.
812            ("void m() { final int q = 1; x = 1; }", 1, 1),
813        ];
814        let mut ran = 0;
815        for (body, expected, before) in cases {
816            let src = format!("class K {{ int x, y; {body} }}\n");
817            let assignments = metrics_verbatim(
818                LANG::Java,
819                src.as_bytes(),
820                MetricsOptions::default().with_only(&[crate::Metric::Abc]),
821            )
822            .abc
823            .assignments_sum();
824            assert_eq!(assignments, expected, "`{body}` (pre-fix {before})");
825            ran += 1;
826        }
827        assert_eq!(ran, cases.len());
828        assert!(cases.iter().any(|&(_, now, before)| now != before));
829    }
830
831    // Constant declarations are not counted as assignments
832    #[cfg(feature = "java")]
833    #[test]
834    fn java_constant_declarations() {
835        check_metrics::<JavaParser>(
836            "class A {
837                private final int X1 = 0, Y1 = 0;
838                public final float PI = 3.14f;
839                final static String HELLO = \"Hello,\";
840                protected String world = \" world!\";   // +1a
841                public float e = 2.718f;                // +1a
842                private int x2 = 1, y2 = 2;             // +2a
843
844                void m() {
845                    final int Z1 = 0, Z2 = 0, Z3 = 0;
846                    final float T = 0.0f;
847                    int z1 = 1, z2 = 2, z3 = 3;         // +3a
848                    float t = 60.0f;                    // +1a
849                }
850            }",
851            "foo.java",
852            |metric| {
853                // magnitude: sqrt(64 + 0 + 0) = sqrt(64)
854                // space count: 3 (1 unit, 1 class and 1 method)
855                insta::assert_json_snapshot!(
856                    metric.abc,
857                    @r#"
858                {
859                  "assignments": 8,
860                  "branches": 0,
861                  "conditions": 0,
862                  "magnitude": 8.0,
863                  "value": 0.0,
864                  "assignments_average": 2.6666666666666665,
865                  "branches_average": 0.0,
866                  "conditions_average": 0.0,
867                  "assignments_min": 0,
868                  "assignments_max": 4,
869                  "branches_min": 0,
870                  "branches_max": 0,
871                  "conditions_min": 0,
872                  "conditions_max": 0
873                }
874                "#
875                );
876            },
877        );
878    }
879
880    // "In computer science, conditionals (that is, conditional statements, conditional expressions
881    // and conditional constructs,) are programming language commands for handling decisions."
882    // Source: https://en.wikipedia.org/wiki/Conditional_(computer_programming)
883    // According to this definition, boolean expressions that are evaluated to make a decision are considered as conditions
884    // Variables, method invocations and true or false values used inside
885    // variable declarations and assignment expressions are not counted as conditions
886    #[cfg(feature = "java")]
887    #[test]
888    fn java_declarations_with_conditions() {
889        check_metrics::<JavaParser>(
890            "
891            boolean a = (1 > 2);            // +1a +1c
892            boolean b = 3 > 4;              // +1a +1c
893            boolean c = (1 > 2) && 3 > 4;   // +1a +2c
894            boolean d = b && (x > 5) || c;  // +1a +3c
895            boolean e = !d;                 // +1a +1c
896            boolean f = ((!false));         // +1a +1c
897            boolean g = !(!(true));         // +1a +1c
898            boolean h = true;               // +1a
899            boolean i = (false);            // +1a
900            boolean j = (((((true)))));     // +1a
901            boolean k = (((((m())))));      // +1a +1b
902            boolean l = (((((!m())))));     // +1a +1b +1c
903            boolean m = (!(!((m()))));      // +1a +1b +1c
904            List<String> n = null;          // +1a (< and > used for generic types are not counted as conditions)
905            ",
906            "foo.java",
907          |metric| {
908                // magnitude: sqrt(196 + 9 + 144) = sqrt(349)
909                // space count: 1 (1 unit)
910                insta::assert_json_snapshot!(
911                    metric.abc,
912                    @r#"
913                {
914                  "assignments": 14,
915                  "branches": 3,
916                  "conditions": 12,
917                  "magnitude": 18.681541692269406,
918                  "value": 18.681541692269406,
919                  "assignments_average": 14.0,
920                  "branches_average": 3.0,
921                  "conditions_average": 12.0,
922                  "assignments_min": 14,
923                  "assignments_max": 14,
924                  "branches_min": 3,
925                  "branches_max": 3,
926                  "conditions_min": 12,
927                  "conditions_max": 12
928                }
929                "#
930                );
931            },
932        );
933    }
934
935    // Conditions can be found in assignment expressions
936    #[cfg(feature = "java")]
937    #[test]
938    fn java_assignments_with_conditions() {
939        check_metrics::<JavaParser>(
940            "
941            a = 2 < 1;                  // +1a +1c
942            b = (4 >= 3) && 2 <= 1;     // +1a +2c
943            c = a || (x != 10) && b;    // +1a +3c
944            d = !false;                 // +1a +1c
945            e = (!false);               // +1a +1c
946            f = !(false);               // +1a +1c
947            g = (!(((true))));          // +1a +1c
948            h = ((true));               // +1a
949            i = !m();                   // +1a +1b +1c
950            j = !((m()));               // +1a +1b +1c
951            k = (!(m()));               // +1a +1b +1c
952            l = ((!(m())));             // +1a +1b +1c
953            m = !B.<Integer>m(2);       // +1a +1b +1c
954            n = !((B.<Integer>m(4)));   // +1a +1b +1c
955            ",
956            "foo.java",
957            |metric| {
958                // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
959                // space count: 1 (1 unit)
960                insta::assert_json_snapshot!(
961                    metric.abc,
962                    @r#"
963                {
964                  "assignments": 14,
965                  "branches": 6,
966                  "conditions": 16,
967                  "magnitude": 22.090722034374522,
968                  "value": 22.090722034374522,
969                  "assignments_average": 14.0,
970                  "branches_average": 6.0,
971                  "conditions_average": 16.0,
972                  "assignments_min": 14,
973                  "assignments_max": 14,
974                  "branches_min": 6,
975                  "branches_max": 6,
976                  "conditions_min": 16,
977                  "conditions_max": 16
978                }
979                "#
980                );
981            },
982        );
983    }
984
985    // Conditions can be found in method arguments
986    #[cfg(feature = "java")]
987    #[test]
988    fn java_methods_arguments_with_conditions() {
989        check_metrics::<JavaParser>(
990            "
991            m1(a);                                  // +1b
992            m2(a, b);                               // +1b
993            m3(true, (false), (((true))));          // +1b
994            m3(m1(false), m1(true), m1(false));     // +4b
995            m1(!a);                                 // +1b +1c
996            m2((((a))), (!b));                      // +1b +1c
997            m3(!(a), b, !!!c);                      // +1b +2c
998            m3(a, !b, m2(!a, !m2(!b, !m1(!c))));    // +4b +6c
999            ",
1000            "foo.java",
1001            |metric| {
1002                // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
1003                // space count: 1 (1 unit)
1004                insta::assert_json_snapshot!(
1005                    metric.abc,
1006                    @r#"
1007                {
1008                  "assignments": 0,
1009                  "branches": 14,
1010                  "conditions": 10,
1011                  "magnitude": 17.204650534085253,
1012                  "value": 17.204650534085253,
1013                  "assignments_average": 0.0,
1014                  "branches_average": 14.0,
1015                  "conditions_average": 10.0,
1016                  "assignments_min": 0,
1017                  "assignments_max": 0,
1018                  "branches_min": 14,
1019                  "branches_max": 14,
1020                  "conditions_min": 10,
1021                  "conditions_max": 10
1022                }
1023                "#
1024                );
1025            },
1026        );
1027    }
1028
1029    // "A unary conditional expression is an implicit condition that uses no relational operators."
1030    // Source: Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report.
1031    // https://www.softwarerenovation.com/Articles.aspx (page 5)
1032    #[cfg(feature = "java")]
1033    #[test]
1034    fn java_if_single_conditions() {
1035        check_metrics::<JavaParser>(
1036            "
1037            if ( a < 0 ) {}             // +1c
1038            if ( ((a != 0)) ) {}        // +1c
1039            if ( !(a > 0) ) {}          // +1c
1040            if ( !(((a == 0))) ) {}     // +1c
1041            if ( b.m1() ) {}            // +1b +1c
1042            if ( !b.m1() ) {}           // +1b +1c
1043            if ( !!b.m2() ) {}          // +1b +1c
1044            if ( (!(b.m1())) ) {}       // +1b +1c
1045            if ( (!(!b.m1())) ) {}      // +1b +1c
1046            if ( ((b.m2())) ) {}        // +1b +1c
1047            if ( ((b.m().m1())) ) {}    // +2b +1c
1048            if ( c ) {}                 // +1c
1049            if ( !c ) {}                // +1c
1050            if ( !!!!!!!!!!c ) {}       // +1c
1051            if ( (((c))) ) {}           // +1c
1052            if ( (((!c))) ) {}          // +1c
1053            if ( ((!(c))) ) {}          // +1c
1054            if ( true ) {}              // +1c
1055            if ( !true ) {}             // +1c
1056            if ( ((false)) ) {}         // +1c
1057            if ( !(!(false)) ) {}       // +1c
1058            if ( !!!false ) {}          // +1c
1059            ",
1060            "foo.java",
1061            |metric| {
1062                // magnitude: sqrt(0 + 64 + 484) = sqrt(548)
1063                // space count: 1 (1 unit)
1064                insta::assert_json_snapshot!(
1065                    metric.abc,
1066                    @r#"
1067                {
1068                  "assignments": 0,
1069                  "branches": 8,
1070                  "conditions": 22,
1071                  "magnitude": 23.40939982143925,
1072                  "value": 23.40939982143925,
1073                  "assignments_average": 0.0,
1074                  "branches_average": 8.0,
1075                  "conditions_average": 22.0,
1076                  "assignments_min": 0,
1077                  "assignments_max": 0,
1078                  "branches_min": 8,
1079                  "branches_max": 8,
1080                  "conditions_min": 22,
1081                  "conditions_max": 22
1082                }
1083                "#
1084                );
1085            },
1086        );
1087    }
1088
1089    #[cfg(feature = "java")]
1090    #[test]
1091    fn java_if_multiple_conditions() {
1092        check_metrics::<JavaParser>(
1093            "
1094            if ( a || b || c || d ) {}              // +4c
1095            if ( a || b && c && d ) {}              // +4c
1096            if ( x < y && a == b ) {}               // +2c
1097            if ( ((z < (x + y))) ) {}               // +1c
1098            if ( a || ((((b))) && c) ) {}           // +3c
1099            if ( a && ((((a == b))) && c) ) {}      // +3c
1100            if ( a || ((((a == b))) || ((c))) ) {}  // +3c
1101            if ( x < y && B.m() ) {}                // +1b +2c
1102            if ( x < y && !(((B.m()))) ) {}         // +1b +2c
1103            if ( !(x < y) && !B.m() ) {}            // +1b +2c
1104            if ( !!!(!!!(a)) && B.m() ||            // +1b +2c
1105                 !B.m() && (((x > 4))) ) {}         // +1b +2c
1106            ",
1107            "foo.java",
1108            |metric| {
1109                // magnitude: sqrt(0 + 25 + 900) = sqrt(925)
1110                // space count: 1 (1 unit)
1111                insta::assert_json_snapshot!(
1112                    metric.abc,
1113                    @r#"
1114                {
1115                  "assignments": 0,
1116                  "branches": 5,
1117                  "conditions": 30,
1118                  "magnitude": 30.4138126514911,
1119                  "value": 30.4138126514911,
1120                  "assignments_average": 0.0,
1121                  "branches_average": 5.0,
1122                  "conditions_average": 30.0,
1123                  "assignments_min": 0,
1124                  "assignments_max": 0,
1125                  "branches_min": 5,
1126                  "branches_max": 5,
1127                  "conditions_min": 30,
1128                  "conditions_max": 30
1129                }
1130                "#
1131                );
1132            },
1133        );
1134    }
1135
1136    #[cfg(feature = "java")]
1137    #[test]
1138    fn java_while_and_do_while_conditions() {
1139        check_metrics::<JavaParser>(
1140            "
1141            while ( (!(!(!(a)))) ) {}                   // +1c
1142            while ( b || 1 > 2 ) {}                     // +2c
1143            while ( x.m() && (((c))) ) {}               // +1b +2c
1144            do {} while ( !!!(((!!!a))) );              // +1c
1145            do {} while ( a || (b && c) );              // +3c
1146            do {} while ( !x.m() && 1 > 2 || !true );   // +1b +3c
1147            ",
1148            "foo.java",
1149            |metric| {
1150                // magnitude: sqrt(0 + 4 + 144) = sqrt(148)
1151                // space count: 1 (1 unit)
1152                insta::assert_json_snapshot!(
1153                    metric.abc,
1154                    @r#"
1155                {
1156                  "assignments": 0,
1157                  "branches": 2,
1158                  "conditions": 12,
1159                  "magnitude": 12.165525060596439,
1160                  "value": 12.165525060596439,
1161                  "assignments_average": 0.0,
1162                  "branches_average": 2.0,
1163                  "conditions_average": 12.0,
1164                  "assignments_min": 0,
1165                  "assignments_max": 0,
1166                  "branches_min": 2,
1167                  "branches_max": 2,
1168                  "conditions_min": 12,
1169                  "conditions_max": 12
1170                }
1171                "#
1172                );
1173            },
1174        );
1175    }
1176
1177    // GMetrics, a Groovy source code analyzer, provides the following definition of unary conditional expression:
1178    // "These are cases where a single variable/field/value is treated as a boolean value.
1179    // Examples include `if (x)` and `return !ready`."
1180    // According to this definition, unary conditional expressions are counted also in function return values.
1181    // Source: https://dx42.github.io/gmetrics/metrics/AbcMetric.html
1182    // Examples: https://github.com/dx42/gmetrics/blob/master/src/test/groovy/org/gmetrics/metric/abc/AbcMetric_MethodTest.groovy
1183    #[cfg(feature = "java")]
1184    #[test]
1185    fn java_return_with_conditions() {
1186        check_metrics::<JavaParser>(
1187            "class A {
1188                boolean m1() {
1189                    return !(z >= 0);       // +1c
1190                }
1191                boolean m2() {
1192                    return (((!x)));        // +1c
1193                }
1194                boolean m3() {
1195                    return x && y;          // +2c
1196                }
1197                boolean m4() {
1198                    return y || (z < 0);    // +2c
1199                }
1200                boolean m5() {
1201                    return x || y ?         // +3c (two unary conditions and one ?)
1202                        true : false;
1203                }
1204            }",
1205            "foo.java",
1206            |metric| {
1207                // magnitude: sqrt(0 + 0 + 81) = sqrt(81)
1208                // space count: 7 (1 unit, 1 class and 5 methods)
1209                insta::assert_json_snapshot!(
1210                    metric.abc,
1211                    @r#"
1212                {
1213                  "assignments": 0,
1214                  "branches": 0,
1215                  "conditions": 9,
1216                  "magnitude": 9.0,
1217                  "value": 0.0,
1218                  "assignments_average": 0.0,
1219                  "branches_average": 0.0,
1220                  "conditions_average": 1.2857142857142858,
1221                  "assignments_min": 0,
1222                  "assignments_max": 0,
1223                  "branches_min": 0,
1224                  "branches_max": 0,
1225                  "conditions_min": 0,
1226                  "conditions_max": 3
1227                }
1228                "#
1229                );
1230            },
1231        );
1232    }
1233
1234    // Variables, method invocations, and true or false values
1235    // inside return statements are not counted as conditions
1236    #[cfg(feature = "java")]
1237    #[test]
1238    fn java_return_without_conditions() {
1239        check_metrics::<JavaParser>(
1240            "class A {
1241                boolean m1() {
1242                    return x;
1243                }
1244                boolean m2() {
1245                    return (x);
1246                }
1247                boolean m3() {
1248                    return y.m();   // +1b
1249                }
1250                boolean m4() {
1251                    return false;
1252                }
1253                void m5() {
1254                    return;
1255                }
1256            }",
1257            "foo.java",
1258            |metric| {
1259                // magnitude: sqrt(0 + 1 + 0) = sqrt(1)
1260                // space count: 7 (1 unit, 1 class and 5 methods)
1261                insta::assert_json_snapshot!(
1262                    metric.abc,
1263                    @r#"
1264                {
1265                  "assignments": 0,
1266                  "branches": 1,
1267                  "conditions": 0,
1268                  "magnitude": 1.0,
1269                  "value": 0.0,
1270                  "assignments_average": 0.0,
1271                  "branches_average": 0.14285714285714285,
1272                  "conditions_average": 0.0,
1273                  "assignments_min": 0,
1274                  "assignments_max": 0,
1275                  "branches_min": 0,
1276                  "branches_max": 1,
1277                  "conditions_min": 0,
1278                  "conditions_max": 0
1279                }
1280                "#
1281                );
1282            },
1283        );
1284    }
1285
1286    // Variables, method invocations, and true or false values
1287    // in lambda expression return values are not counted as conditions
1288    #[cfg(feature = "java")]
1289    #[test]
1290    fn java_lambda_expressions_return_with_conditions() {
1291        check_metrics::<JavaParser>(
1292            "
1293            Predicate<Boolean> p1 = a -> a;                         // +1a
1294            Predicate<Boolean> p2 = b -> true;                      // +1a
1295            Predicate<Boolean> p3 = c -> m();                       // +1a
1296            Predicate<Integer> p4 = d -> d > 10;                    // +1a +1c
1297            Predicate<Boolean> p5 = (e) -> !e;                      // +1a +1c
1298            Predicate<Boolean> p6 = (f) -> !((!f));                 // +1a +1c
1299            Predicate<Boolean> p7 = (g) -> !g && true;              // +1a +2c
1300            BiPredicate<Boolean, Boolean> bp1 = (h, i) -> !h && !i; // +1a +2c
1301            BiPredicate<Boolean, Boolean> bp2 = (j, k) -> {
1302                return j || k;                                      // +1a +2c
1303            };
1304            ",
1305            "foo.java",
1306            |metric| {
1307                // magnitude: sqrt(81 + 1 + 81) = sqrt(163)
1308                // space count: 1 (1 unit)
1309                insta::assert_json_snapshot!(
1310                    metric.abc,
1311                    @r#"
1312                {
1313                  "assignments": 9,
1314                  "branches": 1,
1315                  "conditions": 9,
1316                  "magnitude": 12.767145334803704,
1317                  "value": 12.767145334803704,
1318                  "assignments_average": 9.0,
1319                  "branches_average": 1.0,
1320                  "conditions_average": 9.0,
1321                  "assignments_min": 9,
1322                  "assignments_max": 9,
1323                  "branches_min": 1,
1324                  "branches_max": 1,
1325                  "conditions_min": 9,
1326                  "conditions_max": 9
1327                }
1328                "#
1329                );
1330            },
1331        );
1332    }
1333
1334    #[cfg(feature = "java")]
1335    #[test]
1336    fn java_for_with_variable_declaration() {
1337        check_metrics::<JavaParser>(
1338            "
1339            for ( int i1 = 0; !(!(!(!a))); i1++ ) {}                // +2a +1c
1340            for ( int i2 = 0; !B.m(); i2++ ) {}                     // +2a +1b +1c
1341            for ( int i3 = 0; a || false; i3++ ) {}                 // +2a +2c
1342            for ( int i4 = 0; a && B.m() ? true : false; i4++ ) {}  // +2a +1b +3c
1343            for ( int i5 = 0; true; i5++ ) {}                       // +2a +1c
1344            ",
1345            "foo.java",
1346            |metric| {
1347                // magnitude: sqrt(100 + 4 + 64) = sqrt(168)
1348                // space count: 1 (1 unit)
1349                insta::assert_json_snapshot!(
1350                    metric.abc,
1351                    @r#"
1352                {
1353                  "assignments": 10,
1354                  "branches": 2,
1355                  "conditions": 8,
1356                  "magnitude": 12.96148139681572,
1357                  "value": 12.96148139681572,
1358                  "assignments_average": 10.0,
1359                  "branches_average": 2.0,
1360                  "conditions_average": 8.0,
1361                  "assignments_min": 10,
1362                  "assignments_max": 10,
1363                  "branches_min": 2,
1364                  "branches_max": 2,
1365                  "conditions_min": 8,
1366                  "conditions_max": 8
1367                }
1368                "#
1369                );
1370            },
1371        );
1372    }
1373
1374    #[cfg(feature = "java")]
1375    #[test]
1376    fn java_for_without_variable_declaration() {
1377        check_metrics::<JavaParser>(
1378            "class A{
1379                void m1() {
1380                    for (i = 0; x < y; i++) {}          // +2a +1c
1381                    for (i = 0; ((x < y)); i++) {}      // +2a +1c
1382                    for (i = 0; !(!(x < y)); i++) {}    // +2a +1c
1383                    for (i = 0; true; i++) {}           // +2a +1c
1384                }
1385                void m2() {
1386                    for ( ; true; ) {}  // +1c
1387                }
1388                void m3() {
1389                    for ( ; ; ) {}      // +0c — no condition to count (#1276)
1390                }
1391            }",
1392            "foo.java",
1393            |metric| {
1394                // magnitude: sqrt(64 + 0 + 25) = sqrt(89)
1395                // space count: 5 (1 unit, 1 class and 3 methods)
1396                insta::assert_json_snapshot!(
1397                    metric.abc,
1398                    @r#"
1399                {
1400                  "assignments": 8,
1401                  "branches": 0,
1402                  "conditions": 5,
1403                  "magnitude": 9.433981132056603,
1404                  "value": 0.0,
1405                  "assignments_average": 1.6,
1406                  "branches_average": 0.0,
1407                  "conditions_average": 1.0,
1408                  "assignments_min": 0,
1409                  "assignments_max": 8,
1410                  "branches_min": 0,
1411                  "branches_max": 0,
1412                  "conditions_min": 0,
1413                  "conditions_max": 4
1414                }
1415                "#
1416                );
1417            },
1418        );
1419    }
1420
1421    // Issue #1276 changed Java's answer here. `java_walk_for_statement`
1422    // used to read child(3), fall through to child(4) when that was the
1423    // `;` an expression initializer leaves behind, and count a `;` or
1424    // `)` landing there as a vacuously-true condition — so `for (;;)`
1425    // scored one. Nothing in Fitzpatrick's C dimension attributes a
1426    // condition to a `for` keyword; it counts conditional operators and
1427    // unary conditions that are present. Java and Groovy were the only
1428    // two impls disagreeing, and the field-addressed walker now reports
1429    // zero the way the C family, the JS family, PHP, C# and Go all do.
1430    #[cfg(feature = "java")]
1431    #[test]
1432    fn java_empty_for_condition_counts_nothing() {
1433        check_metrics::<JavaParser>(
1434            "class A { void m() { for (;;) { break; } } }",
1435            "foo.java",
1436            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
1437        );
1438        // The other empty spelling, and the one the old cascade
1439        // actually mis-scored: with an **expression** initializer the
1440        // children are `for ( init ; ; update )`, so child(3) was the
1441        // separating `;` and child(4) the empty condition's `;`, which
1442        // the vacuous-true arm counted. The `for (int i = 0; ; i++)`
1443        // spelling scored 0 both before and after — Java's
1444        // `local_variable_declaration` swallows its own `;`, putting the
1445        // update expression at child(4), where it matched no arm — so it
1446        // would be a vacuous regression test and is deliberately not
1447        // used here.
1448        check_metrics::<JavaParser>(
1449            "class A { void m() { int i; for (i = 0; ; i++) { break; } } }",
1450            "foo.java",
1451            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
1452        );
1453    }
1454
1455    // The second defect the positional cascade carried: tree-sitter
1456    // counts comments among a node's children, so a comment anywhere in
1457    // the header shifted every index and the condition went unread —
1458    // the same failure #1181 removed from `java_walk_ternary`. Reading
1459    // the `condition` field cannot shift.
1460    #[cfg(feature = "java")]
1461    #[test]
1462    fn java_for_condition_survives_a_header_comment() {
1463        check_metrics::<JavaParser>(
1464            "class A { void m(boolean a) { for (; /* n */ a; ) { break; } } }",
1465            "foo.java",
1466            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
1467        );
1468        // Unchanged control: the same loop without the comment. Both
1469        // spellings must agree, which is the property the positional
1470        // form broke.
1471        check_metrics::<JavaParser>(
1472            "class A { void m(boolean a) { for (; a; ) { break; } } }",
1473            "foo.java",
1474            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
1475        );
1476    }
1477
1478    // Variables, method invocations, and true or false values
1479    // in ternary expression return values are not counted as conditions
1480    #[cfg(feature = "java")]
1481    #[test]
1482    fn java_ternary_conditions() {
1483        check_metrics::<JavaParser>(
1484            "
1485            a = true;                                   // +1a
1486            b = a ? true : false;                       // +1a +2c
1487            c = ((((a)))) ? !false : !b;                // +1a +4c
1488            d = !this.m() ? !!a : (false);              // +1a +1b +3c
1489            e = !(a) && b ? ((c)) : !d;                 // +1a +4c
1490            if ( this.m() ? a : !this.m() ) {}          // +2b +3c
1491            if ( x > 0 ? !(false) : this.m() ) {}       // +1b +3c
1492            if ( x > 0 && x != 3 ? !(a) : (!(b)) ) {}   // +5c
1493            ",
1494            "foo.java",
1495            |metric| {
1496                // magnitude: sqrt(25 + 16 + 576) = sqrt(617)
1497                //  space count: 1 (1 unit)
1498                insta::assert_json_snapshot!(
1499                    metric.abc,
1500                    @r#"
1501                {
1502                  "assignments": 5,
1503                  "branches": 4,
1504                  "conditions": 24,
1505                  "magnitude": 24.839484696748443,
1506                  "value": 24.839484696748443,
1507                  "assignments_average": 5.0,
1508                  "branches_average": 4.0,
1509                  "conditions_average": 24.0,
1510                  "assignments_min": 5,
1511                  "assignments_max": 5,
1512                  "branches_min": 4,
1513                  "branches_max": 4,
1514                  "conditions_min": 24,
1515                  "conditions_max": 24
1516                }
1517                "#
1518                );
1519            },
1520        );
1521    }
1522
1523    #[cfg(feature = "bash")]
1524    #[test]
1525    fn bash_assignments_only() {
1526        check_metrics::<BashParser>(
1527            "f() {
1528                 a=1
1529                 b=2
1530                 c+=3
1531             }",
1532            "foo.sh",
1533            |metric| {
1534                insta::assert_json_snapshot!(
1535                    metric.abc,
1536                    @r#"
1537                {
1538                  "assignments": 3,
1539                  "branches": 0,
1540                  "conditions": 0,
1541                  "magnitude": 3.0,
1542                  "value": 0.0,
1543                  "assignments_average": 1.5,
1544                  "branches_average": 0.0,
1545                  "conditions_average": 0.0,
1546                  "assignments_min": 0,
1547                  "assignments_max": 3,
1548                  "branches_min": 0,
1549                  "branches_max": 0,
1550                  "conditions_min": 0,
1551                  "conditions_max": 0
1552                }
1553                "#
1554                );
1555            },
1556        );
1557    }
1558
1559    #[cfg(feature = "bash")]
1560    #[test]
1561    fn bash_commands_only() {
1562        check_metrics::<BashParser>(
1563            "f() {
1564                 echo a
1565                 ls
1566             }",
1567            "foo.sh",
1568            |metric| {
1569                insta::assert_json_snapshot!(
1570                    metric.abc,
1571                    @r#"
1572                {
1573                  "assignments": 0,
1574                  "branches": 2,
1575                  "conditions": 0,
1576                  "magnitude": 2.0,
1577                  "value": 0.0,
1578                  "assignments_average": 0.0,
1579                  "branches_average": 1.0,
1580                  "conditions_average": 0.0,
1581                  "assignments_min": 0,
1582                  "assignments_max": 0,
1583                  "branches_min": 0,
1584                  "branches_max": 2,
1585                  "conditions_min": 0,
1586                  "conditions_max": 0
1587                }
1588                "#
1589                );
1590            },
1591        );
1592    }
1593
1594    #[cfg(feature = "bash")]
1595    #[test]
1596    fn bash_control_flow_counts_conditions() {
1597        // Regression for #696: Bash control-flow branches are ABC
1598        // conditions (a Bash predicate is a command, so the branch keyword
1599        // is the only condition signal). Each mirrors a cyclomatic decision.
1600        //
1601        // expected: 4 conditions — `if` (1) + `elif` (1) + `while` (1) +
1602        // the non-wildcard case arm `a)` (1). The bare-`*)` wildcard arm is
1603        // the Bash analogue of `default:` and is excluded, exactly as the
1604        // cyclomatic standard count excludes it. No comparison / test
1605        // operators appear, so every condition here is control-flow.
1606        check_metrics::<BashParser>(
1607            "f() {
1608                 if cmd; then
1609                     echo a
1610                 elif other; then
1611                     echo b
1612                 fi
1613                 while running; do
1614                     echo c
1615                 done
1616                 case \"$x\" in
1617                     a) echo d ;;
1618                     *) echo e ;;
1619                 esac
1620             }",
1621            "foo.sh",
1622            |metric| {
1623                assert_eq!(metric.abc.conditions_sum(), 4);
1624            },
1625        );
1626    }
1627
1628    #[cfg(feature = "bash")]
1629    #[test]
1630    fn bash_conditions_mix() {
1631        // Exercises every condition path: `==` and `!=` inside `[[ ]]`,
1632        // arithmetic `<` inside `(( ))`, and the prefix `-z` test operator
1633        // inside `[ ]`. Each `if` body's `echo` contributes a branch.
1634        //
1635        // expected: 8 conditions — each of the four `if`s contributes one
1636        // for the control-flow branch (#696) plus one for its comparison /
1637        // test operator (`==`, `!=`, `<`, `-z`). 4 branches (one `echo`
1638        // each). magnitude = sqrt(4² + 8²) = sqrt(80).
1639        check_metrics::<BashParser>(
1640            "f() {
1641                 if [[ \"$a\" == \"$b\" ]]; then
1642                     echo eq
1643                 fi
1644                 if [[ \"$x\" != \"$y\" ]]; then
1645                     echo ne
1646                 fi
1647                 if (( $a < $b )); then
1648                     echo lt
1649                 fi
1650                 if [ -z \"$x\" ]; then
1651                     echo empty
1652                 fi
1653             }",
1654            "foo.sh",
1655            |metric| {
1656                assert_eq!(metric.abc.conditions_sum(), 8);
1657                assert_eq!(metric.abc.branches_sum(), 4);
1658                insta::assert_json_snapshot!(
1659                    metric.abc,
1660                    @r#"
1661                {
1662                  "assignments": 0,
1663                  "branches": 4,
1664                  "conditions": 8,
1665                  "magnitude": 8.94427190999916,
1666                  "value": 0.0,
1667                  "assignments_average": 0.0,
1668                  "branches_average": 2.0,
1669                  "conditions_average": 4.0,
1670                  "assignments_min": 0,
1671                  "assignments_max": 0,
1672                  "branches_min": 0,
1673                  "branches_max": 4,
1674                  "conditions_min": 0,
1675                  "conditions_max": 8
1676                }
1677                "#
1678                );
1679            },
1680        );
1681    }
1682
1683    #[cfg(feature = "bash")]
1684    #[test]
1685    fn bash_redirection_is_not_a_condition() {
1686        // `>` and `<` spell an I/O redirection as well as a comparison, and
1687        // the grammar parents the redirection under `file_redirect` rather
1688        // than `binary_expression`. Ungated, every redirect in a script
1689        // scored a condition: this fixture measured 2 before the parent
1690        // gate — the Bash instance of #1280's positive-parent polarity.
1691        // expected: 0 conditions — no test, no `if`, no comparison.
1692        check_metrics::<BashParser>(
1693            "f() {\n  echo hi > out.txt\n  read x < in.txt\n}\n",
1694            "foo.sh",
1695            |metric| {
1696                assert_eq!(metric.abc.conditions_sum(), 0);
1697                // The two `echo` / `read` commands still count as branches,
1698                // so the zero above is the gate firing rather than the walk
1699                // skipping the function body.
1700                assert_eq!(metric.abc.branches_sum(), 2);
1701            },
1702        );
1703    }
1704
1705    #[cfg(feature = "bash")]
1706    #[test]
1707    fn bash_comparison_inside_an_arithmetic_or_test_context_is_a_condition() {
1708        // The positive control for the gate above: the same tokens under a
1709        // `binary_expression` are real comparisons in both the `[[ … ]]`
1710        // test form and the `(( … ))` arithmetic form, and still count.
1711        // expected: 4 — one `if` control-flow condition and one `>` per
1712        // function.
1713        check_metrics::<BashParser>(
1714            "f() {\n  if [[ $a > $b ]]; then :; fi\n}\ng() {\n  if (( a > b )); then :; fi\n}\n",
1715            "foo.sh",
1716            |metric| {
1717                assert_eq!(metric.abc.conditions_sum(), 4);
1718            },
1719        );
1720    }
1721
1722    #[cfg(feature = "bash")]
1723    #[test]
1724    fn bash_arithmetic_ternary_is_a_condition() {
1725        // The ABC half of #1268. Cyclomatic and cognitive both count Bash's
1726        // only ternary form; ABC did not, so the identical construct scored
1727        // 1 here against the C family's 2.
1728        // expected: 2 — the `>` comparison and the ternary itself, matching
1729        // `int m = a > b ? a : b;` in C.
1730        check_metrics::<BashParser>(
1731            "f() {\n  local m=$(( a > b ? a : b ))\n}\n",
1732            "foo.sh",
1733            |metric| {
1734                assert_eq!(metric.abc.conditions_sum(), 2);
1735            },
1736        );
1737    }
1738
1739    #[cfg(feature = "bash")]
1740    #[test]
1741    fn bash_magnitude() {
1742        // Combined assignments + branches + conditions. The single `if`
1743        // contributes two conditions (the control-flow branch, #696, plus
1744        // the `==` operator), so magnitude = sqrt(2² + 1² + 2²) = sqrt(9).
1745        check_metrics::<BashParser>(
1746            "f() {
1747                 a=1
1748                 b=2
1749                 if [[ \"$a\" == \"$b\" ]]; then
1750                     echo eq
1751                 fi
1752             }",
1753            "foo.sh",
1754            |metric| {
1755                assert_eq!(metric.abc.conditions_sum(), 2);
1756                insta::assert_json_snapshot!(
1757                    metric.abc,
1758                    @r#"
1759                {
1760                  "assignments": 2,
1761                  "branches": 1,
1762                  "conditions": 2,
1763                  "magnitude": 3.0,
1764                  "value": 0.0,
1765                  "assignments_average": 1.0,
1766                  "branches_average": 0.5,
1767                  "conditions_average": 1.0,
1768                  "assignments_min": 0,
1769                  "assignments_max": 2,
1770                  "branches_min": 0,
1771                  "branches_max": 1,
1772                  "conditions_min": 0,
1773                  "conditions_max": 2
1774                }
1775                "#
1776                );
1777            },
1778        );
1779    }
1780
1781    #[cfg(feature = "java")]
1782    #[test]
1783    fn java_malformed_parenthesized_no_panic() {
1784        check_metrics::<JavaParser>("class A { void m() { if (( }) }", "foo.java", |metric| {
1785            // tree-sitter emits ERROR nodes for this malformed source, so no
1786            // IfStatement, branch, or condition is recognised — all counts are 0.
1787            // Primary goal: the unwrap-free path does not panic.
1788            assert_eq!(metric.abc.assignments(), 0);
1789            assert_eq!(metric.abc.branches(), 0);
1790            assert_eq!(metric.abc.conditions(), 0);
1791            assert_eq!(metric.abc.magnitude(), 0.0);
1792        });
1793    }
1794
1795    #[cfg(feature = "java")]
1796    #[test]
1797    fn java_bool_returning_terminal_kinds_count() {
1798        // Companion to `csharp_bool_returning_terminal_kinds_count`
1799        // (issue #372 / lesson #19). Java's grammar wraps every
1800        // if/while/do condition in `parenthesized_expression`, so
1801        // the gap lived in the former `java_inspect_container`'s
1802        // terminal-arm recognizer: `FieldAccess` (`cfg.flag`), `CastExpression`
1803        // (`(boolean)v`), `ArrayAccess` (`flags[0]`), and
1804        // `InstanceofExpression` (`x instanceof Foo`) were never
1805        // counted. Java has no `await` or `is_pattern` analogues,
1806        // so the C# fix's five-kind set collapses to four here.
1807        //
1808        // `instanceof` has since moved out of the terminal set to an
1809        // unconditional arm (#1461), so it no longer depends on the
1810        // slot to be counted. The total is unchanged, which is the
1811        // point of that change rather than an accident of it.
1812        //
1813        // expected: 4 conditions (one per `if`), 0 assignments,
1814        // 0 branches (no invocations).
1815        check_metrics::<JavaParser>(
1816            "class Cfg { boolean flag; }
1817            class A {
1818                void m(Object v, boolean[] flags, Cfg cfg) {
1819                    if (cfg.flag) { }
1820                    if ((boolean) v) { }
1821                    if (v instanceof Cfg) { }
1822                    if (flags[0]) { }
1823                }
1824            }",
1825            "foo.java",
1826            |metric| {
1827                assert_eq!(metric.abc.conditions_sum(), 4);
1828                assert_eq!(metric.abc.assignments_sum(), 0);
1829                assert_eq!(metric.abc.branches_sum(), 0);
1830            },
1831        );
1832    }
1833
1834    // Issue #1274: a generic *declaration* is type syntax, not a
1835    // decision. `java_count_token_condition` denied only
1836    // `type_arguments`, so `class Gen<T>` and `<U> U ident(U x)` — both
1837    // `type_parameters` — each scored two conditions. The fix counts
1838    // `<` / `>` only under `binary_expression`, the polarity C / C++ /
1839    // Rust / Go already use; a `grammar.json` sweep proves the token is
1840    // emitted from exactly three productions, so the two shapes agree on
1841    // every well-formed input.
1842    //
1843    // One fixture per production the arm must ignore: the class-level
1844    // `type_parameters` (`Gen<T …>`), its `extends` bound's nested
1845    // `type_arguments` (`Comparable<T>`), the method-level
1846    // `type_parameters` (`<U>`), and a nested generic
1847    // (`Map<String, List<T>>`) whose two closing `>` lex as separate
1848    // tokens under separate `type_arguments`. Pre-fix this file scored
1849    // 4 conditions — two per `type_parameters` bracket pair; expected 0,
1850    // the file contains no conditional construct at all.
1851    #[cfg(feature = "java")]
1852    #[test]
1853    fn java_generic_declarations_are_not_conditions() {
1854        check_metrics::<JavaParser>(
1855            "class Gen<T extends Comparable<T>> {
1856                <U> U ident(U x) { return x; }
1857                List<T> pick(Map<String, List<T>> m) { return m.get(\"k\"); }
1858            }",
1859            "foo.java",
1860            |metric| {
1861                assert_eq!(metric.abc.conditions_sum(), 0);
1862                // Non-vacuity guard: 0 is also what an unparsed file
1863                // scores, so pin a value only a walked body can produce
1864                // — the `m.get("k")` invocation.
1865                assert_eq!(metric.abc.branches_sum(), 1);
1866            },
1867        );
1868    }
1869
1870    // Second half of #1274, same premise and same match: a wildcard type
1871    // argument's `?` is type syntax, not a ternary. tree-sitter-java emits
1872    // a bare `?` from exactly two productions — `ternary_expression` and
1873    // `wildcard` — so the `QMARK` arm carries the same allowlist gate the
1874    // `<` / `>` arm does.
1875    //
1876    // The body carries a real ternary over a real comparison so the
1877    // expected total is 2, not 0. Asserting 0 on a wildcard-only fixture
1878    // would have been vacuous — an unparsable file scores 0 too, so the
1879    // test could not tell "the wildcard was ignored" from "the fixture
1880    // stopped being recognised". Each failure mode now lands on its own
1881    // number: 4 if the wildcard `?` counts again (the pre-fix value), 3
1882    // if the gate is aimed at `Wildcard` instead, 1 if it swallows the
1883    // genuine ternary, 0 if parsing breaks. Two wildcards against one
1884    // ternary is deliberate — with one of each, aiming the gate at the
1885    // wrong one of the two productions still totals 2 and the test
1886    // cannot see the difference.
1887    #[cfg(feature = "java")]
1888    #[test]
1889    fn java_generic_wildcard_is_not_a_condition() {
1890        check_metrics::<JavaParser>(
1891            "class A {
1892                int m(List<? extends Number> xs, Map<String, ? extends Number> ys,
1893                      int a, int b) { return a < b ? 1 : 2; }
1894            }",
1895            "foo.java",
1896            |metric| {
1897                assert_eq!(metric.abc.conditions_sum(), 2);
1898            },
1899        );
1900    }
1901
1902    // The other half of #1274: narrowing the `<` / `>` arm must not
1903    // swallow real comparisons. `List<String>` in the signature keeps a
1904    // generic in scope so the fixture proves both directions at once,
1905    // and the assertion is the grammar-dispatch §8 pin — on the
1906    // method's own space the ABC condition count must equal the
1907    // cyclomatic decision count (`cyclomatic()` minus the per-space
1908    // base of 1). Both are 2, one per `if`.
1909    #[cfg(feature = "java")]
1910    #[test]
1911    fn java_comparison_operators_still_count_alongside_generics() {
1912        check_func_space::<JavaParser, _>(
1913            "class A {
1914                int m(List<String> xs, int a, int b) {
1915                    if (a < b) { return 1; }
1916                    if (a > b) { return 2; }
1917                    return 0;
1918                }
1919            }",
1920            "foo.java",
1921            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
1922        );
1923    }
1924
1925    #[cfg(feature = "java")]
1926    #[test]
1927    fn java_constructor_delegation_is_a_branch() {
1928        // Regression for #1279: `super(…)` / `this(…)` parse as
1929        // `explicit_constructor_invocation`, not `method_invocation`, so
1930        // each scored zero branches while Groovy scored one for identical
1931        // source. Both constructors delegate, and neither body contains any
1932        // other call.
1933        // expected: 2 branches — one delegation each.
1934        check_metrics::<JavaParser>(
1935            "class Sub extends Base {
1936                Sub() { super(1); }
1937                Sub(int a) { this(); }
1938            }",
1939            "foo.java",
1940            |metric| {
1941                assert_eq!(metric.abc.branches_sum(), 2);
1942            },
1943        );
1944    }
1945
1946    #[cfg(feature = "java")]
1947    #[test]
1948    fn java_constructor_delegation_does_not_double_count_arguments() {
1949        // The delegation node does not wrap a `method_invocation` for the
1950        // call itself, so `super(f())` is exactly two branches — the
1951        // delegation and the argument call — not three (#1279).
1952        // expected: 2 branches.
1953        check_metrics::<JavaParser>(
1954            "class Sub extends Base {
1955                Sub() { super(f()); }
1956            }",
1957            "foo.java",
1958            |metric| {
1959                assert_eq!(metric.abc.branches_sum(), 2);
1960            },
1961        );
1962    }
1963
1964    #[cfg(feature = "csharp")]
1965    #[test]
1966    fn csharp_constructor_initializer_is_a_branch() {
1967        // C# spells the same delegation as a `constructor_initializer`
1968        // (`: base(…)` / `: this(…)`), which likewise scored zero (#1279).
1969        // expected: 2 branches — one initializer each, no other calls.
1970        check_metrics::<CsharpParser>(
1971            "class Sub : Base {
1972                Sub() : base(1) { }
1973                Sub(int a) : this() { }
1974            }",
1975            "foo.cs",
1976            |metric| {
1977                assert_eq!(metric.abc.branches_sum(), 2);
1978            },
1979        );
1980    }
1981
1982    #[cfg(feature = "csharp")]
1983    #[test]
1984    fn csharp_primary_constructor_base_call_is_a_branch() {
1985        // The C# 12 primary-constructor superclass call invokes the base
1986        // constructor exactly as the `: base(…)` above it does, and scored
1987        // zero until #1406 — the C# sibling of Kotlin's #1384.
1988        //
1989        // tree-sitter-c-sharp 0.23.5 spells the `record` and `class`
1990        // families differently, so per `.claude/rules/grammar-dispatch.md`
1991        // §11 they are two independent paths and each needs its own
1992        // fixture: the record form nests the arguments under a
1993        // `primary_constructor_base_type`, the class form hangs them
1994        // straight off the `base_list`. A record-only fixture says nothing
1995        // about the class arm, and vice versa.
1996        //
1997        // `S` and `R3` are the negatives: a base type with no argument
1998        // list is not a call, and neither spelling emits an
1999        // `argument_list` for it.
2000        let src = "class Sub(int x) : Base(x) { }
2001                   record R1(int x) : Base(x);
2002                   record class R2(int x) : Base(x);
2003                   class G<T>(int x) : GBase<T>(x) where T : class { }
2004                   class Multi(int x) : Base(x), IBase { }
2005                   struct S(int x) : IBase { }
2006                   record R3(int x) : Base;";
2007        // Anchors every spelling against a fixture edit: with presence
2008        // checks only, deleting `(x)` from the class form leaves the
2009        // record form satisfying the arm and this test green. The last
2010        // two counts anchor the declarations that would otherwise decay
2011        // into duplicates of `Sub` — `Multi` owns the fixture's only
2012        // comma (`Base(x), IBase`) and `G` its only type argument list
2013        // (`GBase<T>`), so trimming either back fails here by name.
2014        assert_csharp_fixture_spells(
2015            src,
2016            &[
2017                (
2018                    Csharp::PrimaryConstructorBaseType as u16,
2019                    2,
2020                    "`record` base calls",
2021                ),
2022                (Csharp::ArgumentList as u16, 5, "base-call argument lists"),
2023                (Csharp::BaseList2 as u16, 7, "base lists"),
2024                // The §2 drift marker for the arm's other half. Both ids
2025                // render to `"base_list"`; at this pin every declaration
2026                // family above emits 252 and none emits 246, so
2027                // `BaseList` is defensive and this zero says so. A
2028                // grammar bump that starts emitting it fails here rather
2029                // than silently changing which arm does the work.
2030                (Csharp::BaseList as u16, 0, "unaliased `base_list` nodes"),
2031                (Csharp::COMMA as u16, 1, "`Multi`'s trailing interface"),
2032                (Csharp::TypeArgumentList as u16, 1, "`G`'s generic base"),
2033            ],
2034        );
2035        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
2036            for name in ["Sub", "R1", "R2", "G", "Multi"] {
2037                assert_eq!(
2038                    child_space(&space, name).metrics.abc.branches(),
2039                    1,
2040                    "{name}: the primary-constructor base call is one branch"
2041                );
2042            }
2043            for name in ["S", "R3"] {
2044                assert_eq!(
2045                    child_space(&space, name).metrics.abc.branches(),
2046                    0,
2047                    "{name}: a base type with no argument list is not a call"
2048                );
2049            }
2050        });
2051    }
2052
2053    #[cfg(feature = "csharp")]
2054    #[test]
2055    fn csharp_base_list_gate_excludes_other_argument_lists() {
2056        // The `base_list` parent gate on the `ArgumentList` arm is
2057        // load-bearing, not decoration: ungated it bills a branch for the
2058        // argument list of every call and allocation in the file, on top
2059        // of the `InvocationExpression` / `ObjectCreationExpression` the
2060        // arm already counts — doubling both (#1406).
2061        //
2062        // The fixture pairs one real base call with the four other
2063        // argument-carrying productions C# spells nearby: an attribute
2064        // (`attribute_argument_list`), an invocation, an object creation,
2065        // and an indexer (`bracketed_argument_list`) alongside a generic
2066        // (`type_argument_list`). Those last three are distinct kind ids
2067        // rather than `argument_list`, so they pin the §1 neighbour set
2068        // as much as the gate.
2069        let src = "[System.Obsolete(\"why\")]
2070                   class A(int x) : Base(x) {
2071                       void M(int y) { Helper(y); }
2072                       object N() { return new Foo(1); }
2073                       int P(System.Collections.Generic.List<int> l, int i) { return l[i]; }
2074                   }";
2075        assert_csharp_fixture_spells(
2076            src,
2077            &[
2078                (
2079                    Csharp::AttributeArgumentList as u16,
2080                    1,
2081                    "attribute argument lists",
2082                ),
2083                (
2084                    Csharp::BracketedArgumentList as u16,
2085                    1,
2086                    "bracketed argument lists",
2087                ),
2088                (Csharp::TypeArgumentList as u16, 1, "type argument lists"),
2089                (Csharp::ArgumentList as u16, 3, "argument lists"),
2090            ],
2091        );
2092        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
2093            let class = child_space(&space, "A");
2094            assert_eq!(
2095                class.metrics.abc.branches(),
2096                1,
2097                "`: Base(x)` alone — the attribute's arguments are not a call"
2098            );
2099            // The controls. 2 apiece would mean the gate let the
2100            // argument list through beside the call that owns it.
2101            assert_eq!(
2102                child_space(class, "M").metrics.abc.branches(),
2103                1,
2104                "`Helper(y)` is one branch, not one per argument list"
2105            );
2106            assert_eq!(
2107                child_space(class, "N").metrics.abc.branches(),
2108                1,
2109                "`new Foo(1)` is one branch, not one per argument list"
2110            );
2111            // `P` carries the indexer and the generic deliberately, but
2112            // gets no assertion: `bracketed_argument_list` and
2113            // `type_argument_list` are distinct kind ids, so `P` reads 0
2114            // with the gate, without it, and without this change
2115            // altogether. The census above is what actually pins them —
2116            // an assertion here would read as coverage it cannot provide.
2117        });
2118    }
2119
2120    #[cfg(feature = "csharp")]
2121    #[test]
2122    fn csharp_base_call_and_constructor_initializer_do_not_double_count() {
2123        // A class can spell *both* delegations at once: the primary
2124        // constructor's `: Base(x)` and a secondary constructor's
2125        // `: this(a)`. They are separate nodes in separate spaces, so the
2126        // file scores 2 — one each. 3 would mean the #1406 arm also fired
2127        // on something `ConstructorInitializer` already owned; 1 would
2128        // mean one of the two stopped counting.
2129        //
2130        // Java has the equivalent guard
2131        // (`java_constructor_delegation_does_not_double_count_arguments`);
2132        // C# now has two delegation spellings and this is the pairing
2133        // test for them.
2134        let src = "class Both(int x) : Base(x) {
2135                       public Both(int a, int b) : this(a) { }
2136                   }";
2137        assert_csharp_fixture_spells(
2138            src,
2139            &[
2140                (Csharp::ConstructorInitializer as u16, 1, "`: this(a)`"),
2141                (Csharp::BaseList2 as u16, 1, "the primary-constructor base"),
2142            ],
2143        );
2144        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
2145            let class = child_space(&space, "Both");
2146            assert_eq!(
2147                class.metrics.abc.branches(),
2148                1,
2149                "the class space owns `: Base(x)` and nothing else"
2150            );
2151            assert_eq!(
2152                child_space(class, "Both").metrics.abc.branches(),
2153                1,
2154                "the secondary constructor owns `: this(a)` and nothing else"
2155            );
2156        });
2157    }
2158
2159    #[cfg(feature = "kotlin")]
2160    #[test]
2161    fn kotlin_constructor_delegation_is_a_branch() {
2162        // Kotlin's secondary-constructor delegation is a
2163        // `constructor_delegation_call`, a distinct production from
2164        // `CallExpression`, so it scored zero for the same reason (#1279).
2165        // expected: 1 branch — the `: super(x)` delegation.
2166        check_metrics::<KotlinParser>(
2167            "class Sub : Base {
2168                constructor(x: Int) : super(x) { }
2169            }",
2170            "foo.kt",
2171            |metric| {
2172                assert_eq!(metric.abc.branches_sum(), 1);
2173            },
2174        );
2175    }
2176
2177    #[cfg(feature = "kotlin")]
2178    #[test]
2179    fn kotlin_primary_constructor_superclass_call_is_a_branch() {
2180        // Kotlin's *primary*-constructor superclass call is a
2181        // `constructor_invocation` under a `delegation_specifier` — a third
2182        // production, distinct from both `CallExpression` and the
2183        // `ConstructorDelegationCall` #1279 added, so it scored zero while
2184        // the secondary form beside it scored one (#1384). An object
2185        // expression's superclass call uses the same production.
2186        //
2187        // `class Plain : Marker` is the negative case: a delegation
2188        // specifier with no argument list is a plain `user_type`, so it
2189        // must stay at zero. Broadening the arm to bare
2190        // `DelegationSpecifier` fails here at `branches_sum` (5 against
2191        // 3), though not on `Plain`'s account alone: `class Classic :
2192        // Base` below is argument-less too, so deleting `Plain` still
2193        // leaves the broadening failing, at 4. What `Plain` buys is the
2194        // case stated outright rather than riding on the contrast fixture,
2195        // and since the per-space assertions below name it, trimming it
2196        // from the fixture fails in `child_space` instead of passing
2197        // silently as it did while this test read only the sum.
2198        //
2199        // `class Classic` carries the secondary spelling of the same call
2200        // alongside, which is what makes the per-space assertions below a
2201        // contrast rather than a census: a class header's superclass call
2202        // sits in `delegation_specifiers`, outside every member, so no
2203        // function space encloses it and the branch is the **class**
2204        // space's, while `constructor(x: Int) : super(x)` opens a function
2205        // space and takes the branch with it. `branches_sum` cannot see the
2206        // difference — the file totals 3 either way — so a codebase moving
2207        // to primary constructors shifts per-function `branches_max` /
2208        // `branches_average` and per-space `abc` thresholds without any
2209        // behaviour change. That is deliberate (there is no function to
2210        // attribute the call to), and C# has the identical split, pinned by
2211        // `csharp_base_call_and_constructor_initializer_do_not_double_count`
2212        // (#1456).
2213        //
2214        // expected: 3 branches — `Base(1, 2)`, `Base(3)` and `super(x)`.
2215        // Nothing else in the fixture is a call, so deleting any one
2216        // construction from the fixture moves both the total and the space
2217        // it belongs to.
2218        check_func_space::<KotlinParser, _>(
2219            "class Sub : Base(1, 2) { }
2220             class Plain : Marker { }
2221             fun make(): Any = object : Base(3) { }
2222             class Classic : Base {
2223                 constructor(x: Int) : super(x) { }
2224             }",
2225            "foo.kt",
2226            |space| {
2227                assert_eq!(space.metrics.abc.branches_sum(), 3);
2228                assert_eq!(
2229                    child_space(&space, "Sub").metrics.abc.branches(),
2230                    1,
2231                    "the primary-constructor superclass call is the class space's"
2232                );
2233                assert_eq!(
2234                    child_space(&space, "Plain").metrics.abc.branches(),
2235                    0,
2236                    "a delegation specifier with no argument list is not a call"
2237                );
2238                // The object expression is its own Class space (#463), so
2239                // `make` holds nothing of its own — the same attribution
2240                // one level down.
2241                let make = child_space(&space, "make");
2242                assert_eq!(
2243                    make.metrics.abc.branches(),
2244                    0,
2245                    "`make` owns no branch; `object : Base(3)` is its own space"
2246                );
2247                assert_eq!(
2248                    child_space(make, "<anonymous>").metrics.abc.branches(),
2249                    1,
2250                    "the object expression's superclass call is that space's"
2251                );
2252                // The contrast. `Classic` spells the same call through a
2253                // secondary constructor, which *does* open a function
2254                // space, so the class owns nothing.
2255                let classic = child_space(&space, "Classic");
2256                assert_eq!(
2257                    classic.metrics.abc.branches(),
2258                    0,
2259                    "a secondary constructor's `: super(x)` is not the class's"
2260                );
2261                assert_eq!(
2262                    child_space(classic, "<anonymous>").metrics.abc.branches(),
2263                    1,
2264                    "it belongs to the constructor's own function space"
2265                );
2266            },
2267        );
2268    }
2269
2270    #[cfg(feature = "kotlin")]
2271    #[test]
2272    fn kotlin_annotation_arguments_are_not_a_branch() {
2273        // The parent gate on that arm is load-bearing, not decoration:
2274        // tree-sitter-kotlin-ng spells an annotation's argument list with
2275        // the *same* `constructor_invocation` production — `@Mark("a")`
2276        // parses as `annotation > constructor_invocation` and
2277        // `@file:Suppress(…)` as `file_annotation > constructor_invocation`.
2278        // Ungated, every argument-carrying annotation in a Kotlin file
2279        // would bill a branch, and annotations are everywhere (#1384).
2280        //
2281        // The fixture pairs three annotation spellings — file-level,
2282        // class-level and use-site-targeted — with one real superclass
2283        // call, so the total discriminates in both directions: 1 with the
2284        // gate, 4 without it, 0 without the arm at all.
2285        //
2286        // A correctly-excluded annotation contributes to no ABC axis, so
2287        // `branches_sum()` alone cannot notice the annotations being
2288        // trimmed out of the fixture — measured: dropping any of the three
2289        // leaves this test green, and with all three gone, removing the
2290        // production gate fails nothing. The node census below is the
2291        // anchor: it pins that the fixture really does hand the arm four
2292        // `constructor_invocation` nodes for it to score 1 out of, and
2293        // that the `file_annotation` parent — the spelling a denylist of
2294        // `Annotation` alone would have missed — is among them.
2295        //
2296        // expected: 1 branch — `Base(1)` only.
2297        let src = r#"@file:Suppress("unused")
2298             @Mark("a")
2299             class Ann : Base(1) {
2300                 @get:Mark("b")
2301                 val v: Int = 0
2302             }"#;
2303        let parser = KotlinParser::new(
2304            src.as_bytes().to_vec(),
2305            &std::path::PathBuf::from("foo.kt"),
2306            None,
2307        );
2308        assert_eq!(
2309            parser
2310                .root()
2311                .preorder()
2312                .filter(|n| n.kind_id() == Kotlin::ConstructorInvocation as u16)
2313                .count(),
2314            4,
2315            "fixture must keep three annotations plus the superclass call"
2316        );
2317        assert!(ast_has_kind_id(&parser, Kotlin::FileAnnotation as u16));
2318
2319        check_metrics::<KotlinParser>(src, "foo.kt", |metric| {
2320            assert_eq!(metric.abc.branches_sum(), 1);
2321        });
2322    }
2323
2324    #[cfg(feature = "groovy")]
2325    #[test]
2326    fn groovy_constructor_delegation_is_a_branch() {
2327        // Groovy already counted this shape before #1279; the assertion
2328        // pins the JVM-family parity the Java and Kotlin fixes restore.
2329        // expected: 1 branch — the `super(1)` delegation.
2330        check_metrics::<GroovyParser>(
2331            "class Sub extends Base {
2332                Sub() { super(1) }
2333            }",
2334            "foo.groovy",
2335            |metric| {
2336                assert_eq!(metric.abc.branches_sum(), 1);
2337            },
2338        );
2339    }
2340
2341    // The three tests below are one fix seen from three grammars (#1407):
2342    // an enum constant carrying constructor arguments is an object
2343    // construction and scored zero in every JVM-family language, which
2344    // #1279 and #1384 left behind because both looked at superclass
2345    // delegation only.
2346    //
2347    // Each fixture pairs an argument-carrying constant with a bare one in
2348    // the *same* enum, because that is the only shape that shows the gate
2349    // working: a fixture of only `A(1)` scores the same whether the arm is
2350    // gated on the argument list or matches every constant
2351    // (`.claude/rules/grammar-dispatch.md` §6). Each also carries a
2352    // constant whose argument is itself a call, which must score 2 rather
2353    // than 1 (§5).
2354    //
2355    // `branches_sum()` alone cannot notice the bare constant being trimmed
2356    // out — it contributes to no axis — so every fixture is anchored on a
2357    // node census that fails by name when a spelling is edited away
2358    // (`.claude/rules/testing.md`, "Perturb the fixture as well as the
2359    // production line"). The two are coupled in one helper for the same
2360    // reason `assert_kotlin_class_members` couples its pair: a branch
2361    // total added without the census reads as coverage and is not.
2362
2363    #[cfg(any(feature = "groovy", feature = "java", feature = "kotlin"))]
2364    fn assert_enum_branches<P: MetricSuite>(
2365        src: &str,
2366        path: &str,
2367        kinds: &[(u16, usize, &str)],
2368        branches: u64,
2369    ) {
2370        assert_fixture_spells::<P>(src, path, kinds);
2371        check_func_space::<P, _>(src, path, |space| {
2372            assert_eq!(space.metrics.abc.branches_sum(), branches);
2373        });
2374    }
2375
2376    #[cfg(feature = "java")]
2377    #[test]
2378    fn java_enum_constant_with_arguments_is_a_branch() {
2379        // `@SuppressWarnings("x") B` is the discriminating case for the
2380        // gate's *kind*, not just its presence: the annotation carries an
2381        // argument list of its own, so an arm gated on "has some argument
2382        // list anywhere below" would score it. It cannot satisfy this one
2383        // twice over — `annotation_argument_list` is a distinct kind id
2384        // from `argument_list`, and it hangs off the constant's `modifiers`
2385        // child rather than the constant itself. Both verified with `bca
2386        // dump`.
2387        //
2388        // expected: 4 branches — `A(1)`, `K(2)`, `D(…)`, and the `f()`
2389        // inside `D`'s argument list. `B` scores 0.
2390        let src = "class Outer {
2391                static int f() { return 1; }
2392                enum E {
2393                    A(1),
2394                    @SuppressWarnings(\"x\") B,
2395                    K(2),
2396                    D(f());
2397                    E(int v) {}
2398                }
2399            }";
2400        assert_enum_branches::<JavaParser>(
2401            src,
2402            "foo.java",
2403            &[
2404                (Java::EnumConstant as u16, 4, "enum constants"),
2405                (Java::ArgumentList as u16, 4, "argument lists"),
2406                (
2407                    Java::AnnotationArgumentList as u16,
2408                    1,
2409                    "`@SuppressWarnings`'s argument list",
2410                ),
2411                (Java::MethodInvocation as u16, 1, "`D`'s nested `f()` call"),
2412            ],
2413            4,
2414        );
2415    }
2416
2417    #[cfg(feature = "kotlin")]
2418    #[test]
2419    fn kotlin_enum_entry_with_arguments_is_a_branch() {
2420        // Kotlin needs a defaulted primary-constructor parameter to spell
2421        // both cases in one enum: `enum class E(val v: Int)` obliges every
2422        // entry to pass an argument, so `= 0` is what lets the bare `B`
2423        // sit beside `A(1)`. `enum class P` covers the other shape the gate
2424        // has to leave alone — an enum with no constructor at all, whose
2425        // entries can never carry an argument list.
2426        //
2427        // expected: 3 branches — `A(1)`, `C(…)`, and the `g()` inside `C`'s
2428        // argument list. `B`, `X` and `Y` score 0.
2429        let src = "fun g(): Int = 1
2430             enum class E(val v: Int = 0) { A(1), B, C(g()) }
2431             enum class P { X, Y }";
2432        assert_enum_branches::<KotlinParser>(
2433            src,
2434            "foo.kt",
2435            &[
2436                (Kotlin::EnumEntry as u16, 5, "enum entries"),
2437                (Kotlin::ValueArguments as u16, 3, "argument lists"),
2438                (Kotlin::CallExpression as u16, 1, "`C`'s nested `g()` call"),
2439            ],
2440            3,
2441        );
2442    }
2443
2444    #[cfg(feature = "groovy")]
2445    #[test]
2446    fn groovy_enum_constant_with_arguments_is_a_branch() {
2447        // Groovy's enum has no annotated-constant case to cover: the
2448        // dekobon grammar cannot parse `@Deprecated A(1)` inside an enum
2449        // body and recovers into `ERROR` nodes, emitting no `enum_constant`
2450        // at all. The remaining two cases are the ones that matter.
2451        //
2452        // expected: 3 branches — `A(1)`, `D(…)`, and the `Outer.f()` inside
2453        // `D`'s argument list. `B` scores 0.
2454        let src = "class Outer { static int f() { 1 } }
2455             enum E { A(1), B, D(Outer.f()) }";
2456        assert_enum_branches::<GroovyParser>(
2457            src,
2458            "foo.groovy",
2459            &[
2460                (Groovy::EnumConstant as u16, 3, "enum constants"),
2461                (Groovy::ArgumentList as u16, 3, "argument lists"),
2462                (
2463                    Groovy::MethodInvocation as u16,
2464                    1,
2465                    "`D`'s nested `Outer.f()` call",
2466                ),
2467            ],
2468            3,
2469        );
2470    }
2471
2472    #[cfg(feature = "groovy")]
2473    #[test]
2474    fn groovy_no_abc() {
2475        // Comment-only file has no executable code → all-zero ABC.
2476        check_metrics::<GroovyParser>(
2477            "// just a comment, no executable code",
2478            "foo.groovy",
2479            |metric| {
2480                assert_eq!(metric.abc.assignments_sum(), 0);
2481                assert_eq!(metric.abc.branches_sum(), 0);
2482                assert_eq!(metric.abc.conditions_sum(), 0);
2483            },
2484        );
2485    }
2486
2487    #[cfg(feature = "groovy")]
2488    #[test]
2489    fn groovy_single_assignment() {
2490        // `int x = 1` is a local-variable declaration whose `=` counts
2491        // as one assignment (matches Java's semantics).
2492        check_metrics::<GroovyParser>("int x = 1", "foo.groovy", |metric| {
2493            assert_eq!(metric.abc.assignments_sum(), 1);
2494            assert_eq!(metric.abc.branches_sum(), 0);
2495            assert_eq!(metric.abc.conditions_sum(), 0);
2496        });
2497    }
2498
2499    #[cfg(feature = "groovy")]
2500    #[test]
2501    fn groovy_assignments() {
2502        check_metrics::<GroovyParser>(
2503            "void f() {
2504                int a = 1
2505                int b = 2
2506                a = 3
2507                b = 4
2508                a += 1
2509                b -= 1
2510            }",
2511            "foo.groovy",
2512            |metric| {
2513                // Six `=` tokens total. The two `Final`-less local
2514                // var-decls (`int a = 1`, `int b = 2`) and the two
2515                // bare assignments (`a = 3`, `b = 4`) each contribute
2516                // one assignment via the `EQ` arm; the `+=` / `-=`
2517                // each contribute one via the compound-assign arm.
2518                assert_eq!(metric.abc.assignments_sum(), 6);
2519            },
2520        );
2521    }
2522
2523    #[cfg(feature = "groovy")]
2524    #[test]
2525    fn groovy_branches() {
2526        check_metrics::<GroovyParser>(
2527            "void f() {
2528                doStuff()
2529                helper.invoke()
2530                new Worker()
2531            }",
2532            "foo.groovy",
2533            |metric| {
2534                // 2 method invocations + 1 object creation = 3 branches
2535                assert_eq!(metric.abc.branches_sum(), 3);
2536            },
2537        );
2538    }
2539
2540    #[cfg(feature = "groovy")]
2541    #[test]
2542    fn groovy_conditions_in_if() {
2543        check_metrics::<GroovyParser>(
2544            "void f(int a) {
2545                if (a == 0) { println(a) }
2546                if (a >= 1) { println(a) }
2547                if (a != 2) { println(a) }
2548            }",
2549            "foo.groovy",
2550            |metric| {
2551                // Three relational ops = 3 conditions
2552                assert_eq!(metric.abc.conditions_sum(), 3);
2553            },
2554        );
2555    }
2556
2557    #[cfg(feature = "groovy")]
2558    #[test]
2559    fn groovy_branches_with_juxt_call() {
2560        // Groovy's parens-less call form `println foo` must be counted
2561        // as a branch (`JuxtFunctionCall`).
2562        check_metrics::<GroovyParser>(
2563            "void f() {
2564                println 'hi'
2565                println 'bye'
2566            }",
2567            "foo.groovy",
2568            |metric| {
2569                // 2 juxt calls = 2 branches.
2570                assert_eq!(metric.abc.branches_sum(), 2);
2571            },
2572        );
2573    }
2574
2575    #[cfg(feature = "groovy")]
2576    #[test]
2577    fn groovy_try_catch_conditions() {
2578        // Each `try` and `catch` keyword token contributes +1 to
2579        // conditions (mirrors Java).
2580        check_metrics::<GroovyParser>(
2581            "void f() {
2582                try {
2583                    risky()
2584                } catch (Exception e) {
2585                    handle(e)
2586                }
2587            }",
2588            "foo.groovy",
2589            |metric| {
2590                // try + catch = 2 conditions
2591                assert_eq!(metric.abc.conditions_sum(), 2);
2592            },
2593        );
2594    }
2595
2596    #[cfg(feature = "groovy")]
2597    #[test]
2598    fn groovy_ternary_conditions() {
2599        check_metrics::<GroovyParser>(
2600            "void f(int x) {
2601                def y = x > 0 ? 1 : 2
2602            }",
2603            "foo.groovy",
2604            |metric| {
2605                // QMARK alone is +1 condition, plus the `>` condition = 2.
2606                assert_eq!(metric.abc.conditions_sum(), 2);
2607            },
2608        );
2609    }
2610
2611    #[cfg(feature = "groovy")]
2612    #[test]
2613    fn groovy_constant_excluded_from_assignments() {
2614        // `final` declarations are not counted as assignments
2615        // (mirrors Java's `Final` handling).
2616        check_metrics::<GroovyParser>(
2617            "class A {
2618                final int CONST = 42
2619                int field = 0
2620            }",
2621            "foo.groovy",
2622            |metric| {
2623                // The `=` on `final int CONST = 42` is a constant
2624                // initialiser (skipped). Only `field = 0` counts.
2625                assert_eq!(metric.abc.assignments_sum(), 1);
2626            },
2627        );
2628    }
2629
2630    #[cfg(feature = "groovy")]
2631    #[test]
2632    fn groovy_malformed_parenthesized_no_panic() {
2633        // Regression: malformed Groovy input must not panic the ABC
2634        // walker; the `spaces.rs` Unit fallback (lesson 9) covers
2635        // structural recovery. amaanq's grammar treats `def x = (((`
2636        // as a `local_variable_declaration` whose initialiser is the
2637        // first opening paren — the `=` still fires the assignment
2638        // arm.
2639        check_metrics::<GroovyParser>("def x = (((", "foo.groovy", |metric| {
2640            assert_eq!(metric.abc.assignments_sum(), 1);
2641        });
2642    }
2643
2644    #[cfg(feature = "groovy")]
2645    #[test]
2646    fn groovy_bool_returning_terminal_kinds_count() {
2647        // Companion to `csharp_bool_returning_terminal_kinds_count`
2648        // (issue #372 / lesson #19). The dekobon Groovy grammar
2649        // shares Java's wrapping conventions for `FieldAccess` and
2650        // `InstanceofExpression`, but it splits casts into two
2651        // distinct kinds — `cast_expression` for the Groovy-idiomatic
2652        // `v as Boolean` and `parenthesized_type_cast` for the
2653        // Java-style `(boolean) v`. The grammar has no `await` or
2654        // `array_access` analogues, so the C# fix's five-kind set
2655        // collapses to four here (with the cast slot doubled).
2656        //
2657        // `instanceof` has since moved out of the terminal set to an
2658        // unconditional arm (#1461), so it no longer depends on the
2659        // slot to be counted. The total is unchanged, which is the
2660        // point of that change rather than an accident of it.
2661        //
2662        // expected: 4 conditions (one per `if`), 0 assignments,
2663        // 0 branches (no invocations).
2664        check_metrics::<GroovyParser>(
2665            "class Cfg { boolean flag }
2666            class A {
2667                void m(Object v, Cfg cfg) {
2668                    if (cfg.flag) { }
2669                    if ((boolean) v) { }
2670                    if (v as Boolean) { }
2671                    if (v instanceof Cfg) { }
2672                }
2673            }",
2674            "foo.groovy",
2675            |metric| {
2676                assert_eq!(metric.abc.conditions_sum(), 4);
2677                assert_eq!(metric.abc.assignments_sum(), 0);
2678                assert_eq!(metric.abc.branches_sum(), 0);
2679            },
2680        );
2681    }
2682
2683    // Groovy half of #1274 — see `java_generic_declarations_are_not_conditions`.
2684    // The class-level `type_parameters` and its bound's nested
2685    // `type_arguments` mirror the Java fixture; pre-fix this file scored
2686    // 2 conditions, expected 0.
2687    #[cfg(feature = "groovy")]
2688    #[test]
2689    fn groovy_generic_declarations_are_not_conditions() {
2690        check_metrics::<GroovyParser>(
2691            "class Gen<T extends Comparable<T>> {
2692                List<T> pick(Map<String, List<T>> m) { m.get(\"k\") }
2693            }",
2694            "foo.groovy",
2695            |metric| {
2696                assert_eq!(metric.abc.conditions_sum(), 0);
2697                // Non-vacuity guard — see the Java counterpart.
2698                assert_eq!(metric.abc.branches_sum(), 1);
2699            },
2700        );
2701    }
2702
2703    // Groovy counterpart of `java_generic_wildcard_is_not_a_condition`,
2704    // including its choice of a non-zero expected total and its
2705    // two-wildcards-one-ternary shape; the dekobon grammar emits the
2706    // same `wildcard` node. Pre-fix: 4.
2707    #[cfg(feature = "groovy")]
2708    #[test]
2709    fn groovy_generic_wildcard_is_not_a_condition() {
2710        check_metrics::<GroovyParser>(
2711            "class A {
2712                int m(List<? extends Number> xs, Map<String, ? extends Number> ys,
2713                      int a, int b) { a < b ? 1 : 2 }
2714            }",
2715            "foo.groovy",
2716            |metric| {
2717                assert_eq!(metric.abc.conditions_sum(), 2);
2718            },
2719        );
2720    }
2721
2722    // A generic *method* gets its own fixture because the dekobon
2723    // grammar gives it its own production: `def <U> U ident(U x)` emits
2724    // `method_type_parameters`, not the `type_parameters` the class form
2725    // above uses. It is the fourth and last production from which that
2726    // grammar emits a bare `<` / `>` (the others being
2727    // `binary_expression`, `type_arguments` and `type_parameters`), and
2728    // the one a denylist extended only with `TypeParameters` — the fix
2729    // this issue's own plan proposed — would still miss. Revert-verified
2730    // in both directions: under that denylist this test still fails
2731    // while `groovy_generic_declarations_are_not_conditions` passes.
2732    //
2733    // The body carries a ternary over a comparison for the same
2734    // non-vacuity reason as the wildcard tests: expected 2, pre-fix 4
2735    // (the `<U>` bracket pair), 0 if the fixture stops parsing.
2736    #[cfg(feature = "groovy")]
2737    #[test]
2738    fn groovy_method_type_parameters_are_not_conditions() {
2739        check_metrics::<GroovyParser>(
2740            "class A {
2741                def <U> U ident(U x, int a, int b) { a < b ? x : null }
2742            }",
2743            "foo.groovy",
2744            |metric| {
2745                assert_eq!(metric.abc.conditions_sum(), 2);
2746            },
2747        );
2748    }
2749
2750    // Groovy counterpart of
2751    // `java_comparison_operators_still_count_alongside_generics`: the
2752    // narrowed arm keeps counting real comparisons, pinned against the
2753    // cyclomatic decision count on the method's own space (§8).
2754    #[cfg(feature = "groovy")]
2755    #[test]
2756    fn groovy_elvis_counts_one_condition_per_token() {
2757        // `a ?: c` is a short-circuit decision Groovy cyclomatic already
2758        // counts and the Kotlin ABC arm counts for its identical token;
2759        // Groovy's arm listed neither `?:` nor its `elvis_expression`, so
2760        // a method whose only branching is elvis chains reported
2761        // cyclomatic > 1 with zero conditions. One per token, so the
2762        // grammar-dispatch §8 invariant holds on the chain:
2763        // `conditions() == cyclomatic() - 1`.
2764        check_func_space::<GroovyParser, _>(
2765            "class K { def f(a, c) { return a ?: c } }",
2766            "foo.groovy",
2767            |space| assert_deepest_conditions_match_cyclomatic(&space, 1),
2768        );
2769        check_func_space::<GroovyParser, _>(
2770            "class K { def g(a, b, c) { return a ?: b ?: c } }",
2771            "foo.groovy",
2772            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
2773        );
2774    }
2775
2776    #[cfg(feature = "groovy")]
2777    #[test]
2778    fn groovy_comparison_operators_still_count_alongside_generics() {
2779        check_func_space::<GroovyParser, _>(
2780            "class A {
2781                int m(List<String> xs, int a, int b) {
2782                    if (a < b) { return 1 }
2783                    if (a > b) { return 2 }
2784                    return 0
2785                }
2786            }",
2787            "foo.groovy",
2788            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
2789        );
2790    }
2791
2792    #[cfg(feature = "groovy")]
2793    #[test]
2794    fn groovy_if_multiple_conditions() {
2795        // Mirrors `java_if_multiple_conditions`: `&&` / `||` chains
2796        // and parenthesised unary forms each contribute one
2797        // condition per primitive comparison; the inspect-container
2798        // pass picks up the unary `!a` / `!b` arguments inside the
2799        // `BinaryExpression` and counts them too.
2800        check_metrics::<GroovyParser>(
2801            "void f(boolean a, boolean b, boolean c) {
2802                if (a || b || c) { println(a) }
2803                if (a && b && c) { println(a) }
2804                if (!a && !b) { println(a) }
2805            }",
2806            "foo.groovy",
2807            |metric| {
2808                // Conditions counted via the AMPAMP/PIPEPIPE arms
2809                // (one count per identifier in the chain — three
2810                // for `||`, three for `&&`, two for the unary chain)
2811                // = 8.
2812                assert_eq!(metric.abc.conditions_sum(), 8);
2813                // Three `println a` juxt calls — each is a branch.
2814                assert_eq!(metric.abc.branches_sum(), 3);
2815            },
2816        );
2817    }
2818
2819    #[cfg(feature = "groovy")]
2820    #[test]
2821    fn groovy_while_and_do_while_conditions() {
2822        // Covers the WhileStatement and DoStatement arms in
2823        // `impl Abc for GroovyCode`. Each `while` / `do-while` has
2824        // its condition scored through `groovy_count_condition`.
2825        check_metrics::<GroovyParser>(
2826            "void f(boolean a, boolean b) {
2827                while (a) {
2828                    a = false
2829                }
2830                do {
2831                    b = !b
2832                } while (b)
2833            }",
2834            "foo.groovy",
2835            |metric| {
2836                // `while(a)` + `while(b)` each contribute one condition;
2837                // the unary `!b` on the do body's right-hand side adds
2838                // one more via the assignment-arm inspection = 3.
2839                assert_eq!(metric.abc.conditions_sum(), 3);
2840                // Two assignments to existing variables (`a = false`,
2841                // `b = !b`).
2842                assert_eq!(metric.abc.assignments_sum(), 2);
2843            },
2844        );
2845    }
2846
2847    #[cfg(feature = "groovy")]
2848    #[test]
2849    fn groovy_if_while_boolean_literal_condition() {
2850        // Regression for the Groovy half of #371-class bugs: the
2851        // dekobon tree-sitter-groovy grammar wraps a bare
2852        // `true` / `false` literal used as the condition of
2853        // `if` / `while` / `do` / `?:` in a `boolean_literal` node
2854        // (`Groovy::BooleanLiteral`, kind_id 270), not the leaf
2855        // `True` / `False` keyword tokens. `groovy_count_condition`
2856        // must therefore match `BooleanLiteral` (the wrapper).
2857        // Without that, every literal-condition statement silently
2858        // scored 0 conditions. Mirror of
2859        // `csharp_if_while_boolean_literal_condition`.
2860        check_metrics::<GroovyParser>(
2861            "void m() {
2862                if (true) { println 'a' }
2863                if (false) { println 'b' }
2864                while (true) { break }
2865                int t = true ? 1 : 0
2866            }",
2867            "foo.groovy",
2868            |metric| {
2869                // Four literal-condition statements contribute 4
2870                // `BooleanLiteral` conditions (if / if / while /
2871                // ternary), plus the ternary's `?` token adds one
2872                // more via `groovy_count_token_condition` → 5
2873                // total. The `println` calls contribute 2 branches
2874                // (the `while` body's `break` is not a branch).
2875                // The `int t = …` initializer contributes 1
2876                // assignment.
2877                assert_eq!(metric.abc.conditions_sum(), 5);
2878                assert_eq!(metric.abc.branches_sum(), 2);
2879                assert_eq!(metric.abc.assignments_sum(), 1);
2880            },
2881        );
2882    }
2883
2884    #[cfg(feature = "groovy")]
2885    #[test]
2886    fn groovy_return_unary_boolean_literal() {
2887        // Companion to `groovy_if_while_boolean_literal_condition`:
2888        // a `!true` / `!false` operand inside a `return` statement
2889        // routes through `groovy_count_negated`. The `!` proves
2890        // boolean context, and a `BooleanLiteral` does not score
2891        // itself (`groovy_condition_scores_itself`), so the
2892        // negated operand pays one condition.
2893        check_metrics::<GroovyParser>(
2894            "boolean f() {
2895                return !true
2896            }
2897            boolean g() {
2898                return !false
2899            }",
2900            "foo.groovy",
2901            |metric| {
2902                // Each `return !X` walks into `groovy_count_negated`
2903                // with a UnaryExpression wrapping a `BANG` +
2904                // BooleanLiteral. The peel through `!` proves boolean
2905                // context (a `return` is not a slot), then the
2906                // BooleanLiteral operand contributes one condition.
2907                // Two `return !X` → 2 conditions, no branches, no
2908                // assignments.
2909                assert_eq!(metric.abc.conditions_sum(), 2);
2910                assert_eq!(metric.abc.branches_sum(), 0);
2911                assert_eq!(metric.abc.assignments_sum(), 0);
2912            },
2913        );
2914    }
2915
2916    #[cfg(feature = "groovy")]
2917    #[test]
2918    fn groovy_short_circuit_with_boolean_literal_operand() {
2919        // Companion to `groovy_if_while_boolean_literal_condition`:
2920        // a bare `true` / `false` operand of `&&` / `||` is a
2921        // chain-operand slot scored through `groovy_count_condition`.
2922        // A `BooleanLiteral` does not score itself, so the slot pays
2923        // for it; otherwise the operand would silently score zero.
2924        check_metrics::<GroovyParser>(
2925            "void m(boolean x) {
2926                if (x && true) { println 'a' }
2927                if (false || x) { println 'b' }
2928            }",
2929            "foo.groovy",
2930            |metric| {
2931                // `&&` and `||` themselves are NOT in
2932                // `groovy_count_token_condition`'s match list —
2933                // they route through
2934                // `groovy_walk_for_conditions::AMPAMP|PIPEPIPE`,
2935                // which scores each operand of the parent
2936                // BinaryExpression through `groovy_count_condition`.
2937                // An operand pays one condition unless it scores
2938                // itself. For `x && true`: Identifier x
2939                // (+1) + BooleanLiteral true (+1) = 2. For
2940                // `false || x`: BooleanLiteral false (+1) +
2941                // Identifier x (+1) = 2. Total 4.
2942                assert_eq!(metric.abc.conditions_sum(), 4);
2943                assert_eq!(metric.abc.branches_sum(), 2);
2944                assert_eq!(metric.abc.assignments_sum(), 0);
2945            },
2946        );
2947    }
2948
2949    #[cfg(feature = "groovy")]
2950    #[test]
2951    fn groovy_methods_arguments_with_conditions() {
2952        // Mirror of `java_methods_arguments_with_conditions`: a
2953        // unary `!x` inside an argument list must count both the
2954        // method invocation as a branch AND the unary as a
2955        // condition. The `ArgumentList | ArgumentList2` arm in
2956        // `impl Abc for GroovyCode` is what exercises this.
2957        check_metrics::<GroovyParser>(
2958            "void f(boolean a, boolean b, boolean c) {
2959                m1(a)
2960                m1(!a)
2961                m2(!a, !b)
2962            }",
2963            "foo.groovy",
2964            |metric| {
2965                // 3 method invocations (m1, m1, m2) — each fires the
2966                // branches arm.
2967                assert_eq!(metric.abc.branches_sum(), 3);
2968                // Three `!` unaries — `m1(!a)` and the two args of
2969                // `m2(!a, !b)` — each contribute one condition via
2970                // the ArgumentList inspection.
2971                assert_eq!(metric.abc.conditions_sum(), 3);
2972            },
2973        );
2974    }
2975
2976    #[cfg(feature = "groovy")]
2977    #[test]
2978    fn groovy_return_with_conditions() {
2979        // Mirror of `java_return_with_conditions`: a parenthesised
2980        // or unary expression inside `return` flows through the
2981        // `ReturnStatement` arm to `groovy_count_negated`.
2982        check_metrics::<GroovyParser>(
2983            "boolean f(boolean a) {
2984                return (a)
2985            }
2986            boolean g(boolean a) {
2987                return !a
2988            }",
2989            "foo.groovy",
2990            |metric| {
2991                // Only one of the two return forms surfaces a
2992                // condition: `return !a` hits the UnaryExpression
2993                // path and adds one; `return (a)` reaches
2994                // `groovy_count_negated` but no `!` proves the inner
2995                // identifier `a` boolean, so no condition is added.
2996                assert_eq!(metric.abc.conditions_sum(), 1);
2997            },
2998        );
2999    }
3000
3001    #[cfg(feature = "groovy")]
3002    #[test]
3003    fn groovy_for_with_variable_declaration() {
3004        // Classical `for (int i = 0; cond; i++)` form. The init
3005        // slot's `int i = 0` is suppressed from assignments by the
3006        // `LocalVariableDeclaration` push/pop dance; the `i++` in
3007        // the update slot contributes one assignment via the
3008        // `PLUSPLUS` arm. The condition `i < 10` flows through the
3009        // `ForStatement` arm.
3010        check_metrics::<GroovyParser>(
3011            "void f() {
3012                for (int i = 0; i < 10; i++) {
3013                    println(i)
3014                }
3015            }",
3016            "foo.groovy",
3017            |metric| {
3018                // `int i = 0` fires the EQ arm + `i++` fires the
3019                // PLUSPLUS arm = 2 assignments.
3020                assert_eq!(metric.abc.assignments_sum(), 2);
3021                // `i < 10` is one condition (the LT arm).
3022                assert_eq!(metric.abc.conditions_sum(), 1);
3023            },
3024        );
3025    }
3026
3027    /// The existing `for` test uses `i < 10`, which the `LT` token arm
3028    /// counts on its own — `groovy_walk_for_statement` never
3029    /// contributes there, so it stayed uncovered. A bare-identifier
3030    /// condition has no comparison token, so the count can only come
3031    /// from the walker.
3032    #[cfg(feature = "groovy")]
3033    #[test]
3034    fn groovy_for_with_bare_identifier_condition() {
3035        check_metrics::<GroovyParser>(
3036            "void f(boolean go) {
3037                for (int i = 0; go; i++) {
3038                    println(i)
3039                }
3040            }",
3041            "foo.groovy",
3042            |metric| {
3043                // `go` is the whole condition and counts once.
3044                assert_eq!(metric.abc.conditions_sum(), 1);
3045                // `int i = 0` (EQ) + `i++` (PLUSPLUS) = 2, as in
3046                // `groovy_for_with_variable_declaration`.
3047                assert_eq!(metric.abc.assignments_sum(), 2);
3048            },
3049        );
3050    }
3051
3052    /// The same slot with the initialiser hoisted out of the header.
3053    /// Under the pre-#1276 positional cascade this was a distinct code
3054    /// path — the condition moved from child(4) to child(3) — and the
3055    /// pair is kept as a shape guard now that the walker reads the
3056    /// `condition` field and cannot see the difference.
3057    #[cfg(feature = "groovy")]
3058    #[test]
3059    fn groovy_for_with_empty_initializer_counts_the_condition() {
3060        check_metrics::<GroovyParser>(
3061            "void f(boolean go) {
3062                int i = 0
3063                for (; go; i++) {
3064                    println(i)
3065                }
3066            }",
3067            "foo.groovy",
3068            |metric| {
3069                assert_eq!(metric.abc.conditions_sum(), 1);
3070                // `int i = 0` (EQ) + `i++` (PLUSPLUS), as above — the
3071                // initialiser just moved out of the loop header.
3072                assert_eq!(metric.abc.assignments_sum(), 2);
3073            },
3074        );
3075    }
3076
3077    /// Issue #1276 changed Groovy's answer here, exactly as it changed
3078    /// Java's: the positional cascade counted a `;` / `)` landing at
3079    /// child(4) as a vacuously-true condition, so `for (;;)` scored
3080    /// one. An omitted test is not a decision, and every other impl
3081    /// scores it zero. See `java_empty_for_condition_counts_nothing`.
3082    #[cfg(feature = "groovy")]
3083    #[test]
3084    fn groovy_empty_for_condition_counts_nothing() {
3085        check_metrics::<GroovyParser>("void f() { for (;;) { break } }", "foo.groovy", |metric| {
3086            assert_eq!(metric.abc.conditions_sum(), 0);
3087        });
3088        // The second spelling that moved. Unlike Java's, Groovy's
3089        // `local_variable_declaration` does not swallow its `;`, so the
3090        // old cascade found `;` at child(3) and `;` at child(4) here
3091        // too, and counted one.
3092        check_metrics::<GroovyParser>(
3093            "void f() { for (int i = 0; ; i++) { break } }",
3094            "foo.groovy",
3095            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
3096        );
3097    }
3098
3099    /// The cascade's other defect, shared with Java: a comment in the
3100    /// header shifted every child index, so the condition went unread.
3101    /// Reading the `condition` field cannot shift.
3102    #[cfg(feature = "groovy")]
3103    #[test]
3104    fn groovy_for_condition_survives_a_header_comment() {
3105        check_metrics::<GroovyParser>(
3106            "void f(boolean go) { for (; /* n */ go; ) { break } }",
3107            "foo.groovy",
3108            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
3109        );
3110        // Unchanged control: the same loop without the comment.
3111        check_metrics::<GroovyParser>(
3112            "void f(boolean go) { for (; go; ) { break } }",
3113            "foo.groovy",
3114            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
3115        );
3116    }
3117
3118    /// C#'s `csharp_walk_for_statement` reads the loop condition off
3119    /// the named `condition` field and scores a parenthesised or
3120    /// `!`-prefixed one through `csharp_count_condition`. Every other
3121    /// C# `for` test uses a comparison (`i < n`), which the `LT` token
3122    /// arm counts without entering the walker.
3123    #[cfg(feature = "csharp")]
3124    #[test]
3125    fn csharp_for_with_negated_condition() {
3126        check_metrics::<CsharpParser>(
3127            "class A {
3128                void M(bool done) {
3129                    for (int i = 0; !done; i++) { System.Console.WriteLine(i); }
3130                }
3131            }",
3132            "foo.cs",
3133            |metric| {
3134                // `!done` unwraps to the `done` terminal: one condition,
3135                // and no comparison token to double-count it.
3136                assert_eq!(metric.abc.conditions_sum(), 1);
3137                // `int i = 0` + `i++`.
3138                assert_eq!(metric.abc.assignments_sum(), 2);
3139                assert_eq!(metric.abc.branches_sum(), 1);
3140            },
3141        );
3142    }
3143
3144    #[cfg(feature = "groovy")]
3145    #[test]
3146    fn groovy_eq_arm_counts_outside_final_declarations() {
3147        // Bare reassignment of an already-declared variable: the `=`
3148        // belongs to no `final` declaration, so it counts. Mirrors
3149        // `java_eq_arm_counts_outside_final_declarations`.
3150        check_metrics::<GroovyParser>(
3151            "void f(int x) {
3152                x = 42
3153            }",
3154            "foo.groovy",
3155            |metric| {
3156                assert_eq!(metric.abc.assignments_sum(), 1);
3157                assert_eq!(metric.abc.branches_sum(), 0);
3158                assert_eq!(metric.abc.conditions_sum(), 0);
3159            },
3160        );
3161    }
3162
3163    #[cfg(feature = "groovy")]
3164    #[test]
3165    fn groovy_final_field_initializer_does_not_suppress_the_closure_body() {
3166        // The Groovy spelling of
3167        // `java_final_initializer_does_not_suppress_the_assignments_inside_it`:
3168        // a `final` field's closure body opens no space, and the sentinel
3169        // stack suppressed its `x = 1` with the declarator's own `=`.
3170        check_metrics::<GroovyParser>(
3171            "class K {
3172                int x
3173                final Closure c = { x = 1 }
3174                Closure d = { x = 2 }
3175                final int q = 3
3176            }",
3177            "foo.groovy",
3178            |metric| {
3179                // `x = 1`, `d = {…}`, `x = 2`; the two `final`
3180                // initializers are suppressed. Pre-fix: 2.
3181                assert_eq!(metric.abc.assignments_sum(), 3);
3182            },
3183        );
3184    }
3185
3186    #[cfg(feature = "groovy")]
3187    #[test]
3188    fn groovy_final_local_is_an_error_at_the_pinned_grammar() {
3189        // `groovy_eq_initializes_final_binding` lists
3190        // `local_variable_declaration` for symmetry with Java, but at
3191        // dekobon-tree-sitter-groovy 0.2.2 a `final` local never
3192        // reaches it: the parser emits an `ERROR` node for the
3193        // declaration, with or without a terminator, so both of its `=`
3194        // tokens count. This pins the grammar limitation so a bump that
3195        // starts parsing the local shows up here rather than as a silent
3196        // change in what the predicate suppresses.
3197        let source = b"class K { int x; def m() { final int a = 0; x = 1 } }";
3198        let parser = GroovyParser::new(
3199            source.to_vec(),
3200            &std::path::PathBuf::from("foo.groovy"),
3201            None,
3202        );
3203        assert!(
3204            ast_has_kind_id(&parser, u16::MAX),
3205            "a `final` local should still parse to an ERROR node; if the grammar \
3206             now accepts it, re-derive the local half of the predicate",
3207        );
3208        check_metrics::<GroovyParser>(
3209            "class K { int x; def m() { final int a = 0; x = 1 } }",
3210            "foo.groovy",
3211            |metric| {
3212                assert_eq!(metric.abc.assignments_sum(), 2);
3213            },
3214        );
3215    }
3216
3217    #[cfg(feature = "csharp")]
3218    #[test]
3219    fn csharp_const_initializer_shapes() {
3220        // `csharp_eq_initializes_const_binding` reads the `const`
3221        // through the `modifier` node one hop above the
3222        // `variable_declaration`, for a local and a field alike, behind
3223        // other modifiers; `readonly` is not `const` and its initializer
3224        // counts. A lambda's body opens its own space in C#, so unlike
3225        // Java there was never a nested `=` to leak — the row pins that
3226        // the structural rule keeps the count the sentinel gave.
3227        check_metrics::<CsharpParser>(
3228            "class K {
3229                int x;
3230                private const int Q = 1;
3231                public static readonly int R = 2;
3232                void M() { const int q = 3; System.Action a = () => { x = 4; }; }
3233            }",
3234            "foo.cs",
3235            |metric| {
3236                // `R = 2`, `a = () => …`, `x = 4`.
3237                assert_eq!(metric.abc.assignments_sum(), 3);
3238            },
3239        );
3240    }
3241
3242    #[cfg(feature = "csharp")]
3243    #[test]
3244    fn csharp_constant_declarations() {
3245        check_metrics::<CsharpParser>(
3246            "class A {
3247                private const int X1 = 0, Y1 = 0;
3248                public const float PI = 3.14f;
3249                const string HELLO = \"Hello,\";
3250                protected string world = \" world!\";
3251                public float e = 2.718f;
3252                private int x2 = 1, y2 = 2;
3253                void M() {
3254                    const int Z1 = 0, Z2 = 0, Z3 = 0;
3255                    const float T = 0.0f;
3256                    int z1 = 1, z2 = 2, z3 = 3;
3257                }
3258            }",
3259            "foo.cs",
3260            |metric| insta::assert_json_snapshot!(metric.abc),
3261        );
3262    }
3263
3264    #[cfg(feature = "csharp")]
3265    #[test]
3266    fn csharp_declarations_with_conditions() {
3267        check_metrics::<CsharpParser>(
3268            "class A {
3269                bool a = (1 == 2);
3270                bool b = (1 < 2);
3271                bool c = !true;
3272                bool d = !false;
3273            }",
3274            "foo.cs",
3275            |metric| insta::assert_json_snapshot!(metric.abc),
3276        );
3277    }
3278
3279    #[cfg(feature = "csharp")]
3280    #[test]
3281    fn csharp_assignments_with_conditions() {
3282        check_metrics::<CsharpParser>(
3283            "class A {
3284                void M() {
3285                    int a = 0;
3286                    a += 1;
3287                    a -= 2;
3288                    a *= 3;
3289                    a /= 4;
3290                    a %= 5;
3291                    a++;
3292                    a--;
3293                }
3294            }",
3295            "foo.cs",
3296            |metric| insta::assert_json_snapshot!(metric.abc),
3297        );
3298    }
3299
3300    #[cfg(feature = "csharp")]
3301    #[test]
3302    fn csharp_methods_arguments_with_conditions() {
3303        check_metrics::<CsharpParser>(
3304            "class A {
3305                void M(int x, int y) {
3306                    F(x == y, x < y, !x.Equals(y));
3307                }
3308                void F(bool a, bool b, bool c) {}
3309            }",
3310            "foo.cs",
3311            |metric| {
3312                // expected: `==` +1, `<` +1, and the negated argument
3313                // `!x.Equals(y)` +1 — it scored 0 until #1537, when every
3314                // C# argument reached the peel still in its `argument`
3315                // wrapper.
3316                assert_eq!(metric.abc.conditions_sum(), 3);
3317                insta::assert_json_snapshot!(metric.abc);
3318            },
3319        );
3320    }
3321
3322    // #1537: every C# argument is wrapped in an `argument` node that the
3323    // negation peel declined, so a negated argument scored 0 where every
3324    // other C-family language scores 1. Each row is a C# call next to its
3325    // Java twin, which takes the expected value from outside the fix.
3326    #[cfg(all(feature = "csharp", feature = "java"))]
3327    #[test]
3328    fn csharp_negated_call_argument_matches_java() {
3329        let rows = [
3330            ("G(!a);", "g(!a);", 1),
3331            ("G((!a));", "g((!a));", 1),
3332            ("G(!a, !b);", "g(!a, !b);", 2),
3333            ("G(a, !b);", "g(a, !b);", 1),
3334            ("new B(!a);", "new B(!a);", 1),
3335            ("G(-a);", "g(-a);", 0),
3336            // A negated comparison pays once, for its `>`; a chain pays
3337            // per operand. Neither argument may pay again as a negation.
3338            ("G(!(a > b));", "g(!(a > b));", 1),
3339            ("G(!a && b);", "g(!a && b);", 2),
3340        ];
3341        for (csharp, java, expected) in rows {
3342            let cs = abc_conditions(
3343                LANG::Csharp,
3344                &format!("class A {{ void M() {{ {csharp} }} }}"),
3345            );
3346            let jv = abc_conditions(LANG::Java, &format!("class A {{ void m() {{ {java} }} }}"));
3347            assert_eq!(jv, expected, "Java twin `{java}`");
3348            assert_eq!(cs, expected, "C# `{csharp}` against Java `{java}`");
3349        }
3350    }
3351
3352    // The C#-only spellings around an argument. The value is the
3353    // `argument` wrapper's last operand, so a named-argument label, a
3354    // passing-mode keyword or a comment before it must not hide it.
3355    #[cfg(feature = "csharp")]
3356    #[test]
3357    fn csharp_negated_call_argument_spellings() {
3358        let rows = [
3359            // A named argument: the value follows `name` and `:`.
3360            ("class A { void M() { G(name: !a); } }", 1),
3361            // A comment between the label and the value sits inside the
3362            // `argument` wrapper, so a positional read of the value lands
3363            // on it. A comment before or after a whole argument is a
3364            // sibling of the wrapper instead and never reaches this read.
3365            ("class A { void M() { G(name: /*c*/ !a); } }", 1),
3366            // `ref` / `out` / `in` precede a variable, never a negation. A
3367            // control only: 0 is also what the pre-#1537 walk scored, so
3368            // this row cannot fail for any defect the fix addresses.
3369            ("class A { void M() { G(ref x, out var y, in z); } }", 0),
3370            // The lambda arm pays for its `!a` body; the argument holding
3371            // the lambda is not a negation and must not pay again.
3372            ("class A { void M() { G(x => !a); } }", 1),
3373            // `: base(!a)` — a `constructor_initializer`'s list reaches the
3374            // walk like any call's.
3375            ("class A : B { A(bool a) : base(!a) {} }", 1),
3376            // The two primary-constructor base calls: the record nests its
3377            // list in a `primary_constructor_base_type`, the class's list
3378            // is the branch node itself and is scored on the branch path.
3379            ("record R(bool a) : Base(!a);", 1),
3380            ("class S(bool a) : Base(!a) {}", 1),
3381            // An indexer's `bracketed_argument_list` is not a call, as
3382            // Java's array index is not.
3383            ("class A { void M() { var q = o[!a]; } }", 0),
3384        ];
3385        for (src, expected) in rows {
3386            assert_eq!(abc_conditions(LANG::Csharp, src), expected, "{src}");
3387        }
3388    }
3389
3390    #[cfg(feature = "csharp")]
3391    #[test]
3392    fn csharp_if_single_conditions() {
3393        check_metrics::<CsharpParser>(
3394            "class A {
3395                void M(int x) {
3396                    if (x > 0) { System.Console.WriteLine(\"a\"); }
3397                    if (x < 0) { System.Console.WriteLine(\"b\"); }
3398                    if (x == 0) { System.Console.WriteLine(\"c\"); }
3399                }
3400            }",
3401            "foo.cs",
3402            |metric| insta::assert_json_snapshot!(metric.abc),
3403        );
3404    }
3405
3406    #[cfg(feature = "csharp")]
3407    #[test]
3408    fn csharp_if_multiple_conditions() {
3409        check_metrics::<CsharpParser>(
3410            "class A {
3411                void M(int x, int y) {
3412                    if (x > 0 && y > 0) { System.Console.WriteLine(\"a\"); }
3413                    if (x < 0 || y < 0) { System.Console.WriteLine(\"b\"); }
3414                }
3415            }",
3416            "foo.cs",
3417            |metric| insta::assert_json_snapshot!(metric.abc),
3418        );
3419    }
3420
3421    #[cfg(feature = "csharp")]
3422    #[test]
3423    fn csharp_while_and_do_while_conditions() {
3424        check_metrics::<CsharpParser>(
3425            "class A {
3426                void M(int x) {
3427                    while (x > 0) { x--; }
3428                    do { x++; } while (x < 10);
3429                }
3430            }",
3431            "foo.cs",
3432            |metric| insta::assert_json_snapshot!(metric.abc),
3433        );
3434    }
3435
3436    #[cfg(feature = "csharp")]
3437    #[test]
3438    fn csharp_return_with_conditions() {
3439        check_metrics::<CsharpParser>(
3440            "class A {
3441                bool M(int x) {
3442                    return (x > 0);
3443                }
3444                bool N(int x) {
3445                    return !(x < 0);
3446                }
3447            }",
3448            "foo.cs",
3449            |metric| insta::assert_json_snapshot!(metric.abc),
3450        );
3451    }
3452
3453    // C# `switch` *expression* arms scored zero ABC conditions before
3454    // #456 — they carry no `case` / `default` token, so the token-driven
3455    // `csharp_count_token_condition` never saw them, even though C#
3456    // cyclomatic counts each non-discard arm. Revert-verified: adding the
3457    // gated `SwitchExpressionArm` arm is what lifts this from 0 to 2. The
3458    // bare `_ =>` discard arm is excluded (the `default:` analogue),
3459    // mirroring the cyclomatic gate (lesson 11).
3460    #[cfg(feature = "csharp")]
3461    #[test]
3462    fn csharp_switch_expression_arm_counts_condition() {
3463        check_metrics::<CsharpParser>(
3464            "class A {
3465                int M(int x) {
3466                    return x switch { 1 => 10, 2 => 20, _ => 0 };
3467                }
3468            }",
3469            "foo.cs",
3470            |metric| {
3471                // arm `1 =>` (+1) + arm `2 =>` (+1) + `_ =>` discard (+0).
3472                assert_eq!(metric.abc.conditions_sum(), 2);
3473            },
3474        );
3475    }
3476
3477    // Cross-language parity (lesson 11): a C# `switch` expression and the
3478    // equivalent Java arrow-`switch` must report the same ABC condition
3479    // count on equivalent code. Both have two concrete case arms and no
3480    // fallback arm, so both must count exactly 2. `check_metrics` takes a
3481    // non-capturing `fn` pointer, so the shared expected value (2) is
3482    // asserted in each callback rather than compared across closures; the
3483    // matching constant is what enforces parity. This guards against the
3484    // C# fix drifting away from the Java arrow-case treatment.
3485    #[cfg(all(feature = "csharp", feature = "java"))]
3486    #[test]
3487    fn csharp_java_switch_arm_abc_parity() {
3488        // C# switch expression: two arms, no fallback → 2 conditions.
3489        check_metrics::<CsharpParser>(
3490            "class A {
3491                int M(int x) {
3492                    return x switch { 1 => 10, 2 => 20 };
3493                }
3494            }",
3495            "foo.cs",
3496            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3497        );
3498
3499        // Equivalent Java arrow-`switch`: two case arms, no default → 2.
3500        check_metrics::<JavaParser>(
3501            "class A {
3502                int m(int x) {
3503                    return switch (x) { case 1 -> 10; case 2 -> 20; };
3504                }
3505            }",
3506            "foo.java",
3507            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3508        );
3509    }
3510
3511    // Issue #469: the `default` arm of a C-family `switch` is the
3512    // unconditional fallthrough and must NOT count as an ABC condition,
3513    // mirroring cyclomatic — which counts only the `Case` arms, never
3514    // the `Default` token.
3515    //
3516    // expected: each fixture is a single function whose switch has two
3517    // concrete `case` arms plus one `default`. ABC must count exactly
3518    // the two case arms (conditions = 2), matching cyclomatic's two
3519    // case-arm decisions. Pre-fix, every language below scored 3 (the
3520    // `Default` token leaked into the condition tally) — revert-verified
3521    // against the pre-#469 condition arms. We anchor on the integer
3522    // `conditions_sum()` headline (the value the public JSON serializes;
3523    // float magnitude is bit-brittle and excluded by the snapshot
3524    // policy). The cyclomatic side is pinned separately in
3525    // `java_csharp_cpp_switch_default_cyclomatic_parity` below, where
3526    // the per-space `cyclomatic()` decision count is isolated.
3527    #[cfg(feature = "java")]
3528    #[test]
3529    fn java_switch_default_not_a_condition() {
3530        // Classic statement `default:`.
3531        check_metrics::<JavaParser>(
3532            "class A {
3533                int m(int x) {
3534                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3535                }
3536            }",
3537            "foo.java",
3538            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3539        );
3540        // Arrow `default ->` — shares the same `Default` token.
3541        check_metrics::<JavaParser>(
3542            "class A {
3543                int m(int x) {
3544                    return switch (x) { case 1 -> 1; case 2 -> 2; default -> 0; };
3545                }
3546            }",
3547            "foo.java",
3548            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3549        );
3550    }
3551
3552    // Over-exclusion guard (issue #469): a statement `switch` with two
3553    // `case` arms and NO `default` must still count both cases. This
3554    // pins that the fix excludes only the `Default` token, never a
3555    // `Case` arm — the count is identical before and after #469 (two
3556    // cases → 2), so it would catch a fix that over-eagerly dropped a
3557    // real case (e.g. treating the trailing case as a fallthrough).
3558    // expected: case 1 (+1) + case 2 (+1) = 2.
3559    #[cfg(feature = "java")]
3560    #[test]
3561    fn java_switch_without_default_counts_all_cases() {
3562        check_metrics::<JavaParser>(
3563            "class A {
3564                int m(int x) {
3565                    switch (x) { case 1: return 1; case 2: return 2; }
3566                    return -1;
3567                }
3568            }",
3569            "foo.java",
3570            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3571        );
3572    }
3573
3574    #[cfg(feature = "csharp")]
3575    #[test]
3576    fn csharp_switch_default_not_a_condition() {
3577        check_metrics::<CsharpParser>(
3578            "class A {
3579                int M(int x) {
3580                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3581                }
3582            }",
3583            "foo.cs",
3584            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3585        );
3586    }
3587
3588    /// Regression #1450 / #1451: `goto case 2;` spells the same `case`
3589    /// keyword token (id 114) as a real arm, from a second production
3590    /// (`goto_statement`), and scored an ABC condition it does not earn —
3591    /// an unconditional jump to an arm is not a decision.
3592    ///
3593    /// `jmp` differs from `ctl` by exactly one statement, so the
3594    /// `goto case` is the only thing that can separate their scores
3595    /// (`.claude/rules/grammar-dispatch.md` §11 — an arm and a
3596    /// `goto case` are independent paths into the same counter, and a
3597    /// fixture carrying only arms cannot show the gate works). `dfl` is
3598    /// the control that was already correct, by accident rather than
3599    /// design: `goto default;` spells `Default`, which neither metric
3600    /// counts because it is the switch's unconditional fallthrough
3601    /// (#456, #469).
3602    ///
3603    /// [`assert_every_member_scores`] covers cyclomatic as well as ABC,
3604    /// through its per-member §8 parity assertion, and would catch the
3605    /// gate being removed from *either* side alone: dropping ABC's puts
3606    /// `jmp` at conditions 3 against the asserted 2, and dropping
3607    /// cyclomatic's leaves conditions 2 against a decision count of 3.
3608    /// Removing both restores the parity at `3 == 4 - 1` and is caught by
3609    /// the conditions value, which is why 2 is named rather than derived.
3610    ///
3611    /// The `Case` anchor is the second axis. Every member scores 2 from
3612    /// its two arms alone, so deleting `goto case 2;` leaves every
3613    /// assertion below satisfied and the construct under test gone; the
3614    /// anchor's count of 7 — six arms plus the one jump — fails by name
3615    /// instead.
3616    #[cfg(feature = "csharp")]
3617    #[test]
3618    fn csharp_goto_case_is_not_a_condition() {
3619        let src = "class A {
3620                int ctl(int x) { switch (x) { case 1: return 1; case 2: return 2; default: return 0; } }
3621                int jmp(int x) { switch (x) { case 1: goto case 2; case 2: return 2; default: return 0; } }
3622                int dfl(int x) { switch (x) { case 1: goto default; case 2: return 2; default: return 0; } }
3623            }";
3624        assert_csharp_fixture_spells(
3625            src,
3626            &[
3627                (Csharp::Case as u16, 7, "`case` tokens (6 arms + 1 jump)"),
3628                (Csharp::SwitchSection as u16, 9, "switch arms"),
3629                (Csharp::GotoStatement as u16, 2, "`goto` statements"),
3630                (
3631                    Csharp::Default as u16,
3632                    4,
3633                    "`default` tokens (3 arms + 1 jump)",
3634                ),
3635            ],
3636        );
3637        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
3638            assert_every_member_scores(
3639                &space.spaces[0],
3640                3,
3641                2,
3642                "one condition per `case` arm, none for a `goto` into one",
3643            );
3644        });
3645    }
3646
3647    #[cfg(feature = "cpp")]
3648    #[test]
3649    fn cpp_switch_default_not_a_condition() {
3650        // C++ (and plain C, which shares this grammar) already excluded
3651        // `default`; this pins the cross-language parity invariant.
3652        check_metrics::<CppParser>(
3653            "void f(int x) {
3654                 switch (x) { case 1: return; case 2: return; default: return; }
3655             }",
3656            "foo.cpp",
3657            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3658        );
3659    }
3660
3661    #[cfg(feature = "objc")]
3662    #[test]
3663    fn objc_abc() {
3664        // ObjC ABC reuses the C/C++ walker with two additions: a message
3665        // send `[obj msg]` is a call (B), and `@try` / `@catch` count as
3666        // conditions (C) like C++ try/catch.
3667        //   A: `int total = 0`, `int i = 0`, `i++`, `total = total + …`,
3668        //      `total = -1` = 5.
3669        //   B: `[self valueAt:i]`, `[self risky]` = 2 message sends.
3670        //   C: `i < n`, `@try`, `@catch`, `total >= 0` = 4.
3671        check_metrics::<ObjcParser>(
3672            "@implementation Foo\n\
3673             - (int)bar:(int)n {\n\
3674                 int total = 0;\n\
3675                 for (int i = 0; i < n; i++) {\n\
3676                     total = total + [self valueAt:i];\n\
3677                 }\n\
3678                 @try {\n\
3679                     [self risky];\n\
3680                 } @catch (NSException *e) {\n\
3681                     total = -1;\n\
3682                 }\n\
3683                 if (total >= 0) {\n\
3684                     return total;\n\
3685                 }\n\
3686                 return 0;\n\
3687             }\n\
3688             @end\n",
3689            "foo.m",
3690            |metric| {
3691                assert_eq!(metric.abc.assignments_sum(), 5);
3692                assert_eq!(metric.abc.branches_sum(), 2);
3693                assert_eq!(metric.abc.conditions_sum(), 4);
3694            },
3695        );
3696    }
3697
3698    #[cfg(feature = "objc")]
3699    #[test]
3700    fn objc_abc_conditions() {
3701        // Exercises the condition-slot arms shared with C/C++: a `while`
3702        // head, a `&&` chain, a `do … while` trailing condition, and a
3703        // `return <comparison>`. ObjC routes these through the same
3704        // grammar-agnostic `cpp_inspect_*` helpers.
3705        //   A: `p++`, `p--` = 2.   B: no calls = 0.
3706        //   C: `p != 0`, `*p > 0`, `*p < 9`, `*p == 0` = 4.
3707        check_metrics::<ObjcParser>(
3708            "@implementation Foo\n\
3709             - (int)g:(int *)p {\n\
3710                 while (p != 0 && *p > 0) {\n\
3711                     p++;\n\
3712                 }\n\
3713                 do {\n\
3714                     p--;\n\
3715                 } while (*p < 9);\n\
3716                 return *p == 0;\n\
3717             }\n\
3718             @end\n",
3719            "foo.m",
3720            |metric| {
3721                assert_eq!(metric.abc.assignments_sum(), 2);
3722                assert_eq!(metric.abc.branches_sum(), 0);
3723                assert_eq!(metric.abc.conditions_sum(), 4);
3724            },
3725        );
3726    }
3727
3728    #[cfg(feature = "objc")]
3729    #[test]
3730    fn objc_abc_message_send_unary_condition() {
3731        // A negated boolean passed as a message-send argument is a unary
3732        // condition (Fitzpatrick Rule 9), the same as in a C-call argument.
3733        // Message args are direct children of `message_expression` (no
3734        // `argument_list`), so they are inspected in the `MessageExpression`
3735        // arm. Here: `[self use:!a]` (1 call + 1 unary condition) +
3736        // `cFunc(!a)` (1 call + 1 unary condition) → B=2, C=2.
3737        check_metrics::<ObjcParser>(
3738            "@implementation Foo\n\
3739             - (void)bar:(int)a {\n\
3740                 [self use:!a];\n\
3741                 cFunc(!a);\n\
3742             }\n\
3743             @end\n",
3744            "foo.m",
3745            |metric| {
3746                assert_eq!(metric.abc.branches_sum(), 2);
3747                assert_eq!(metric.abc.conditions_sum(), 2);
3748            },
3749        );
3750    }
3751
3752    #[cfg(feature = "objc")]
3753    #[test]
3754    fn objc_message_send_is_a_bool_terminal_in_condition_slots() {
3755        // `[obj ok]` is Objective-C's call, so in a condition slot it is
3756        // the unary condition `ok()` is (Fitzpatrick Rule 9). Until
3757        // `message_expression` joined the former `cpp_bool_terminal_kinds!` every
3758        // row here scored zero conditions where its C-call twin scored
3759        // one — and #1276's `for` slot inherited the gap. Each row is a
3760        // message send as the *whole* slot (grammar-dispatch §11: a
3761        // comparison would be counted by its own operator arm anyway),
3762        // and branches are asserted beside conditions so the arm cannot
3763        // be read as counting the call twice. The last three are the
3764        // over-count guards: a value position counts nothing, as `ok()`'s
3765        // does not.
3766        let cases = [
3767            ("if ([self ok]) {}", 1, 1),
3768            ("while (![self ok]) {}", 1, 1),
3769            ("do {} while ([self ok]);", 1, 1),
3770            ("for (; [self ok]; ) {}", 1, 1),
3771            ("x = [self ok] ? 1 : 2;", 2, 1),
3772            ("if ([self ok] && [o ok]) {}", 2, 2),
3773            ("return [self ok];", 0, 1),
3774            ("[self use:[self ok]];", 0, 2),
3775            ("[self use:![self ok]];", 1, 2),
3776        ];
3777        let mut ran = 0;
3778        for (body, conditions, branches) in cases {
3779            let src = format!("@implementation Foo\n- (int)bar {{\n    {body}\n}}\n@end\n");
3780            let abc = metrics_verbatim(
3781                LANG::Objc,
3782                src.as_bytes(),
3783                MetricsOptions::default().with_only(&[crate::Metric::Abc]),
3784            )
3785            .abc;
3786            assert_eq!(abc.conditions_sum(), conditions, "`{body}` conditions");
3787            assert_eq!(abc.branches_sum(), branches, "`{body}` branches");
3788            ran += 1;
3789        }
3790        assert_eq!(ran, cases.len());
3791        // Both answers are present, so a walker stuck at 0 or at 1 cannot
3792        // pass half the table silently.
3793        assert!(cases.iter().any(|&(_, c, _)| c == 0));
3794        assert!(cases.iter().any(|&(_, c, _)| c == 2));
3795    }
3796
3797    #[cfg(feature = "objc")]
3798    #[test]
3799    fn objc_message_send_condition_agrees_with_c_call() {
3800        // The intra-ObjC parity C++ cannot express: a message send and a
3801        // C call in the same slot score alike. Non-degenerate by the
3802        // `assert_eq!(…, 1)` on the reference.
3803        for (send, call) in [
3804            ("if ([a ok]) {}", "if (ok()) {}"),
3805            ("for (; [a ok]; ) {}", "for (; ok(); ) {}"),
3806            ("x = [a ok] ? 1 : 2;", "x = ok() ? 1 : 2;"),
3807        ] {
3808            let wrap =
3809                |body: &str| format!("@implementation Foo\n- (int)bar {{\n    {body}\n}}\n@end\n");
3810            let reference = abc_conditions(LANG::Objc, &wrap(call));
3811            assert!(reference >= 1, "`{call}` must count at least the slot");
3812            assert_eq!(
3813                abc_conditions(LANG::Objc, &wrap(send)),
3814                reference,
3815                "`{send}`"
3816            );
3817        }
3818    }
3819
3820    #[cfg(feature = "groovy")]
3821    #[test]
3822    fn groovy_switch_default_not_a_condition() {
3823        check_metrics::<GroovyParser>(
3824            "class A {
3825                int m(int x) {
3826                    switch (x) { case 1: return 1; case 2: return 2; default: return 0 }
3827                }
3828            }",
3829            "foo.groovy",
3830            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3831        );
3832    }
3833
3834    #[cfg(feature = "javascript")]
3835    #[test]
3836    fn js_switch_default_not_a_condition() {
3837        check_metrics::<JavascriptParser>(
3838            "function f(x) {
3839                 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3840             }",
3841            "foo.js",
3842            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3843        );
3844    }
3845
3846    #[cfg(feature = "typescript")]
3847    #[test]
3848    fn ts_switch_default_not_a_condition() {
3849        check_metrics::<TypescriptParser>(
3850            "function f(x: number): number {
3851                 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3852             }",
3853            "foo.ts",
3854            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3855        );
3856    }
3857
3858    // Cross-language parity (lesson 11): the equivalent statement-`switch`
3859    // with a `default` arm reports the same ABC condition count across
3860    // Java / C# / C++. All three have two concrete case arms plus a
3861    // fallthrough `default`, so all three must count exactly 2 conditions
3862    // (the `default` excluded). `check_metrics` takes a non-capturing
3863    // `fn` pointer, so the shared expected value is asserted in each
3864    // callback; the matching constant is what enforces parity.
3865    #[cfg(all(feature = "cpp", feature = "csharp", feature = "java"))]
3866    #[test]
3867    fn java_csharp_cpp_switch_default_abc_parity() {
3868        check_metrics::<JavaParser>(
3869            "class A {
3870                int m(int x) {
3871                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3872                }
3873            }",
3874            "foo.java",
3875            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3876        );
3877        check_metrics::<CsharpParser>(
3878            "class A {
3879                int M(int x) {
3880                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3881                }
3882            }",
3883            "foo.cs",
3884            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3885        );
3886        check_metrics::<CppParser>(
3887            "void f(int x) {
3888                 switch (x) { case 1: return; case 2: return; default: return; }
3889             }",
3890            "foo.cpp",
3891            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3892        );
3893    }
3894
3895    // Pins the ABC-vs-cyclomatic agreement the fix is about (lesson 11):
3896    // on the method's own function space, the cyclomatic decision count
3897    // (`cyclomatic()` minus the per-space base of 1) must equal the ABC
3898    // `conditions()` for the same switch. Both must be 2 — the two case
3899    // arms — with the `default` excluded from each. Revert-verified: pre-
3900    // #469 ABC `conditions()` was 3 here while cyclomatic stayed at 2.
3901    #[cfg(all(feature = "cpp", feature = "csharp", feature = "java"))]
3902    #[test]
3903    fn java_csharp_cpp_switch_default_cyclomatic_parity() {
3904        check_func_space::<JavaParser, _>(
3905            "class A {
3906                int m(int x) {
3907                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3908                }
3909            }",
3910            "foo.java",
3911            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3912        );
3913        check_func_space::<CsharpParser, _>(
3914            "class A {
3915                int M(int x) {
3916                    switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3917                }
3918            }",
3919            "foo.cs",
3920            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3921        );
3922        check_func_space::<CppParser, _>(
3923            "void f(int x) {
3924                 switch (x) { case 1: return; case 2: return; default: return; }
3925             }",
3926            "foo.cpp",
3927            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3928        );
3929    }
3930
3931    // Issue #473: PHP `switch` `default:` (`DefaultStatement`) is the
3932    // unconditional fallthrough, not a condition. ABC `conditions()` must
3933    // equal the cyclomatic decision count (`cyclomatic() - 1`) on the
3934    // function's own space — both 2 for the two `case` arms, with the
3935    // `default` excluded. Revert-verified: re-adding `DefaultStatement` to
3936    // the PHP ABC condition arm makes `conditions()` 3 here while cyclomatic
3937    // stays at 2, failing the invariant.
3938    #[cfg(feature = "php")]
3939    #[test]
3940    fn php_switch_default_not_a_condition() {
3941        check_func_space::<PhpParser, _>(
3942            "<?php
3943            function f($x) {
3944                switch ($x) {
3945                    case 1: return 1;
3946                    case 2: return 2;
3947                    default: return 0;
3948                }
3949            }",
3950            "foo.php",
3951            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3952        );
3953    }
3954
3955    // Issue #473: PHP `match` `default =>` (`MatchDefaultExpression`) is the
3956    // unconditional fallthrough, mirroring the switch `default:` case above.
3957    // ABC `conditions()` must equal the cyclomatic decision count
3958    // (`cyclomatic() - 1`) — both 2 for the two non-default match arms.
3959    // Revert-verified: re-adding `MatchDefaultExpression` to the PHP ABC
3960    // condition arm makes `conditions()` 3 here while cyclomatic stays at 2.
3961    #[cfg(feature = "php")]
3962    #[test]
3963    fn php_match_default_not_a_condition() {
3964        check_func_space::<PhpParser, _>(
3965            "<?php
3966            function g($x) {
3967                return match ($x) {
3968                    1 => \"a\",
3969                    2 => \"b\",
3970                    default => \"z\",
3971                };
3972            }",
3973            "foo.php",
3974            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3975        );
3976    }
3977
3978    #[cfg(feature = "csharp")]
3979    #[test]
3980    fn csharp_if_bare_identifier_condition() {
3981        check_metrics::<CsharpParser>(
3982            "class A {
3983                void M(bool x) {
3984                    if (x) { System.Console.WriteLine(\"a\"); }
3985                }
3986            }",
3987            "foo.cs",
3988            |metric| {
3989                // `if (x)` contributes 1 condition (bare identifier).
3990                // `System.Console.WriteLine(...)` is the only call → 1 branch.
3991                // `*_sum()` is what the public JSON serializes as the
3992                // headline value (see `crate::wire::Abc`).
3993                assert_eq!(metric.abc.conditions_sum(), 1);
3994                assert_eq!(metric.abc.branches_sum(), 1);
3995                assert_eq!(metric.abc.assignments_sum(), 0);
3996            },
3997        );
3998    }
3999
4000    #[cfg(feature = "csharp")]
4001    #[test]
4002    fn csharp_while_bare_identifier_condition() {
4003        check_metrics::<CsharpParser>(
4004            "class A {
4005                void M(bool x) {
4006                    while (x) { x = false; }
4007                }
4008            }",
4009            "foo.cs",
4010            |metric| {
4011                // `while (x)` contributes 1 condition; `x = false` is 1 assignment.
4012                assert_eq!(metric.abc.conditions_sum(), 1);
4013                assert_eq!(metric.abc.assignments_sum(), 1);
4014                assert_eq!(metric.abc.branches_sum(), 0);
4015            },
4016        );
4017    }
4018
4019    #[cfg(feature = "csharp")]
4020    #[test]
4021    fn csharp_do_while_bare_identifier_condition() {
4022        check_metrics::<CsharpParser>(
4023            "class A {
4024                void M(bool x) {
4025                    do { x = true; } while (x);
4026                }
4027            }",
4028            "foo.cs",
4029            |metric| {
4030                // `do { ... } while (x)` contributes 1 condition;
4031                // `x = true` is 1 assignment.
4032                assert_eq!(metric.abc.conditions_sum(), 1);
4033                assert_eq!(metric.abc.assignments_sum(), 1);
4034                assert_eq!(metric.abc.branches_sum(), 0);
4035            },
4036        );
4037    }
4038
4039    #[cfg(feature = "csharp")]
4040    #[test]
4041    fn csharp_if_unary_not_condition() {
4042        // Two cases share one test:
4043        //
4044        //   if (!x) { … }  — the `if` predicate is a boolean slot, so
4045        //   `csharp_count_condition` pays for it once the peel
4046        //   (`csharp_wrapper_operand`) has taken the `!` off; the
4047        //   slot would pay without the `!` too.
4048        //
4049        //   return !x;  — a `return` value is not a slot, so the `!`
4050        //   is the *only* thing that makes the operand count, through
4051        //   `csharp_count_negated`. Asserting the `return !x;`
4052        //   case isolates the negation path from the slot path.
4053        check_metrics::<CsharpParser>(
4054            "class A {
4055                void M(bool x) {
4056                    if (!x) { System.Console.WriteLine(\"a\"); }
4057                }
4058                bool N(bool x) {
4059                    return !x;
4060                }
4061            }",
4062            "foo.cs",
4063            |metric| {
4064                // `if (!x)` contributes 1 condition (the slot pays for the
4065                // peeled `x`). `return !x;` contributes 1 condition (not a
4066                // slot — the unary `!` is what makes `x` count).
4067                // → 2 conditions total. 1 branch from WriteLine().
4068                assert_eq!(metric.abc.conditions_sum(), 2);
4069                assert_eq!(metric.abc.branches_sum(), 1);
4070                assert_eq!(metric.abc.assignments_sum(), 0);
4071            },
4072        );
4073    }
4074
4075    #[cfg(feature = "csharp")]
4076    #[test]
4077    fn csharp_if_double_parenthesized_condition() {
4078        // Audit-tests follow-up: with only the
4079        // `csharp_prefix_unary_expr_kinds!()` arm covered by
4080        // `csharp_if_unary_not_condition`, the
4081        // `csharp_paren_expr_kinds!()` delegation arm in
4082        // `csharp_count_condition` was a pure dead-code candidate —
4083        // disabling it caused zero existing tests to fail (verified
4084        // 2026-05-26).
4085        //
4086        // `if ((x))` puts a `ParenthesizedExpression` at child(2) of
4087        // the IfStatement (child(1) is the literal `(`, child(2) is
4088        // the inner parenthesised expression, child(3) is the literal
4089        // `)`). `csharp_count_condition` must peel that parenthesis
4090        // (`csharp_wrapper_operand`) down to the inner Identifier,
4091        // which does not score itself, so the slot pays once.
4092        check_metrics::<CsharpParser>(
4093            "class A {
4094                void M(bool x) {
4095                    if ((x)) { System.Console.WriteLine(\"a\"); }
4096                }
4097            }",
4098            "foo.cs",
4099            |metric| {
4100                assert_eq!(metric.abc.conditions_sum(), 1);
4101                assert_eq!(metric.abc.branches_sum(), 1);
4102                assert_eq!(metric.abc.assignments_sum(), 0);
4103            },
4104        );
4105    }
4106
4107    #[cfg(feature = "csharp")]
4108    #[test]
4109    fn csharp_bool_returning_terminal_kinds_count() {
4110        // Regression for issue #372 (lesson #19): before the fix,
4111        // `csharp_count_condition` / the former `csharp_inspect_container`
4112        // only recognised invocation / identifier / boolean literal as
4113        // terminal-bool operands, so the five idiomatic boolean
4114        // expressions in the `if (...)` slots below silently scored
4115        // zero conditions:
4116        //
4117        //   - `cfg.flag`        — MemberAccessExpression
4118        //   - `await c.Check()` — AwaitExpression
4119        //   - `(bool)v`         — CastExpression
4120        //   - `v is not null`   — IsPatternExpression
4121        //   - `flags[0]`        — ElementAccessExpression
4122        //
4123        // The `is` test has since moved out of the former terminal set to an
4124        // unconditional arm (#1461), so it no longer depends on the
4125        // slot to be counted. The total is unchanged, which is the
4126        // point of that change rather than an accident of it.
4127        //
4128        // expected: 5 conditions (one per `if`), 0 assignments,
4129        // 1 branch (the single `c.Check()` invocation; the other
4130        // `if`-condition expressions are not invocations).
4131        check_metrics::<CsharpParser>(
4132            "using System.Threading.Tasks;
4133            class A {
4134                async Task M(object v, bool[] flags, Cfg cfg, C c) {
4135                    if (cfg.flag) { }
4136                    if (await c.Check()) { }
4137                    if ((bool)v) { }
4138                    if (v is not null) { }
4139                    if (flags[0]) { }
4140                }
4141            }
4142            class Cfg { public bool flag; }
4143            class C { public Task<bool> Check() => null; }",
4144            "foo.cs",
4145            |metric| {
4146                assert_eq!(metric.abc.conditions_sum(), 5);
4147                assert_eq!(metric.abc.assignments_sum(), 0);
4148                assert_eq!(metric.abc.branches_sum(), 1);
4149            },
4150        );
4151    }
4152
4153    #[cfg(feature = "csharp")]
4154    #[test]
4155    fn csharp_if_method_call_condition() {
4156        check_metrics::<CsharpParser>(
4157            "class A {
4158                void M(string s) {
4159                    if (s.StartsWith(\"x\")) { System.Console.WriteLine(\"a\"); }
4160                }
4161            }",
4162            "foo.cs",
4163            |metric| {
4164                // `if (s.StartsWith("x"))` contributes 1 condition
4165                // (InvocationExpression) plus 1 branch for the call itself,
4166                // plus 1 branch for WriteLine.
4167                assert_eq!(metric.abc.conditions_sum(), 1);
4168                assert_eq!(metric.abc.branches_sum(), 2);
4169                assert_eq!(metric.abc.assignments_sum(), 0);
4170            },
4171        );
4172    }
4173
4174    #[cfg(feature = "csharp")]
4175    #[test]
4176    fn csharp_if_while_boolean_literal_condition() {
4177        // Regression for #371: the tree-sitter-c-sharp grammar wraps a
4178        // bare `true` / `false` literal used as the condition of
4179        // `if` / `while` / `do` / `?:` in a `boolean_literal` node,
4180        // not the leaf `true` / `false` tokens. `csharp_count_condition`
4181        // must therefore match `BooleanLiteral` (the wrapper),
4182        // mirroring the existing `csharp_walk_for_statement` arm.
4183        // Without that, every literal-condition statement scored 0
4184        // conditions. The `&&` / `||` chain-operand slot is covered
4185        // separately by `csharp_short_circuit_with_boolean_literal_operand`
4186        // and the negated-operand path (`csharp_count_negated`) by
4187        // `csharp_declarations_with_conditions` (`!true` / `!false`).
4188        check_metrics::<CsharpParser>(
4189            "class A {
4190                void M() {
4191                    if (true) { System.Console.WriteLine(\"a\"); }
4192                    if (false) { System.Console.WriteLine(\"b\"); }
4193                    while (true) { break; }
4194                    do { break; } while (false);
4195                    int t = true ? 1 : 0;
4196                }
4197            }",
4198            "foo.cs",
4199            |metric| {
4200                // Five literal-condition statements contribute 5
4201                // `BooleanLiteral` conditions (one per if/if/while/
4202                // do-while/ternary), plus the ternary's `?` token
4203                // adds one more via `csharp_count_token_condition`
4204                // → 6 total. The two `System.Console.WriteLine`
4205                // calls contribute 2 branches; the `int t = …`
4206                // initializer contributes 1 assignment.
4207                assert_eq!(metric.abc.conditions_sum(), 6);
4208                assert_eq!(metric.abc.branches_sum(), 2);
4209                assert_eq!(metric.abc.assignments_sum(), 1);
4210            },
4211        );
4212    }
4213
4214    #[cfg(feature = "csharp")]
4215    #[test]
4216    fn csharp_short_circuit_with_boolean_literal_operand() {
4217        // Regression for #371 (companion to
4218        // `csharp_if_while_boolean_literal_condition`): a bare
4219        // `true` / `false` operand of `&&` / `||` is a chain-operand
4220        // slot scored through `csharp_count_condition`. A
4221        // `BooleanLiteral` does not score itself, so the slot pays for
4222        // it — otherwise the operand silently scores zero.
4223        check_metrics::<CsharpParser>(
4224            "class A {
4225                void M(bool x) {
4226                    if (x && true) { System.Console.WriteLine(\"a\"); }
4227                    if (false || x) { System.Console.WriteLine(\"b\"); }
4228                }
4229            }",
4230            "foo.cs",
4231            |metric| {
4232                // `&&` and `||` themselves are NOT in
4233                // `csharp_count_token_condition`'s match list — they
4234                // route through `csharp_walk_for_conditions::AMPAMP|
4235                // PIPEPIPE`, which scores each operand of the parent
4236                // BinaryExpression through `csharp_count_condition`.
4237                // An operand pays one condition unless it scores
4238                // itself. For
4239                // `x && true`: 1 (Identifier x) + 1 (BooleanLiteral
4240                // true) = 2. For `false || x`: 1 (BooleanLiteral
4241                // false) + 1 (Identifier x) = 2. Total 4.
4242                assert_eq!(metric.abc.conditions_sum(), 4);
4243                assert_eq!(metric.abc.branches_sum(), 2);
4244                assert_eq!(metric.abc.assignments_sum(), 0);
4245            },
4246        );
4247    }
4248
4249    #[cfg(feature = "csharp")]
4250    #[test]
4251    fn csharp_return_without_conditions() {
4252        check_metrics::<CsharpParser>(
4253            "class A {
4254                int M() { return 42; }
4255                string N() { return \"hi\"; }
4256            }",
4257            "foo.cs",
4258            |metric| insta::assert_json_snapshot!(metric.abc),
4259        );
4260    }
4261
4262    #[cfg(feature = "csharp")]
4263    #[test]
4264    fn csharp_lambda_expressions_return_with_conditions() {
4265        check_metrics::<CsharpParser>(
4266            "class A {
4267                public void M() {
4268                    System.Func<int, bool> f = x => (x > 0);
4269                    System.Func<int, bool> g = x => !(x < 0);
4270                }
4271            }",
4272            "foo.cs",
4273            |metric| insta::assert_json_snapshot!(metric.abc),
4274        );
4275    }
4276
4277    #[cfg(feature = "csharp")]
4278    #[test]
4279    fn csharp_for_with_variable_declaration() {
4280        check_metrics::<CsharpParser>(
4281            "class A {
4282                void M() {
4283                    for (int i = 0; i < 10; i++) {
4284                        System.Console.WriteLine(i);
4285                    }
4286                }
4287            }",
4288            "foo.cs",
4289            |metric| insta::assert_json_snapshot!(metric.abc),
4290        );
4291    }
4292
4293    #[cfg(feature = "csharp")]
4294    #[test]
4295    fn csharp_for_without_variable_declaration() {
4296        check_metrics::<CsharpParser>(
4297            "class A {
4298                void M() {
4299                    int i;
4300                    for (i = 0; i < 10; i++) {
4301                        System.Console.WriteLine(i);
4302                    }
4303                }
4304            }",
4305            "foo.cs",
4306            |metric| insta::assert_json_snapshot!(metric.abc),
4307        );
4308    }
4309
4310    #[cfg(feature = "csharp")]
4311    #[test]
4312    fn csharp_for_identifier_condition() {
4313        check_metrics::<CsharpParser>(
4314            "class A {
4315                void M(bool ready) {
4316                    for (; ready ;) { }
4317                }
4318            }",
4319            "foo.cs",
4320            |metric| {
4321                // expected: assignments=0 (no `=` / `++` / `--`),
4322                // branches=0 (no invocation / object creation),
4323                // conditions=1 (bare-identifier for-loop condition).
4324                // Averages divide by 3 spaces (top-level + class + method).
4325                insta::assert_json_snapshot!(
4326                    metric.abc,
4327                    @r#"
4328                {
4329                  "assignments": 0,
4330                  "branches": 0,
4331                  "conditions": 1,
4332                  "magnitude": 1.0,
4333                  "value": 0.0,
4334                  "assignments_average": 0.0,
4335                  "branches_average": 0.0,
4336                  "conditions_average": 0.3333333333333333,
4337                  "assignments_min": 0,
4338                  "assignments_max": 0,
4339                  "branches_min": 0,
4340                  "branches_max": 0,
4341                  "conditions_min": 0,
4342                  "conditions_max": 1
4343                }
4344                "#
4345                );
4346            },
4347        );
4348    }
4349
4350    #[cfg(feature = "csharp")]
4351    #[test]
4352    fn csharp_for_invocation_condition() {
4353        check_metrics::<CsharpParser>(
4354            "class A {
4355                bool Ok() { return true; }
4356                void M() {
4357                    for (; Ok() ;) { }
4358                }
4359            }",
4360            "foo.cs",
4361            |metric| {
4362                // expected: assignments=0, branches=1 (the `Ok()` call),
4363                // conditions=1 (invocation as for-loop condition).
4364                // Averages divide by 4 spaces (top-level + class + two
4365                // methods).
4366                insta::assert_json_snapshot!(
4367                    metric.abc,
4368                    @r#"
4369                {
4370                  "assignments": 0,
4371                  "branches": 1,
4372                  "conditions": 1,
4373                  "magnitude": 1.4142135623730951,
4374                  "value": 0.0,
4375                  "assignments_average": 0.0,
4376                  "branches_average": 0.25,
4377                  "conditions_average": 0.25,
4378                  "assignments_min": 0,
4379                  "assignments_max": 0,
4380                  "branches_min": 0,
4381                  "branches_max": 1,
4382                  "conditions_min": 0,
4383                  "conditions_max": 1
4384                }
4385                "#
4386                );
4387            },
4388        );
4389    }
4390
4391    // Regression coverage for #279: the C# grammar wraps a literal
4392    // `true` / `false` for-loop condition in a `boolean_literal` node.
4393    // The `BooleanLiteral` arm in the `ForStatement` dispatch must
4394    // attribute one condition; without it, `for (; true ;)` would
4395    // contribute 0 (the bug fixed by this commit also affected this
4396    // shape).
4397    #[cfg(feature = "csharp")]
4398    #[test]
4399    fn csharp_for_boolean_literal_condition() {
4400        check_metrics::<CsharpParser>(
4401            "class A {
4402                void M() {
4403                    for (; true ;) { }
4404                }
4405            }",
4406            "foo.cs",
4407            |metric| {
4408                // expected: assignments=0, branches=0,
4409                // conditions=1 (the `true` literal as condition).
4410                assert_eq!(metric.abc.conditions_sum(), 1);
4411                assert_eq!(metric.abc.assignments_sum(), 0);
4412                assert_eq!(metric.abc.branches_sum(), 0);
4413            },
4414        );
4415    }
4416
4417    // Regression coverage for #279: an empty for-loop condition such as
4418    // `for (; ;) {}` must contribute 0 to conditions — there is no
4419    // condition node to count.
4420    #[cfg(feature = "csharp")]
4421    #[test]
4422    fn csharp_for_empty_condition() {
4423        check_metrics::<CsharpParser>(
4424            "class A {
4425                void M() {
4426                    for (; ;) { }
4427                }
4428            }",
4429            "foo.cs",
4430            |metric| {
4431                // expected: assignments=0, branches=0, conditions=0
4432                // (no condition expression in `for (; ;)`).
4433                insta::assert_json_snapshot!(
4434                    metric.abc,
4435                    @r#"
4436                {
4437                  "assignments": 0,
4438                  "branches": 0,
4439                  "conditions": 0,
4440                  "magnitude": 0.0,
4441                  "value": 0.0,
4442                  "assignments_average": 0.0,
4443                  "branches_average": 0.0,
4444                  "conditions_average": 0.0,
4445                  "assignments_min": 0,
4446                  "assignments_max": 0,
4447                  "branches_min": 0,
4448                  "branches_max": 0,
4449                  "conditions_min": 0,
4450                  "conditions_max": 0
4451                }
4452                "#
4453                );
4454            },
4455        );
4456    }
4457
4458    #[cfg(feature = "csharp")]
4459    #[test]
4460    fn csharp_ternary_conditions() {
4461        check_metrics::<CsharpParser>(
4462            "class A {
4463                int Sign(int x) {
4464                    return (x > 0) ? 1 : (x < 0 ? -1 : 0);
4465                }
4466            }",
4467            "foo.cs",
4468            |metric| insta::assert_json_snapshot!(metric.abc),
4469        );
4470    }
4471
4472    #[cfg(feature = "csharp")]
4473    #[test]
4474    fn csharp_malformed_parenthesized_no_panic() {
4475        check_metrics::<CsharpParser>("class A { void M() { if (( }) }", "foo.cs", |metric| {
4476            // Don't panic on malformed source.
4477            assert_eq!(metric.abc.assignments(), 0);
4478            assert_eq!(metric.abc.branches(), 0);
4479        });
4480    }
4481
4482    #[cfg(feature = "csharp")]
4483    #[test]
4484    fn csharp_function_pointer_type_no_double_count() {
4485        // EC1 extension — `<` and `>` are also parameter-list delimiters
4486        // for unsafe function-pointer types. Since #1297 the arm is an
4487        // allowlist, so `FunctionPointerType` is excluded by being
4488        // absent from it rather than by being named in a deny set;
4489        // either way these brackets must not accumulate `conditions`.
4490        check_metrics::<CsharpParser>(
4491            "unsafe class A {
4492                public delegate*<int, int, int> Adder;
4493                public delegate*<string, void> Logger;
4494            }",
4495            "foo.cs",
4496            |metric| {
4497                assert_eq!(
4498                    metric.abc.conditions(),
4499                    0,
4500                    "function-pointer-type angle brackets must not count"
4501                );
4502            },
4503        );
4504    }
4505
4506    #[cfg(feature = "csharp")]
4507    #[test]
4508    fn csharp_generic_type_args_no_double_count() {
4509        // EC1 — `<` and `>` inside TypeArgumentList must not count as
4510        // boolean conditions.
4511        check_metrics::<CsharpParser>(
4512            "class A {
4513                void M(System.Collections.Generic.Dictionary<string, System.Collections.Generic.List<int>> d) {
4514                    System.Console.WriteLine(d);
4515                }
4516            }",
4517            "foo.cs",
4518            |metric| insta::assert_json_snapshot!(metric.abc),
4519        );
4520    }
4521
4522    // #1297: `operator <` names the operator a type *defines*; applying
4523    // one is what a decision looks like. tree-sitter-c-sharp spells the
4524    // declaration's operator with the same bare token as a comparison,
4525    // and the previous denylist named only `type_argument_list`,
4526    // `type_parameter_list` and `function_pointer_type`, so every
4527    // comparison-operator overload scored a condition.
4528    //
4529    // The fixture carries the two overloads plus one `a < b` inside a
4530    // `binary_expression` and one `x is > 0 ? 2 : 3`, which the grammar
4531    // parses as a `relational_pattern` whose operand is the ternary (see
4532    // the assertion). Every mis-aim lands on its own number: 7
4533    // with neither gate, 5 if `RelationalPattern` is readmitted to the
4534    // allowlist (#1383 dropped it), 3 if the gate swallows
4535    // `BinaryExpression` too (only the ternary's `?` and slot and the
4536    // `is` test survive), 0 if the fixture stops parsing. Each is two
4537    // higher than before #1461 and #1526, which added the `is` test and
4538    // the ternary's slot as counts no gate on the `<` / `>` token can
4539    // reach.
4540    #[cfg(feature = "csharp")]
4541    #[test]
4542    fn csharp_operator_declaration_is_not_a_condition() {
4543        check_func_space::<CsharpParser, _>(
4544            "class V {
4545                public static bool operator <(V a, V b) { return true; }
4546                public static bool operator >(V a, V b) { return true; }
4547                int m(int a, int b, int x) {
4548                    if (a < b) { return 1; }
4549                    return x is > 0 ? 2 : 3;
4550                }
4551            }",
4552            "foo.cs",
4553            |space| {
4554                // Assert the claim per space rather than through the
4555                // file total, which is 2 with the two overloads and 2
4556                // without them — an aggregate assertion would pass on a
4557                // fixture that had lost the very construct under test.
4558                let class = &space.spaces[0];
4559                assert_eq!(class.spaces.len(), 3, "two overloads plus `m`");
4560                for (i, member) in class.spaces.iter().enumerate().take(2) {
4561                    assert_eq!(
4562                        member.metrics.abc.conditions(),
4563                        0,
4564                        "operator declaration {i} must score no condition"
4565                    );
4566                }
4567                // 4: the `a < b` comparison, the ternary `?`, its
4568                // condition slot, and the `is` test. tree-sitter-c-sharp
4569                // 0.23.5 parses `x is > 0 ? 2 : 3` as `x is > (0 ? 2 :
4570                // 3)` — the ternary is the pattern's operand, so the
4571                // pattern sits in no decision slot and its `>` still
4572                // scores nothing (#1383), while the ternary's condition
4573                // is the literal `0`, which pays as a slot since #1526.
4574                // The fourth count is the enclosing
4575                // `is_pattern_expression` itself, which since #1461
4576                // scores by use rather than only inside a boolean slot.
4577                // C# itself binds the source `(x is > 0) ? 2 : 3`; a
4578                // grammar that agrees reads 3 here, because the `is`
4579                // test in the condition slot already pays for it.
4580                assert_eq!(class.spaces[2].metrics.abc.conditions(), 4);
4581            },
4582        );
4583    }
4584
4585    // #1420, the other four spellings of the test above. C# overloads six
4586    // comparison operators and #1297 gated two: `<=` `>=` `==` `!=` are
4587    // distinct tokens that reach a different arm of
4588    // `csharp_count_token_condition` and each still scored one spurious
4589    // condition on its declaration — measured 1 apiece against `<` / `>`'s
4590    // 0, on a class where `cyclomatic()` is 1 for all six.
4591    //
4592    // Each token gets its own member so a partial fix names the spelling
4593    // it missed: gating `EQEQ | BANGEQ` and forgetting `GTEQ | LTEQ` fails
4594    // here by operator, where the file total cannot tell the two apart —
4595    // it reads 4 before the fix and 0 after, so any two-of-four fix halves
4596    // it and still looks like movement.
4597    //
4598    // `n` is the deliberate control (§11): the same four tokens *applied*,
4599    // inside `binary_expression` parents, which must keep scoring one
4600    // condition each. Without it the gate could be satisfied by refusing
4601    // to count these tokens at all.
4602    #[cfg(feature = "csharp")]
4603    #[test]
4604    fn csharp_comparison_operator_overloads_are_not_conditions() {
4605        let src = "class V {
4606                public static bool operator <=(V a, V b) { return true; }
4607                public static bool operator >=(V a, V b) { return true; }
4608                public static bool operator ==(V a, V b) { return true; }
4609                public static bool operator !=(V a, V b) { return true; }
4610                int n(int a, int b) {
4611                    if (a <= b) { return 1; }
4612                    if (a >= b) { return 2; }
4613                    if (a == b) { return 3; }
4614                    if (a != b) { return 4; }
4615                    return 0;
4616                }
4617            }";
4618        // Two of each token: one declaring, one applying. Trimming either
4619        // half out collapses this test into the other half's control and
4620        // is what this anchor exists to catch.
4621        assert_csharp_fixture_spells(
4622            src,
4623            &[
4624                (
4625                    Csharp::OperatorDeclaration as u16,
4626                    4,
4627                    "operator declarations",
4628                ),
4629                (Csharp::LTEQ as u16, 2, "`<=`"),
4630                (Csharp::GTEQ as u16, 2, "`>=`"),
4631                (Csharp::EQEQ as u16, 2, "`==`"),
4632                (Csharp::BANGEQ as u16, 2, "`!=`"),
4633            ],
4634        );
4635        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
4636            let class = &space.spaces[0];
4637            assert_eq!(class.spaces.len(), 5, "four overloads plus `n`");
4638            for (member, spelling) in class.spaces.iter().zip(["<=", ">=", "==", "!="]) {
4639                assert_eq!(
4640                    member.metrics.abc.conditions(),
4641                    0,
4642                    "`operator {spelling}` declares an operator, it does not apply one"
4643                );
4644                assert_eq!(
4645                    member.metrics.abc.conditions(),
4646                    member.metrics.cyclomatic.cyclomatic() - 1,
4647                    "`operator {spelling}`: §8 parity with the cyclomatic decision count"
4648                );
4649            }
4650            // The control, found by name so a reordered fixture cannot
4651            // silently re-point it at an overload. 0 here would mean the
4652            // gate swallowed the applying form along with the declaring
4653            // one.
4654            let control = child_space(class, "n");
4655            assert_eq!(
4656                control.metrics.abc.conditions(),
4657                4,
4658                "`n` applies all four tokens: one condition each"
4659            );
4660        });
4661    }
4662
4663    // The allowlist #1420 installed is the only thing keeping `#if A == B`
4664    // a condition, and it keeps it for a reason no arm states: the grammar
4665    // aliases `preproc_binary_expression` onto `binary_expression`, so the
4666    // preprocessor's `==` arrives with kind id `BinaryExpression` (369)
4667    // and not with one of its own. Before #1420 the token was ungated and
4668    // no fact about its parent mattered; now a grammar bump that gave the
4669    // alias a distinct id would stop counting every `#if` equality with
4670    // nothing red. This is that pin.
4671    //
4672    // `BinaryExpression2` (469) is the pre-alias symbol the arm also
4673    // lists, defensively, per `.claude/rules/grammar-dispatch.md` §1. §2
4674    // asks that such an entry be kept *and* have its unreachability
4675    // asserted, so that a pin promoting the symbol changes behaviour
4676    // loudly rather than invisibly.
4677    #[cfg(feature = "csharp")]
4678    #[test]
4679    fn csharp_preproc_equality_counts_through_the_binary_expression_alias() {
4680        let src = "class P {
4681            #if A == B
4682                int x;
4683            #endif
4684            #if C != D
4685                int y;
4686            #endif
4687            }";
4688        let parser = CsharpParser::new(
4689            src.as_bytes().to_vec(),
4690            std::path::Path::new("foo.cs"),
4691            None,
4692        );
4693        assert!(
4694            ast_has_kind_id(&parser, Csharp::BinaryExpression as u16),
4695            "the `#if` expressions must parse as `binary_expression` — \
4696             the alias is what the arm's allowlist matches"
4697        );
4698        assert!(
4699            !ast_has_kind_id(&parser, Csharp::BinaryExpression2 as u16),
4700            "`BinaryExpression2` is the pre-alias symbol and must stay \
4701             unreachable; if the grammar starts emitting it, re-derive the \
4702             allowlist rather than trusting this arm"
4703        );
4704        // 2, one per `#if` expression. The two directives carry no
4705        // declarations the walk would score, so nothing else contributes.
4706        check_metrics::<CsharpParser>(src, "foo.cs", |metric| {
4707            assert_eq!(metric.abc.conditions_sum(), 2);
4708        });
4709    }
4710
4711    // #1448, the C++ / Mozcpp instance of #1420. C++ overloads seven
4712    // comparison operators; before the fix only `<` / `>` were gated on a
4713    // `binary_expression` parent, so `operator<=` `>=` `==` `!=` and the
4714    // spaceship `operator<=>` each scored one spurious condition on a
4715    // struct where every member has `cyclomatic()` 1.
4716    //
4717    // Members are looked up by name, so a partial fix fails on the
4718    // spelling it missed rather than on a total that merely moved. `n`
4719    // is the §11 control: the same seven tokens applied, which must
4720    // keep scoring one condition each. The census anchors both halves —
4721    // trimming an overload or an application out of the fixture fails
4722    // by name instead of decaying into its counterpart.
4723    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
4724    const CPP_COMPARISON_OVERLOADS: &str = "struct V {
4725            bool operator<(const V& o) const { return true; }
4726            bool operator>(const V& o) const { return true; }
4727            bool operator<=(const V& o) const { return true; }
4728            bool operator>=(const V& o) const { return true; }
4729            bool operator==(const V& o) const { return true; }
4730            bool operator!=(const V& o) const { return true; }
4731            auto operator<=>(const V& o) const { return 0; }
4732        };
4733        struct W {
4734            int n;
4735            bool operator==(const W&) const = default;
4736        };
4737        int n(int a, int b) {
4738            return (a < b) + (a > b) + (a <= b) + (a >= b) + (a == b) + (a != b) + (a <=> b);
4739        }";
4740
4741    // `operator_name` id, then the seven token ids in source order, for
4742    // whichever of the two grammars `P` parses.
4743    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
4744    fn assert_cpp_comparison_overloads_are_not_conditions<P: MetricSuite>(
4745        operator_name: u16,
4746        tokens: [u16; 7],
4747    ) {
4748        #[cfg(any(feature = "cpp", feature = "mozcpp"))]
4749        const SPELLINGS: [&str; 7] = ["<", ">", "<=", ">=", "==", "!=", "<=>"];
4750        let mut census = vec![(operator_name, 8, "operator names")];
4751        // Two of each token, one declaring and one applying, plus the
4752        // third `==` that `W`'s defaulted comparison declares.
4753        census.extend(
4754            tokens
4755                .iter()
4756                .zip(SPELLINGS)
4757                .map(|(&id, s)| (id, if s == "==" { 3 } else { 2 }, s)),
4758        );
4759        assert_fixture_spells::<P>(CPP_COMPARISON_OVERLOADS, "foo.cpp", &census);
4760        check_func_space::<P, _>(CPP_COMPARISON_OVERLOADS, "foo.cpp", |space| {
4761            let class = child_space(&space, "V");
4762            assert_eq!(class.spaces.len(), 7, "seven overloads");
4763            for spelling in SPELLINGS {
4764                let member = child_space(class, &format!("operator{spelling}"));
4765                assert_eq!(
4766                    member.metrics.abc.conditions(),
4767                    0,
4768                    "`operator{spelling}` declares an operator, it does not apply one"
4769                );
4770                assert_eq!(
4771                    member.metrics.abc.conditions(),
4772                    member.metrics.cyclomatic.cyclomatic() - 1,
4773                    "`operator{spelling}`: §8 parity with the cyclomatic decision count"
4774                );
4775            }
4776            assert_eq!(
4777                child_space(&space, "n").metrics.abc.conditions(),
4778                7,
4779                "`n` applies all seven tokens: one condition each"
4780            );
4781            // A defaulted C++20 comparison declares the operator with no
4782            // body; it is the common modern spelling and must score
4783            // nothing anywhere in the file.
4784            assert_eq!(
4785                space.metrics.abc.conditions_sum(),
4786                7,
4787                "only `n`'s seven applications are conditions"
4788            );
4789        });
4790    }
4791
4792    #[cfg(feature = "cpp")]
4793    #[test]
4794    fn cpp_comparison_operator_overloads_are_not_conditions() {
4795        assert_cpp_comparison_overloads_are_not_conditions::<CppParser>(
4796            Cpp::OperatorName as u16,
4797            [
4798                Cpp::LT,
4799                Cpp::GT,
4800                Cpp::LTEQ,
4801                Cpp::GTEQ,
4802                Cpp::EQEQ,
4803                Cpp::BANGEQ,
4804                Cpp::LTEQGT,
4805            ]
4806            .map(|k| k as u16),
4807        );
4808    }
4809
4810    // Mozcpp owns no file extension, so this is its only coverage of the
4811    // arm: no integration snapshot routes to it.
4812    #[cfg(feature = "mozcpp")]
4813    #[test]
4814    fn mozcpp_comparison_operator_overloads_are_not_conditions() {
4815        use crate::Mozcpp;
4816        assert_cpp_comparison_overloads_are_not_conditions::<crate::MozcppParser>(
4817            Mozcpp::OperatorName as u16,
4818            [
4819                Mozcpp::LT,
4820                Mozcpp::GT,
4821                Mozcpp::LTEQ,
4822                Mozcpp::GTEQ,
4823                Mozcpp::EQEQ,
4824                Mozcpp::BANGEQ,
4825                Mozcpp::LTEQGT,
4826            ]
4827            .map(|k| k as u16),
4828        );
4829    }
4830
4831    // ISO C++ spells `!`, `&&`, `||` and `!=` equally as `not`, `and`,
4832    // `or` and `not_eq` ([lex.digraph]), and the C++ grammars give each
4833    // alternative token a kind of its own. ABC tested only the symbols,
4834    // so `if (not b)` scored 0 against `if (!b)`'s 1, `b and c` scored 0
4835    // against `b && c`'s 2, and `b not_eq c` scored 0 against `b != c`'s
4836    // 1. Each row pairs a symbolic spelling with its alternative and the
4837    // value both must give; the census proves the alternative fixture
4838    // really carries the alternative token rather than parsing it as an
4839    // identifier, which is what tree-sitter-c does with the same text.
4840    //
4841    // Arguments: the `not`, `and`, `or` and `not_eq` ids of `P`'s grammar.
4842    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
4843    fn assert_cpp_alternative_tokens_score_like_symbols<P: MetricSuite>(alt: [u16; 4]) {
4844        let [not, and, or, not_eq] = alt;
4845        let rows = [
4846            ("if (!b) {}", "if (not b) {}", not, 1),
4847            ("bool x = !b && c;", "bool x = not b && c;", not, 2),
4848            ("bool x = c ? !b : c;", "bool x = c ? not b : c;", not, 3),
4849            ("bool x = b && c;", "bool x = b and c;", and, 2),
4850            ("if (b || c) {}", "if (b or c) {}", or, 2),
4851            ("if (b != c) {}", "if (b not_eq c) {}", not_eq, 1),
4852        ];
4853        for (symbolic, alternative, token, expected) in rows {
4854            let alt_source = format!("void f(bool b, bool c) {{\n  {alternative}\n}}\n");
4855            assert_fixture_spells::<P>(&alt_source, "foo.cpp", &[(token, 1, alternative)]);
4856            for source in [symbolic, alternative] {
4857                let source = format!("void f(bool b, bool c) {{\n  {source}\n}}\n");
4858                check_func_space::<P, _>(&source, "foo.cpp", |space| {
4859                    assert_eq!(
4860                        space.metrics.abc.conditions_sum(),
4861                        expected,
4862                        "`{source}` must score like its symbolic spelling `{symbolic}`"
4863                    );
4864                });
4865            }
4866        }
4867    }
4868
4869    #[cfg(feature = "cpp")]
4870    #[test]
4871    fn cpp_alternative_tokens_score_like_symbols() {
4872        assert_cpp_alternative_tokens_score_like_symbols::<CppParser>(
4873            [Cpp::Not, Cpp::And, Cpp::Or, Cpp::NotEq].map(|k| k as u16),
4874        );
4875    }
4876
4877    // Mozcpp owns no file extension, so this is its only coverage of the
4878    // alternative tokens: no integration snapshot routes to it.
4879    #[cfg(feature = "mozcpp")]
4880    #[test]
4881    fn mozcpp_alternative_tokens_score_like_symbols() {
4882        use crate::Mozcpp;
4883        assert_cpp_alternative_tokens_score_like_symbols::<crate::MozcppParser>(
4884            [Mozcpp::Not, Mozcpp::And, Mozcpp::Or, Mozcpp::NotEq].map(|k| k as u16),
4885        );
4886    }
4887
4888    // The two other productions #1448's allowlist decides, alongside the
4889    // operator overloads above.
4890    //
4891    // `#if A <= B` is a real decision and must keep counting: the grammar
4892    // aliases `preproc_binary_expression` onto `binary_expression`, and
4893    // the allowlist is the only thing that admits it now that `<=` / `!=`
4894    // are gated. The fixture's two directives are the root's only
4895    // conditions.
4896    //
4897    // A fold over a comparison operator counts once, through the fold's
4898    // `operator` field: before the fix `lt` scored 0, `le` 1 and the
4899    // binary fold `eq` 2 (one per spelling of its single operator).
4900    // `idiom` is the control — its `==` sits in a `binary_expression`
4901    // inside the fold and counts there, while the fold's own `&&` is not
4902    // a comparison: it scores what its twin `(a == 0) && (a == 0)` does,
4903    // 2 (#1533). `neg` is the same twice-spelled operator under the `||`
4904    // chain arm: it scores what `true || !a` does, 2, where walking the
4905    // fold once per spelling would pay 4.
4906    //
4907    // The census also pins §2: every `binary_expression` here carries the
4908    // `BinaryExpression2` id and the pre-alias `BinaryExpression` is never
4909    // emitted. `cpp_operator_is_applied` matches the kind *name*, which
4910    // covers both, so a grammar that starts emitting the pre-alias id
4911    // changes nothing — this census is what would notice it.
4912    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
4913    fn assert_cpp_comparison_folds_and_preproc_operands<P: MetricSuite>(
4914        binary_expression: u16,
4915        binary_expression2: u16,
4916        fold_expression: u16,
4917    ) {
4918        let src = "#if A <= B
4919            #endif
4920            #if C != D
4921            #endif
4922            template <typename... T> bool lt(T... a) { return (a < ...); }
4923            template <typename... T> bool le(T... a) { return (a <= ...); }
4924            template <typename... T> bool eq(T... a) { return (0 == ... == a); }
4925            template <typename... T> bool idiom(T... a) { return ((a == 0) && ...); }
4926            template <typename... T> bool neg(T... a) { return (true || ... || !a); }";
4927        assert_fixture_spells::<P>(
4928            src,
4929            "foo.cpp",
4930            &[
4931                (fold_expression, 5, "fold expressions"),
4932                (binary_expression2, 3, "binary expressions"),
4933                (binary_expression, 0, "pre-alias binary expressions"),
4934            ],
4935        );
4936        check_func_space::<P, _>(src, "foo.cpp", |space| {
4937            for (name, want) in [("lt", 1), ("le", 1), ("eq", 1), ("idiom", 2), ("neg", 2)] {
4938                assert_eq!(
4939                    child_space(&space, name).metrics.abc.conditions(),
4940                    want,
4941                    "fold in `{name}`"
4942                );
4943            }
4944            assert_eq!(
4945                space.metrics.abc.conditions(),
4946                2,
4947                "one condition per `#if` comparison"
4948            );
4949        });
4950    }
4951
4952    #[cfg(feature = "cpp")]
4953    #[test]
4954    fn cpp_comparison_folds_and_preproc_operands() {
4955        assert_cpp_comparison_folds_and_preproc_operands::<CppParser>(
4956            Cpp::BinaryExpression as u16,
4957            Cpp::BinaryExpression2 as u16,
4958            Cpp::FoldExpression as u16,
4959        );
4960    }
4961
4962    #[cfg(feature = "mozcpp")]
4963    #[test]
4964    fn mozcpp_comparison_folds_and_preproc_operands() {
4965        use crate::Mozcpp;
4966        assert_cpp_comparison_folds_and_preproc_operands::<crate::MozcppParser>(
4967            Mozcpp::BinaryExpression as u16,
4968            Mozcpp::BinaryExpression2 as u16,
4969            Mozcpp::FoldExpression as u16,
4970        );
4971    }
4972
4973    // #1383: a `relational_pattern`'s operator is not a condition of
4974    // its own — the `switch_expression_arm` that owns it already scores
4975    // the decision, exactly as it does for the constant arm `5 => 1`.
4976    // Counting both charged a relational arm twice, and twice C#'s own
4977    // cyclomatic decision count.
4978    //
4979    // Per `.claude/rules/grammar-dispatch.md` §11 the `operator <`
4980    // fixture above cannot prove this alone: its `is > 0` sits beside a
4981    // `binary_expression` comparison that supplies a plausible total on
4982    // its own. Here the pattern operators are the *only* `<` / `>` /
4983    // `>=` / `<=` in the file, so a readmitted `RelationalPattern`
4984    // parent is the only thing that can lift the count.
4985    //
4986    // Both spellings are covered, and asserted per member rather than
4987    // through a total, because they reached two different arms when this
4988    // was written: `n`'s `>` / `<` were gated by a `GT | LT` parent
4989    // allowlist and `g`'s `>=` / `<=` by a separate `RelationalPattern`
4990    // denial, so fixing one and not the other left the *other method* at
4991    // 4. #1420 merged all six comparison tokens onto the one allowlist,
4992    // which removes that particular way to get it half right — but the
4993    // per-member shape still earns its keep, since a total of 6 is what
4994    // both `{2, 2, 2}` and a regressed `{4, 2, 0}` report:
4995    //
4996    // | state | `n` | `g` | `c` |
4997    // |---|---|---|---|
4998    // | `relational_pattern` excluded (shipped) | 2 | 2 | 2 |
4999    // | readmitted to the allowlist             | 4 | 4 | 2 |
5000    //
5001    // `c` is the constant-pattern control and reads 2 in both rows: it is
5002    // what the relational methods are supposed to agree with.
5003    #[cfg(feature = "csharp")]
5004    #[test]
5005    fn csharp_relational_pattern_does_not_double_count_its_arm() {
5006        let src = "class A {
5007                int n(int x) => x switch { > 5 => 1, < 0 => 2, _ => 3 };
5008                int g(int x) => x switch { >= 5 => 1, <= 0 => 2, _ => 3 };
5009                int c(int x) => x switch { 5 => 1, 0 => 2, _ => 3 };
5010            }";
5011        assert_csharp_fixture_spells(
5012            src,
5013            &[
5014                (Csharp::RelationalPattern as u16, 4, "relational patterns"),
5015                (Csharp::GT as u16, 1, "`>`"),
5016                (Csharp::LT as u16, 1, "`<`"),
5017                (Csharp::GTEQ as u16, 1, "`>=`"),
5018                (Csharp::LTEQ as u16, 1, "`<=`"),
5019            ],
5020        );
5021        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5022            // Per space, not through the root: the root can only see
5023            // the sum, and a `{4, 2, 0}` regression sums to the same 6
5024            // as the correct `{2, 2, 2}`. `cyclomatic()` parity is also
5025            // only expressible per space — `cyclomatic_sum()` folds in
5026            // a base of 1 for each one.
5027            assert_every_member_scores(&space.spaces[0], 3, 2, "one condition per non-discard arm");
5028        });
5029    }
5030
5031    // The `is` half of #1383, and the one whose over-count came from a
5032    // different owner: `if (x is > 0)` scored the `IfStatement`
5033    // condition slot *and* the pattern's `>`. `f`'s guard is the
5034    // deliberate control — a `binary_expression` comparison in the same
5035    // slot still scores exactly one, so the fix cannot be read as
5036    // "stop counting `if` conditions".
5037    //
5038    // The two combinator methods pin the compound forms the issue asks
5039    // about. `and` / `or` are keyword tokens C# ABC never listed and C#
5040    // cyclomatic never counts, so the arm stays the single decision
5041    // however many relational operands it carries — which is exactly
5042    // what would regress if a later fix re-derived the gate from the
5043    // operand instead of the parent.
5044    #[cfg(feature = "csharp")]
5045    #[test]
5046    fn csharp_is_pattern_and_combinators_score_one_decision() {
5047        let src = "class A {
5048                int m(int x) { if (x is > 0) { return 1; } return 0; }
5049                int f(int x) { if (x > 0) { return 1; } return 0; }
5050                int a(int x) { if (x is > 0 and < 9) { return 1; } return 0; }
5051                int o(int x) { if (x is < 0 or >= 100) { return 1; } return 0; }
5052                int n(int x) { if (x is not > 5) { return 1; } return 0; }
5053            }";
5054        assert_csharp_fixture_spells(
5055            src,
5056            &[
5057                (Csharp::RelationalPattern as u16, 6, "relational patterns"),
5058                (Csharp::IsPatternExpression as u16, 4, "`is` pattern tests"),
5059                (Csharp::AndPattern as u16, 1, "`and`"),
5060                (Csharp::OrPattern as u16, 1, "`or`"),
5061                (Csharp::NegatedPattern as u16, 1, "`not`"),
5062                (Csharp::GTEQ as u16, 1, "`>=` (only `o` carries one)"),
5063            ],
5064        );
5065        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5066            assert_every_member_scores(&space.spaces[0], 5, 1, "the `if` is the only decision");
5067        });
5068    }
5069
5070    // #1383's second, quieter effect, and the one its issue does not
5071    // mention: a relational pattern outside any decision slot went from
5072    // 1 to 0 as well, because the gate is on the operator's parent and
5073    // not on what encloses the pattern. That left `q` — the equivalent
5074    // binary comparison `x > 5` in the same slot — scoring 1 against
5075    // these four's 0, so a relational pattern read one *lower* than the
5076    // comparison it is sugar for. #1383 recorded that as a deliberate
5077    // exception to Fitzpatrick Rule 5 and kept it, because the only
5078    // route it had was to readmit the pattern's `<` / `>` token, which
5079    // would have scored the decision-slot case twice.
5080    //
5081    // #1461 closed it from the other end, and the test now pins the
5082    // agreement rather than the exception. The enclosing
5083    // `is_pattern_expression` scores by use, so `x is > 5` reads 1
5084    // wherever it is written — level with `q`, and level with the plain
5085    // type test `x is int` (`t`, whose `is_expression` moved for the
5086    // same reason). The pattern's own `>` still scores nothing, which
5087    // is what keeps the whole test at 1 rather than 2: one relational
5088    // operation, one condition.
5089    //
5090    // The four probes stay because they are four different enclosings —
5091    // a declarator, a bare `return`, an argument, a type test — and the
5092    // point was never that the pattern is special in one of them.
5093    #[cfg(feature = "csharp")]
5094    #[test]
5095    fn csharp_relational_pattern_scores_one_wherever_it_is_written() {
5096        let src = "class A {
5097                static bool M(bool b) { return b; }
5098                bool p(int x) { bool b = x is > 5; return b; }
5099                bool q(int x) { bool b = x > 5; return b; }
5100                bool r(int x) { return x is > 5; }
5101                bool s(int x) { return M(x is > 5); }
5102                bool t(object x) { bool b = x is int; return b; }
5103            }";
5104        assert_csharp_fixture_spells(
5105            src,
5106            &[
5107                (Csharp::RelationalPattern as u16, 3, "relational patterns"),
5108                (Csharp::IsPatternExpression as u16, 3, "`is` pattern tests"),
5109                (Csharp::IsExpression as u16, 1, "the `is int` control"),
5110            ],
5111        );
5112        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5113            let class = &space.spaces[0];
5114            assert_eq!(class.spaces.len(), 6, "`M` plus five probes");
5115            let by_name = |n: &str| {
5116                class
5117                    .spaces
5118                    .iter()
5119                    .find(|s| s.name.as_deref() == Some(n))
5120                    .unwrap_or_else(|| panic!("fixture lost `{n}`"))
5121                    .metrics
5122                    .abc
5123                    .conditions()
5124            };
5125            // Named rather than indexed: the claim is about which
5126            // spelling scores what, so a reordering of the fixture must
5127            // not silently re-point the assertions.
5128            for probe in ["p", "r", "s"] {
5129                assert_eq!(by_name(probe), 1, "`{probe}`: the `is` test, once");
5130            }
5131            assert_eq!(by_name("t"), 1, "the bare type test, the same once");
5132            assert_eq!(by_name("q"), 1, "the comparison it is sugar for");
5133            assert_eq!(by_name("M"), 0, "no relational operator anywhere");
5134        });
5135    }
5136
5137    // The boundary of #1383: the pattern's operator stops counting, a
5138    // *binary* operator in the arm's `when` guard keeps counting (`w`).
5139    // Both sit under the same `switch_expression_arm`, so this is what
5140    // stops a later "patterns don't count" pass from suppressing the
5141    // guard too.
5142    //
5143    // A relational *pattern* in the guard (`g`) is the other side of that
5144    // line: the operator itself still scores nothing, because the gate is
5145    // on the operator's parent — but since #1422 the guard is a condition
5146    // slot, and an `is` test scores itself (`csharp_condition_scores_itself`),
5147    // so the slot does not pay again. That is what closed the gap this test used to
5148    // carry a `FIXME(#1422)` for: `when n is > 5` now reads level with
5149    // `when n > 5` instead of one below it.
5150    //
5151    // §8 holds throughout, which it did not before #1422. The old table
5152    // here recorded the defect: neither metric modelled the guard, so
5153    // ABC's count was whatever operator token happened to sit inside it
5154    // and cyclomatic's was nothing at all. Both tiers measured, before
5155    // and after:
5156    //
5157    // | guard | `conditions()` | `cyclomatic() - 1` |
5158    // |---|---|---|
5159    // | `when x % 2 == 0` (this fixture) | 3 → 3 | 2 → 3 |
5160    // | `when x > 2`                     | 3 → 3 | 2 → 3 |
5161    // | `when IsEven(x)`                 | 2 → 3 | 2 → 3 |
5162    //
5163    // The `==` is the guard's own operator and the only `==` in the
5164    // file, so trimming the `when` clause out of `w` drops its count to
5165    // 2 rather than leaving the assertion satisfied by something else;
5166    // `assert_csharp_fixture_spells` pins both guards by kind against
5167    // the same decay.
5168    #[cfg(feature = "csharp")]
5169    #[test]
5170    fn csharp_switch_arm_guard_operator_still_counts() {
5171        let src = "class A {
5172                int w(int x) => x switch { > 0 when x % 2 == 0 => 1, > 0 => 2, _ => 3 };
5173                int g(int x) => x switch { int n when n is > 5 => 1, _ => 0 };
5174            }";
5175        assert_csharp_fixture_spells(
5176            src,
5177            &[
5178                (Csharp::WhenClause as u16, 2, "`when` guards"),
5179                (Csharp::EQEQ as u16, 1, "the guard's `==`"),
5180                (Csharp::RelationalPattern as u16, 3, "relational patterns"),
5181            ],
5182        );
5183        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5184            let m = &space.spaces[0].spaces[0];
5185            assert_eq!(m.name.as_deref(), Some("w"));
5186            // 4 if the guard slot started counting a `binary_expression`
5187            // on top of its `==`, 2 if the `==` were suppressed along
5188            // with the pattern operators — both are live regressions.
5189            assert_eq!(
5190                m.metrics.abc.conditions(),
5191                3,
5192                "two arms plus the guard's `==`"
5193            );
5194            assert_eq!(m.metrics.cyclomatic.cyclomatic(), 4);
5195
5196            let g = &space.spaces[0].spaces[1];
5197            assert_eq!(g.name.as_deref(), Some("g"));
5198            assert_eq!(
5199                g.metrics.abc.conditions(),
5200                2,
5201                "one arm plus the guard slot, which scores the `is` test"
5202            );
5203            assert_eq!(g.metrics.cyclomatic.cyclomatic(), 3);
5204        });
5205    }
5206
5207    // #1422's headline: four spellings of one guard, previously worth
5208    // two different ABC numbers because nothing modelled the guard and
5209    // ABC counted whatever operator token happened to sit inside it —
5210    // `when x % 2 == 0` and `when x > 2` scored 1 through the
5211    // comparison-token arm, `when IsEven(x)` scored 0. Modelled as a
5212    // condition slot each contributes exactly 1, and the cyclomatic
5213    // `WhenClause` arm lifts the decision count to match.
5214    //
5215    // `none` is the unguarded control and the anchor: a two-arm switch
5216    // over the same relational patterns carrying no guard at all, so
5217    // every member here is exactly one guard away from it. Without that
5218    // row a fix that scored the *arms* differently would satisfy the
5219    // four guarded rows alike. Its arms run `> 2` before `> 0` because
5220    // the reverse order is a C# compile error (CS8510, the later arm
5221    // subsumed) — the guarded members escape that only because a guard
5222    // can fail, which is what makes their repeated `> 0` legal.
5223    //
5224    // `paren` is not a fourth spelling for its own sake — it is the only
5225    // member whose guard reaches the slot as a
5226    // `parenthesized_expression` for the peel (`csharp_wrapper_operand`)
5227    // to take off, since `when_clause` wraps a parenthesised guard in a
5228    // real `parenthesized_expression` rather than the anonymous parens
5229    // `catch_filter_clause` uses.
5230    //
5231    // `nullc` is #1459's row and the counter-example that falsified the
5232    // claim above when it was first written. The slot leaves an
5233    // operator guard to the arm that already counts the operator, and
5234    // no ABC arm counted `??`, so this one spelling read 2 where the
5235    // other four read 3. Counting the token levels it without touching
5236    // the slot; an unconditional `+1` in the slot instead would have
5237    // taken `cmp` to 4, re-creating the double count #1422 removed.
5238    //
5239    // It is the one guarded member whose cyclomatic is not 4, and that
5240    // is not a discrepancy to fix: C# cyclomatic counts `??` as a
5241    // decision *in addition to* the guard clause, so the arm below
5242    // narrows the ABC-versus-cyclomatic gap from two to one rather than
5243    // closing it. The remaining one is the slot's standing policy of
5244    // leaving a counted operator to its arm, and `cmp` pays it
5245    // too — it just happens to break even there.
5246    #[cfg(feature = "csharp")]
5247    #[test]
5248    fn csharp_switch_arm_guard_scores_one_condition_however_spelled() {
5249        let src = "class A {
5250                static bool IsEven(int x) { return true; }
5251                int tok(int x) => x switch { > 0 when x % 2 == 0 => 1, > 0 => 2, _ => 3 };
5252                int cmp(int x) => x switch { > 0 when x > 2 => 1, > 0 => 2, _ => 3 };
5253                int call(int x) => x switch { > 0 when IsEven(x) => 1, > 0 => 2, _ => 3 };
5254                int paren(int x) => x switch { > 0 when (IsEven(x)) => 1, > 0 => 2, _ => 3 };
5255                int nullc(int x, bool? b) => x switch { > 0 when b ?? false => 1, > 0 => 2, _ => 3 };
5256                int none(int x) => x switch { > 2 => 1, > 0 => 2, _ => 3 };
5257            }";
5258        assert_csharp_fixture_spells(
5259            src,
5260            &[
5261                (Csharp::WhenClause as u16, 5, "`when` guards"),
5262                (Csharp::EQEQ as u16, 1, "`tok`'s `==`"),
5263                (Csharp::QMARKQMARK as u16, 1, "`nullc`'s `??`"),
5264                (
5265                    Csharp::ParenthesizedExpression as u16,
5266                    1,
5267                    "`paren`'s parenthesised guard",
5268                ),
5269                // By the call's *argument list*, which carries no
5270                // numeric-suffix alias, rather than by
5271                // `invocation_expression`, which has three
5272                // (`InvocationExpression` / `2` / `3`) and would pin this
5273                // anchor to whichever one the pinned grammar happens to
5274                // emit. `call` and `paren` hold the only two argument
5275                // lists in the fixture — `IsEven(int x)` is a
5276                // `parameter_list`.
5277                (Csharp::ArgumentList as u16, 2, "the `IsEven` calls"),
5278            ],
5279        );
5280        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5281            assert_members_score(
5282                &space.spaces[0],
5283                &[
5284                    ("IsEven", 0, 1),
5285                    ("tok", 3, 4),
5286                    ("cmp", 3, 4),
5287                    ("call", 3, 4),
5288                    ("paren", 3, 4),
5289                    // Was 2 — the guard spelling nothing counted.
5290                    // Cyclomatic 5 rather than 4 because `??` is a
5291                    // decision there on top of the guard clause.
5292                    ("nullc", 3, 5),
5293                    ("none", 2, 3),
5294                ],
5295            );
5296        });
5297    }
5298
5299    // The bare type test `x is int` is `is_expression` (391); only once
5300    // a pattern is involved (`x is int y`, `x is null`) does the grammar
5301    // emit `is_pattern_expression` (392). They are distinct kinds, and
5302    // the former `csharp_bool_terminal_kinds!()` listed only the
5303    // second, so the two spellings of one test disagreed: `if (x is int)` scored zero
5304    // conditions against a cyclomatic decision of one while
5305    // `if (x is int y)` scored one.
5306    //
5307    // Found by the whole-branch review of this batch, and it falsified
5308    // #1422's own claim in the arm above that a guard "scores one
5309    // however it is spelled" — `when x is int` read 1 where
5310    // `when IsEven(x)` read 2, which is the spelling-dependence that
5311    // fix exists to remove. It is the C# half of the gap #1421 closed
5312    // for Kotlin by adding `IsExpression | InExpression` there. Both
5313    // languages' type tests have since moved out of the terminal set to
5314    // an unconditional arm (#1461), which leaves every number here
5315    // unchanged — the slot no longer counts the test and the arm does.
5316    //
5317    // The plain `if` members are the control that keeps this honest: a
5318    // guard-only fixture could be satisfied by a guard-specific rule,
5319    // and the defect was never guard-specific. `q` and `iq` are the
5320    // already-correct `is_pattern_expression` twins, so a regression
5321    // that reintroduced the asymmetry fails on the pair rather than on
5322    // an absolute number.
5323    #[cfg(feature = "csharp")]
5324    #[test]
5325    fn csharp_bare_is_type_test_scores_one_condition() {
5326        let src = "class A {
5327                static bool IsEven(int x) { return true; }
5328                int g(object x) => x switch { object o when x is int => 1, _ => 0 };
5329                int gq(object x) => x switch { object o when x is int y => 1, _ => 0 };
5330                int gc(object x) => x switch { object o when IsEven(1) => 1, _ => 0 };
5331                int p(object x) { if (x is int) { return 1; } return 0; }
5332                int q(object x) { if (x is int y) { return 1; } return 0; }
5333            }";
5334        assert_csharp_fixture_spells(
5335            src,
5336            &[
5337                (Csharp::IsExpression as u16, 2, "the bare `is` type tests"),
5338                (
5339                    Csharp::IsPatternExpression as u16,
5340                    2,
5341                    "the designation-pattern twins",
5342                ),
5343                (Csharp::WhenClause as u16, 3, "`when` guards"),
5344            ],
5345        );
5346        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5347            assert_members_score(
5348                &space.spaces[0],
5349                &[
5350                    ("IsEven", 0, 1),
5351                    // Level with `gq` and `gc`: every guard spelling
5352                    // scores one, which is what #1422 claims.
5353                    ("g", 2, 3),
5354                    ("gq", 2, 3),
5355                    ("gc", 2, 3),
5356                    // Level with `q`: the two spellings of one type
5357                    // test agree outside a guard too.
5358                    ("p", 1, 2),
5359                    ("q", 1, 2),
5360                ],
5361            );
5362        });
5363    }
5364
5365    // The null-forgiving `b!` is transparent: it scores whatever the
5366    // operand it wraps scores (#1463).
5367    //
5368    // `postfix_unary_expression` was in neither
5369    // the former `csharp_bool_terminal_kinds!()` nor the wrapper peel,
5370    // so every spelling below scored **zero** conditions against a cyclomatic
5371    // decision of one, while the bare `b` control scored one. In a
5372    // codebase with nullable reference types enabled the suffix is
5373    // everywhere, so the gap is not an exotic corner.
5374    //
5375    // Both walker paths, per `.claude/rules/grammar-dispatch.md` §11:
5376    // `p*` go through the `if` predicate slot
5377    // (`csharp_count_condition`) and `c*` through the `&&` chain slot
5378    // (also `csharp_count_condition`, per chain operand), which reach
5379    // the peel independently. A fixture of only one
5380    // reads correct with the other path dead.
5381    //
5382    // The controls are load-bearing in both directions. `p` / `c` pin
5383    // what the wrapper must score, so a regression fails on the pair
5384    // rather than on an absolute number; and the cyclomatic column pins
5385    // that only `conditions` moved — the suffix is not a decision in any
5386    // language, so a fix that moved both together would be a different
5387    // bug.
5388    #[cfg(feature = "csharp")]
5389    #[test]
5390    fn csharp_null_forgiving_operand_scores_like_its_operand() {
5391        let src = "class A {
5392                int p(bool b) { if (b) { return 1; } return 0; }
5393                int pn(bool b) { if (b!) { return 1; } return 0; }
5394                int pp(bool b) { if ((b!)) { return 1; } return 0; }
5395                int pd(bool b) { if (b!!) { return 1; } return 0; }
5396                int pb(bool b) { if (!b!) { return 1; } return 0; }
5397                int c(bool b) { if (b && b) { return 1; } return 0; }
5398                int cn(bool b) { if (b! && b) { return 1; } return 0; }
5399                int cp(bool b) { if ((b!) && b) { return 1; } return 0; }
5400            }";
5401        assert_csharp_fixture_spells(
5402            src,
5403            &[
5404                // Seven, not five: `b!!` is two nested nodes, and each
5405                // chain member carries one. Trimming a `!` out of any
5406                // member fails here by name instead of turning that
5407                // member into a silent copy of its control.
5408                (
5409                    Csharp::PostfixUnaryExpression as u16,
5410                    7,
5411                    "the null-forgiving suffixes",
5412                ),
5413                // The `if (…)` parens are anonymous tokens rather than
5414                // nodes in this grammar, so these two are `pp`'s and
5415                // `cp`'s explicit ones — the wrappers that make the peel
5416                // chain rather than peel once.
5417                (
5418                    Csharp::ParenthesizedExpression as u16,
5419                    2,
5420                    "the explicit parenthesised spellings",
5421                ),
5422                (Csharp::PrefixUnaryExpression as u16, 1, "`pb`'s `!`"),
5423            ],
5424        );
5425        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5426            assert_members_score(
5427                &space.spaces[0],
5428                &[
5429                    ("p", 1, 2),
5430                    ("pn", 1, 2),
5431                    ("pp", 1, 2),
5432                    ("pd", 1, 2),
5433                    ("pb", 1, 2),
5434                    ("c", 2, 3),
5435                    ("cn", 2, 3),
5436                    ("cp", 2, 3),
5437                ],
5438            );
5439        });
5440    }
5441
5442    // The null-forgiving operand is read by position, but from the end
5443    // the grammar cannot move: a `postfix_unary_expression` *starts* at
5444    // its operand, so `child(0)` is the operand whatever `extra` sits
5445    // between it and the operator. The operator is identified by
5446    // membership rather than by index for the same reason.
5447    //
5448    // The other two wrappers name no field either — the C# grammar gives
5449    // `parenthesized_expression`, `prefix_unary_expression` and
5450    // `postfix_unary_expression` an empty `fields` map in
5451    // node-types.json — so `child(1)` was a comment whenever one was
5452    // written there, and `nc` / `rc` scored zero. They read their
5453    // operand as the first one that is not an extra since #1455, so all
5454    // three pairs agree with their controls.
5455    #[cfg(feature = "csharp")]
5456    #[test]
5457    fn csharp_null_forgiving_operand_survives_an_interposed_comment() {
5458        let src = "class A {
5459                int p(bool b) { if (b!) { return 1; } return 0; }
5460                int pc(bool b) { if (b /*c*/ !) { return 1; } return 0; }
5461                int n(bool b) { if (!b) { return 1; } return 0; }
5462                int nc(bool b) { if (! /*c*/ b) { return 1; } return 0; }
5463                int r(bool b) { if ((b)) { return 1; } return 0; }
5464                int rc(bool b) { if (( /*c*/ b)) { return 1; } return 0; }
5465            }";
5466        assert_csharp_fixture_spells(
5467            src,
5468            &[
5469                (
5470                    Csharp::Comment as u16,
5471                    3,
5472                    "the interposed comments — without them every row is its own control",
5473                ),
5474                (Csharp::PostfixUnaryExpression as u16, 2, "`p` and `pc`"),
5475                (Csharp::PrefixUnaryExpression as u16, 2, "`n` and `nc`"),
5476                (
5477                    Csharp::ParenthesizedExpression as u16,
5478                    2,
5479                    "`r` and `rc` — the `if` parens are anonymous tokens",
5480                ),
5481            ],
5482        );
5483        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5484            assert_members_score(
5485                &space.spaces[0],
5486                &[
5487                    ("p", 1, 2),
5488                    // The row #1463 fixes.
5489                    ("pc", 1, 2),
5490                    ("n", 1, 2),
5491                    // #1455: `child(1)` of the prefix wrapper is the
5492                    // comment.
5493                    ("nc", 1, 2),
5494                    ("r", 1, 2),
5495                    // #1455 again, through the paren wrapper.
5496                    ("rc", 1, 2),
5497                ],
5498            );
5499        });
5500    }
5501
5502    // The statement `switch` reaches the same `when_clause` kind through
5503    // a different parent — `switch_section` rather than
5504    // `switch_expression_arm` — which makes it an independent path in
5505    // the sense of `.claude/rules/grammar-dispatch.md` §11: the arms
5506    // themselves are counted by two different owners (a `Case` token
5507    // here, the `SwitchExpressionArm` node there), so a guard rule
5508    // written against one shape could be dead for the other and every
5509    // expression-form fixture would still read correct.
5510    #[cfg(feature = "csharp")]
5511    #[test]
5512    fn csharp_statement_switch_section_guard_counts() {
5513        let src = "class A {
5514                static bool IsEven(int x) { return true; }
5515                int guarded(int x) {
5516                    switch (x) {
5517                        case > 0 when IsEven(x): return 1;
5518                        case > 0: return 2;
5519                        default: return 3;
5520                    }
5521                }
5522                int plain(int x) {
5523                    switch (x) {
5524                        case > 2: return 1;
5525                        case > 0: return 2;
5526                        default: return 3;
5527                    }
5528                }
5529            }";
5530        assert_csharp_fixture_spells(
5531            src,
5532            &[
5533                (Csharp::WhenClause as u16, 1, "the `case … when` guard"),
5534                (Csharp::SwitchSection as u16, 6, "switch sections"),
5535                (
5536                    Csharp::SwitchExpressionArm as u16,
5537                    0,
5538                    "expression arms (this fixture is the statement form)",
5539                ),
5540            ],
5541        );
5542        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5543            assert_members_score(
5544                &space.spaces[0],
5545                &[("IsEven", 0, 1), ("guarded", 3, 4), ("plain", 2, 3)],
5546            );
5547        });
5548    }
5549
5550    // The sibling #1422's issue body does not mention, measured to have
5551    // the identical defect: `catch (E e) when (…)` is a second guard
5552    // spelling on its own `catch_filter_clause` kind, and it scored
5553    // conditions 3 / 2 / 2 for the comparison / call spellings and the
5554    // unfiltered control while `cyclomatic()` sat at 2 for all three.
5555    //
5556    // ABC runs one above `cyclomatic() - 1` on every row, guarded and
5557    // unguarded alike, and that offset is not this fix's: C# ABC counts
5558    // the `Try` token as a condition (Fitzpatrick Rule 5, the same
5559    // family as `else`) where cyclomatic counts only the `catch`. The
5560    // guard moves both tiers by exactly one, which is what `none`
5561    // anchors — the offset is a constant here, not something a guard
5562    // introduces.
5563    //
5564    // `paren` carries the *double* parenthesis on purpose. The grammar
5565    // spells `catch_filter_clause`'s own parentheses as anonymous tokens
5566    // the way `if_statement` does, so `when (IsEven(x))` hands the slot a
5567    // bare `invocation_expression` with no parenthesis for the peel
5568    // (`csharp_wrapper_operand`) to take off; only a
5569    // second pair of parentheses produces a `parenthesized_expression`
5570    // there.
5571    #[cfg(feature = "csharp")]
5572    #[test]
5573    fn csharp_catch_filter_guard_scores_one_condition_however_spelled() {
5574        let src = "class A {
5575                static bool IsEven(int x) { return true; }
5576                int tok(int x) { try { return 1; } catch (System.Exception e) when (x % 2 == 0) { return 2; } }
5577                int cmp(int x) { try { return 1; } catch (System.Exception e) when (x > 0) { return 2; } }
5578                int call(int x) { try { return 1; } catch (System.Exception e) when (IsEven(x)) { return 2; } }
5579                int paren(int x) { try { return 1; } catch (System.Exception e) when ((IsEven(x))) { return 2; } }
5580                int none(int x) { try { return 1; } catch (System.Exception e) { return 2; } }
5581            }";
5582        assert_csharp_fixture_spells(
5583            src,
5584            &[
5585                (
5586                    Csharp::CatchFilterClause as u16,
5587                    4,
5588                    "`catch … when` filters",
5589                ),
5590                (Csharp::CatchClause as u16, 5, "catch clauses"),
5591                (Csharp::WhenClause as u16, 0, "switch guards (none here)"),
5592                (
5593                    Csharp::ParenthesizedExpression as u16,
5594                    1,
5595                    "`paren`'s inner parentheses",
5596                ),
5597            ],
5598        );
5599        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5600            assert_members_score(
5601                &space.spaces[0],
5602                &[
5603                    ("IsEven", 0, 1),
5604                    ("tok", 3, 3),
5605                    ("cmp", 3, 3),
5606                    ("call", 3, 3),
5607                    ("paren", 3, 3),
5608                    ("none", 2, 2),
5609                ],
5610            );
5611        });
5612    }
5613
5614    // A guarded discard is where the two rules meet. `_ when g =>` is
5615    // already exempt from the `default:` exclusion —
5616    // `csharp_switch_expression_arm_is_bare_discard` looks for a
5617    // `when_clause` for exactly this reason — so after #1422 it scores
5618    // twice: once as an arm that is no longer unconditional, once for
5619    // the guard that makes it conditional. `bare` is the control that
5620    // keeps the exclusion itself pinned: drop the guard and the arm goes
5621    // back to costing nothing.
5622    #[cfg(feature = "csharp")]
5623    #[test]
5624    fn csharp_guarded_discard_arm_scores_arm_and_guard() {
5625        let src = "class A {
5626                static bool IsEven(int x) { return true; }
5627                int guarded(int x) => x switch { > 0 => 1, _ when IsEven(x) => 2, _ => 3 };
5628                int bare(int x) => x switch { > 0 => 1, _ => 3 };
5629            }";
5630        assert_csharp_fixture_spells(
5631            src,
5632            &[
5633                (Csharp::WhenClause as u16, 1, "the guarded discard"),
5634                (Csharp::Discard as u16, 3, "discard patterns"),
5635            ],
5636        );
5637        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5638            assert_members_score(
5639                &space.spaces[0],
5640                &[("IsEven", 0, 1), ("guarded", 3, 4), ("bare", 1, 2)],
5641            );
5642        });
5643    }
5644
5645    // A comment inside the guard must not cost it its condition.
5646    // tree-sitter `extra`s are *named* nodes, so `comment` can be the
5647    // first named child of either guard clause — and a slot that read
5648    // only the first named child scored `when /*c*/ IsEven(x)` as 1
5649    // where `when IsEven(x)` scores 2, silently reinstating the very
5650    // spelling-dependence #1422 removes and breaking the §8 parity the
5651    // fix establishes. Found in review of #1422 before it shipped.
5652    //
5653    // This is the C# instance of the class #1181 fixed for the ternary,
5654    // where a comment between a token and its operand shifted every
5655    // positional read. Each commented member is asserted against the
5656    // same literal pair as its uncommented twin in the same table, so a
5657    // regression that moved only the commented spelling fails here; the
5658    // literals themselves are pinned by
5659    // `csharp_switch_arm_guard_scores_one_condition_however_spelled` and
5660    // its catch-filter sibling.
5661    #[cfg(feature = "csharp")]
5662    #[test]
5663    fn csharp_guard_keeps_its_condition_across_a_comment() {
5664        let src = "class A {
5665                static bool IsEven(int x) => true;
5666                int plain(int x) => x switch { > 0 when IsEven(x) => 1, _ => 0 };
5667                int cmt(int x) => x switch { > 0 when /*c*/ IsEven(x) => 1, _ => 0 };
5668                int cplain(int x) { try { return 1; } catch (System.Exception e) when (IsEven(x)) { return 2; } }
5669                int ccmt(int x) { try { return 1; } catch (System.Exception e) when (/*c*/ IsEven(x)) { return 2; } }
5670            }";
5671        assert_csharp_fixture_spells(
5672            src,
5673            &[
5674                (Csharp::Comment as u16, 2, "the in-guard comments"),
5675                (Csharp::WhenClause as u16, 2, "switch guards"),
5676                (Csharp::CatchFilterClause as u16, 2, "catch filters"),
5677            ],
5678        );
5679        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5680            assert_members_score(
5681                &space.spaces[0],
5682                &[
5683                    ("IsEven", 0, 1),
5684                    ("plain", 2, 3),
5685                    ("cmt", 2, 3),
5686                    ("cplain", 3, 3),
5687                    ("ccmt", 3, 3),
5688                ],
5689            );
5690        });
5691    }
5692
5693    // Why the guard is a condition *slot* and not a flat +1: a compound
5694    // guard keeps its sub-structure. `when a > 1 && b < 2` scores the
5695    // two comparisons through the token arm and nothing extra from the
5696    // slot, because a `&&` chain scores itself
5697    // (`csharp_condition_scores_itself`) — so it reads one above `single`'s
5698    // lone comparison rather than collapsing to the same number, and
5699    // cyclomatic agrees because it counts the `&&`.
5700    #[cfg(feature = "csharp")]
5701    #[test]
5702    fn csharp_compound_guard_keeps_its_sub_structure() {
5703        let src = "class A {
5704                int compound(int a, int b) => a switch { > 0 when a > 1 && b < 2 => 1, _ => 0 };
5705                int single(int a, int b) => a switch { > 0 when a > 1 => 1, _ => 0 };
5706            }";
5707        assert_csharp_fixture_spells(
5708            src,
5709            &[
5710                (Csharp::WhenClause as u16, 2, "`when` guards"),
5711                (Csharp::AMPAMP as u16, 1, "`compound`'s `&&`"),
5712                (Csharp::LT as u16, 1, "`compound`'s `<`"),
5713            ],
5714        );
5715        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5716            assert_members_score(&space.spaces[0], &[("compound", 3, 4), ("single", 2, 3)]);
5717        });
5718    }
5719
5720    // #1275: tree-sitter-c-sharp spells `int?`, `where T : class?`, the
5721    // ternary and `a?.b` with one and the same bare `?` token, so the
5722    // unguarded `QMARK` arm scored the two type-syntax forms as
5723    // decisions.
5724    //
5725    // The fixture carries two `nullable_type` `?`, one
5726    // `type_parameter_constraint` `?`, and one real ternary over a real
5727    // `>` comparison, so every way of getting the gate wrong lands on
5728    // its own number: 5 pre-fix, 3 if only `NullableType` is denied, 4
5729    // if only `TypeParameterConstraint` is, 1 if the gate swallows the
5730    // genuine ternary, 0 if the fixture stops parsing. Asserting 0 on a
5731    // nullable-only body would have been vacuous — an unparsable file
5732    // scores 0 too.
5733    #[cfg(feature = "csharp")]
5734    #[test]
5735    fn csharp_nullable_type_syntax_is_not_a_condition() {
5736        check_metrics::<CsharpParser>(
5737            "class A {
5738                int M<T>(int? p, int a, int b) where T : class? {
5739                    int? q = null;
5740                    return a > b ? 1 : 2;
5741                }
5742            }",
5743            "foo.cs",
5744            |metric| {
5745                assert_eq!(metric.abc.conditions_sum(), 2);
5746            },
5747        );
5748    }
5749
5750    // The polarity pin for #1275's C# half. `csharp_count_token_condition`
5751    // denies the two type-syntax parents rather than allowing the two
5752    // decision-bearing ones, so that `a?.b` and `a?[0]` keep counting by
5753    // construction: both spell their operator as the same bare `?` under
5754    // `conditional_access_expression`, and C# cyclomatic counts that node
5755    // (`safe_navigation_chain_parity` pins C# at one decision per
5756    // operator alongside every other safe-navigation language).
5757    //
5758    // This test is what stops a later "make all three languages
5759    // consistent" pass from flipping C# to a `ConditionalExpression`
5760    // allowlist: that would silently drop both `?` counts here while
5761    // every other ABC test still passed. The third condition is the
5762    // `??`, which #1459 added to close the gap this comment used to
5763    // record as out of scope for #1275 — so a flipped `QMARK` polarity
5764    // now reads 1 rather than 0, and the discrimination is unchanged.
5765    #[cfg(feature = "csharp")]
5766    #[test]
5767    fn csharp_conditional_access_still_counts_as_a_condition() {
5768        let src = "class A {
5769                object M(string s, int[] xs) {
5770                    return s?.Length ?? xs?[0];
5771                }
5772            }";
5773        // The total is now fed by two token arms rather than one, so it
5774        // carries the per-source claim only alongside these counts: 3 is
5775        // two bare `?` plus one `??`, and without them a `QMARK` arm
5776        // that had stopped counting `a?[0]` while `??` gained a second
5777        // count elsewhere would still read 3.
5778        assert_csharp_fixture_spells(
5779            src,
5780            &[
5781                (Csharp::QMARK as u16, 2, "the `?.` and `?[` operators"),
5782                (Csharp::QMARKQMARK as u16, 1, "the `??`"),
5783            ],
5784        );
5785        check_metrics::<CsharpParser>(src, "foo.cs", |metric| {
5786            assert_eq!(metric.abc.conditions_sum(), 3);
5787        });
5788    }
5789
5790    // The `QMARKQMARK` arm away from any guard, where nothing else can
5791    // supply the count (#1459). `coalesce`'s body holds no comparison,
5792    // no call and no control-flow keyword, so its one condition is the
5793    // `??` and nothing else; it read 0 before the arm while C#
5794    // cyclomatic scored the decision, which is the direction ABC is not
5795    // allowed to sit on.
5796    //
5797    // `plain` is the control that keeps this from being an assertion
5798    // about `return` or about the method shape: same body without the
5799    // operator, 0 conditions and 1 decision.
5800    #[cfg(feature = "csharp")]
5801    #[test]
5802    fn csharp_null_coalescing_is_a_condition() {
5803        let src = "class A {
5804                int coalesce(int? x) { return x ?? 0; }
5805                int plain(int x) { return x; }
5806            }";
5807        assert_csharp_fixture_spells(src, &[(Csharp::QMARKQMARK as u16, 1, "the `??`")]);
5808        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5809            assert_members_score(&space.spaces[0], &[("coalesce", 1, 2), ("plain", 0, 1)]);
5810        });
5811    }
5812
5813    // The other direction of #1275's C# gate, isolated: narrowing
5814    // `QMARK` must not swallow a real ternary. The fixture carries no
5815    // comparison operator at all, so the ternary is the only thing that
5816    // can put the count above the walker's own contribution — in the
5817    // test above, a surviving `>` would have masked an over-suppressed
5818    // `?` at 1 instead of 0 (grammar-dispatch §11). A nullable
5819    // parameter keeps the denied production live in the same parse, so
5820    // one fixture proves both directions: 3 pre-fix, 2 after, 1 if the
5821    // allowlist/denylist is aimed at `ConditionalExpression`.
5822    //
5823    // The 2 is the `?` token plus the ternary's condition slot, which
5824    // `csharp_walk_for_conditions` counts as a Fitzpatrick unary
5825    // condition. That is why this is not written as an
5826    // `assert_deepest_conditions_match_cyclomatic` parity pin the way
5827    // the Java `<` / `>` sibling is: C# cyclomatic scores this method 1
5828    // decision, and the divergence is the pre-existing unary-condition
5829    // rule, not the `?` gate.
5830    #[cfg(feature = "csharp")]
5831    #[test]
5832    fn csharp_ternary_still_counts_alongside_nullable_types() {
5833        check_metrics::<CsharpParser>(
5834            "class A {
5835                int M(int? n, bool c) {
5836                    return c ? 1 : 2;
5837                }
5838            }",
5839            "foo.cs",
5840            |metric| {
5841                assert_eq!(metric.abc.conditions_sum(), 2);
5842            },
5843        );
5844    }
5845
5846    #[cfg(feature = "csharp")]
5847    #[test]
5848    fn csharp_aliased_invocation_expression_branches() {
5849        // Regression for issue #94 (lesson #2): the C# grammar emits three
5850        // aliased `kind_id`s for `invocation_expression`. Code that matches
5851        // only the unsuffixed `Csharp::InvocationExpression` undercounts ABC
5852        // branches whenever the AST emits an aliased variant. The three
5853        // method calls live in `M`, so the per-method maximum (visible at
5854        // the unit-space aggregate as `branches_max`) must be 3.
5855        check_metrics::<CsharpParser>(
5856            "class A {
5857                void M() {
5858                    System.Console.WriteLine(1);
5859                    System.Console.WriteLine(2);
5860                    System.Console.WriteLine(3);
5861                }
5862            }",
5863            "foo.cs",
5864            |metric| {
5865                assert_eq!(metric.abc.branches_max(), 3);
5866                assert_eq!(metric.abc.conditions_max(), 0);
5867            },
5868        );
5869    }
5870
5871    #[cfg(feature = "php")]
5872    #[test]
5873    fn php_zero_abc() {
5874        check_metrics::<PhpParser>("<?php\n", "foo.php", |metric| {
5875            assert_eq!(metric.abc.assignments_sum(), 0);
5876            assert_eq!(metric.abc.branches_sum(), 0);
5877            assert_eq!(metric.abc.conditions_sum(), 0);
5878            insta::assert_json_snapshot!(metric.abc);
5879        });
5880    }
5881
5882    #[cfg(feature = "php")]
5883    #[test]
5884    fn php_simple_assignment() {
5885        check_metrics::<PhpParser>(
5886            "<?php
5887function f(): void {
5888    $a = 1;
5889    $b = 2;
5890}",
5891            "foo.php",
5892            |metric| insta::assert_json_snapshot!(metric.abc),
5893        );
5894    }
5895
5896    #[cfg(feature = "php")]
5897    #[test]
5898    fn php_augmented_assignment() {
5899        check_metrics::<PhpParser>(
5900            "<?php
5901function f(int $x): int {
5902    $a = 0;
5903    $a += $x;
5904    $a -= 1;
5905    $a *= 2;
5906    return $a;
5907}",
5908            "foo.php",
5909            |metric| insta::assert_json_snapshot!(metric.abc),
5910        );
5911    }
5912
5913    #[cfg(feature = "php")]
5914    #[test]
5915    fn php_const_excluded() {
5916        // Constant declarations and enum cases are NOT counted as
5917        // assignments — they declare immutable values.
5918        check_metrics::<PhpParser>(
5919            "<?php
5920class A {
5921    const PI = 3.14;
5922    const E = 2.71;
5923}
5924enum Color {
5925    case Red;
5926    case Green;
5927}",
5928            "foo.php",
5929            |metric| insta::assert_json_snapshot!(metric.abc),
5930        );
5931    }
5932
5933    #[cfg(feature = "php")]
5934    #[test]
5935    fn php_function_call() {
5936        check_metrics::<PhpParser>(
5937            "<?php
5938function f(): void {
5939    foo();
5940    bar(1, 2);
5941}",
5942            "foo.php",
5943            |metric| insta::assert_json_snapshot!(metric.abc),
5944        );
5945    }
5946
5947    #[cfg(feature = "php")]
5948    #[test]
5949    fn php_method_call() {
5950        check_metrics::<PhpParser>(
5951            "<?php
5952function f($obj): void {
5953    $obj->m1();
5954    $obj->m2(1);
5955}",
5956            "foo.php",
5957            |metric| insta::assert_json_snapshot!(metric.abc),
5958        );
5959    }
5960
5961    #[cfg(feature = "php")]
5962    #[test]
5963    fn php_static_call() {
5964        check_metrics::<PhpParser>(
5965            "<?php
5966function f(): void {
5967    Foo::bar();
5968    Foo::baz(1);
5969}",
5970            "foo.php",
5971            |metric| insta::assert_json_snapshot!(metric.abc),
5972        );
5973    }
5974
5975    #[cfg(feature = "php")]
5976    #[test]
5977    fn php_nullsafe_call() {
5978        check_metrics::<PhpParser>(
5979            "<?php
5980function f($obj): void {
5981    $obj?->m1();
5982    $obj?->m2(1);
5983}",
5984            "foo.php",
5985            |metric| insta::assert_json_snapshot!(metric.abc),
5986        );
5987    }
5988
5989    #[cfg(feature = "php")]
5990    #[test]
5991    fn php_object_creation() {
5992        check_metrics::<PhpParser>(
5993            "<?php
5994function f(): void {
5995    new Foo();
5996    new Bar(1);
5997}",
5998            "foo.php",
5999            |metric| insta::assert_json_snapshot!(metric.abc),
6000        );
6001    }
6002
6003    #[cfg(feature = "php")]
6004    #[test]
6005    fn php_comparison_eq() {
6006        check_metrics::<PhpParser>(
6007            "<?php
6008function f(int $a, int $b): bool {
6009    return $a == $b || $a != $b;
6010}",
6011            "foo.php",
6012            |metric| insta::assert_json_snapshot!(metric.abc),
6013        );
6014    }
6015
6016    #[cfg(feature = "php")]
6017    #[test]
6018    fn php_comparison_strict() {
6019        check_metrics::<PhpParser>(
6020            "<?php
6021function f(int $a, int $b): bool {
6022    return $a === $b || $a !== $b;
6023}",
6024            "foo.php",
6025            |metric| insta::assert_json_snapshot!(metric.abc),
6026        );
6027    }
6028
6029    #[cfg(feature = "php")]
6030    #[test]
6031    fn php_spaceship() {
6032        check_metrics::<PhpParser>(
6033            "<?php
6034function f(int $a, int $b): int {
6035    return $a <=> $b;
6036}",
6037            "foo.php",
6038            |metric| insta::assert_json_snapshot!(metric.abc),
6039        );
6040    }
6041
6042    #[cfg(feature = "php")]
6043    #[test]
6044    fn php_instanceof() {
6045        check_metrics::<PhpParser>(
6046            "<?php
6047function f($x): bool {
6048    return $x instanceof Foo;
6049}",
6050            "foo.php",
6051            |metric| insta::assert_json_snapshot!(metric.abc),
6052        );
6053    }
6054
6055    #[cfg(feature = "php")]
6056    #[test]
6057    fn php_complex_function() {
6058        // One snippet exercising A, B, C buckets together.
6059        check_metrics::<PhpParser>(
6060            "<?php
6061function f(int $a, int $b): int {
6062    $sum = $a + $b;
6063    $prod = $a * $b;
6064    if ($sum > 0 && $prod === 0) {
6065        return foo($sum);
6066    }
6067    return bar()->double();
6068}",
6069            "foo.php",
6070            |metric| insta::assert_json_snapshot!(metric.abc),
6071        );
6072    }
6073
6074    #[cfg(feature = "php")]
6075    #[test]
6076    fn php_if_boolean_literal_condition() {
6077        check_metrics::<PhpParser>(
6078            "<?php\n\
6079             function f() {\n\
6080             \x20   if (true) {}                 // +1c\n\
6081             \x20   if (!false) {}               // +1c\n\
6082             \x20   while (true) {}              // +1c\n\
6083             \x20   do {} while (false);         // +1c\n\
6084             }\n",
6085            "foo.php",
6086            |metric| {
6087                assert_eq!(metric.abc.conditions_sum(), 4);
6088                insta::assert_json_snapshot!(metric.abc);
6089            },
6090        );
6091    }
6092
6093    #[cfg(feature = "php")]
6094    #[test]
6095    fn php_methods_arguments_with_conditions() {
6096        check_metrics::<PhpParser>(
6097            "<?php\n\
6098             function f($a, $b) {\n\
6099             \x20   m($a, $b);                   // +1b\n\
6100             \x20   m(!$a, !$b);                 // +1b +2c\n\
6101             }\n",
6102            "foo.php",
6103            |metric| {
6104                assert_eq!(metric.abc.branches_sum(), 2);
6105                assert_eq!(metric.abc.conditions_sum(), 2);
6106                insta::assert_json_snapshot!(metric.abc);
6107            },
6108        );
6109    }
6110
6111    #[cfg(feature = "php")]
6112    #[test]
6113    fn php_return_with_conditions() {
6114        check_metrics::<PhpParser>(
6115            "<?php\n\
6116             function m1($z) { return !($z >= 0); }\n\
6117             function m2($x) { return (((!$x))); }\n\
6118             function m3($x, $y) { return $x && $y; }\n",
6119            "foo.php",
6120            |metric| {
6121                // m1: `>=` (1). m2: walker unwraps to $x (1).
6122                // m3: `&&` walker counts both (2). Sum: 4.
6123                assert_eq!(metric.abc.conditions_sum(), 4);
6124                insta::assert_json_snapshot!(metric.abc);
6125            },
6126        );
6127    }
6128
6129    #[cfg(feature = "php")]
6130    #[test]
6131    fn php_name2_hidden_rule_drift_marker() {
6132        // Drift marker (findings.md round-2 #3): `Php::Name2` maps
6133        // to the hidden grammar rule `_name`. At the pinned
6134        // tree-sitter-php version it is never emitted as a concrete
6135        // node — the visible `Name` (= 1) carries every name.
6136        // The former `php_bool_terminal_kinds!()` listed `Name2`
6137        // defensively (lesson 34); if a future grammar bump promotes `_name`
6138        // to a visible rule, this assertion fails loudly.
6139        let src = "<?php\nfunction f($x) { if ($x) { foo($x); } }\n";
6140        let parser = PhpParser::new(
6141            src.as_bytes().to_vec(),
6142            &std::path::PathBuf::from("foo.php"),
6143            None,
6144        );
6145        assert!(!ast_has_kind_id(&parser, Php::Name2 as u16));
6146    }
6147
6148    #[cfg(feature = "php")]
6149    #[test]
6150    fn php_scoped_property_access_condition_counts() {
6151        // Regression for findings.md round-2 #1 (PHP):
6152        // `if (Config::$enabled) {}` parses with
6153        // `scoped_property_access_expression` as the condition
6154        // node (kind_id 333 at the pinned grammar version — the
6155        // `*2` alias). Pre-fix, neither `ScopedPropertyAccessExpression`
6156        // nor its alias was in the former `php_bool_terminal_kinds!()`.
6157        // The walker reached the access node, found it non-terminal,
6158        // and broke. Mirrors C#'s `MemberAccessExpression` rule
6159        // (lesson 19, #372).
6160        check_metrics::<PhpParser>(
6161            "<?php\n\
6162             class Config { public static $enabled = true; }\n\
6163             function f() { if (Config::$enabled) { } }\n",
6164            "foo.php",
6165            |metric| {
6166                assert_eq!(metric.abc.conditions_sum(), 1);
6167                insta::assert_json_snapshot!(metric.abc);
6168            },
6169        );
6170    }
6171
6172    #[cfg(feature = "php")]
6173    #[test]
6174    fn php_named_argument_unary_conditional_counts() {
6175        // Regression for the code-review finding: PHP 8 named-argument
6176        // syntax `m(name: !$a)` parses as `argument(name, ':',
6177        // unary_op_expression)`. Pre-fix, the count walker took
6178        // `argument.child(0)` (the name) and missed the value at the
6179        // last child. Now it picks the last named child as the value.
6180        check_metrics::<PhpParser>(
6181            "<?php\nfunction f($a) { m(name: !$a); }\n",
6182            "foo.php",
6183            |metric| {
6184                // 1 call (branch) + 1 unary-conditional named argument.
6185                assert_eq!(metric.abc.branches_sum(), 1);
6186                assert_eq!(metric.abc.conditions_sum(), 1);
6187                insta::assert_json_snapshot!(metric.abc);
6188            },
6189        );
6190    }
6191
6192    #[cfg(feature = "php")]
6193    #[test]
6194    fn php_low_precedence_keyword_logical_ops_trigger_walker() {
6195        // Regression: pre-fix, `$a or $b` reported 0 conditions
6196        // because the dispatcher only handled `AMPAMP|PIPEPIPE`,
6197        // skipping the PHP-specific `and` / `or` keyword forms even
6198        // though they parse under the same `binary_expression` shape.
6199        // (`xor` is no chain: it is a value since #1536.)
6200        check_metrics::<PhpParser>(
6201            "<?php\n\
6202             function f($a, $b) {\n\
6203             \x20   return $a or $b;\n\
6204             }\n",
6205            "foo.php",
6206            |metric| {
6207                assert_eq!(metric.abc.conditions_sum(), 2);
6208                insta::assert_json_snapshot!(metric.abc);
6209            },
6210        );
6211    }
6212
6213    #[cfg(feature = "php")]
6214    #[test]
6215    fn php_if_multiple_conditions() {
6216        check_metrics::<PhpParser>(
6217            "<?php\n\
6218             function f($a, $b, $c, $d) {\n\
6219             \x20   if ($a || $b || $c || $d) {}     // +4c\n\
6220             \x20   if ($a && $b && $c) {}           // +3c\n\
6221             \x20   if (!$a && !$b) {}               // +2c\n\
6222             }\n",
6223            "foo.php",
6224            |metric| {
6225                assert_eq!(metric.abc.conditions_sum(), 9);
6226                insta::assert_json_snapshot!(metric.abc);
6227            },
6228        );
6229    }
6230
6231    #[cfg(feature = "php")]
6232    #[test]
6233    fn php_while_and_do_while_conditions() {
6234        check_metrics::<PhpParser>(
6235            "<?php\n\
6236             function f($a, $b) {\n\
6237             \x20   while ($a || $b) {}              // +2c\n\
6238             \x20   do {} while ($a && !$b);         // +2c\n\
6239             }\n",
6240            "foo.php",
6241            |metric| {
6242                assert_eq!(metric.abc.conditions_sum(), 4);
6243                insta::assert_json_snapshot!(metric.abc);
6244            },
6245        );
6246    }
6247
6248    #[cfg(feature = "php")]
6249    #[test]
6250    fn php_short_circuit_with_boolean_literal_operand() {
6251        check_metrics::<PhpParser>(
6252            "<?php\nfunction f($a) { return $a && true; }\n",
6253            "foo.php",
6254            |metric| {
6255                assert_eq!(metric.abc.conditions_sum(), 2);
6256                insta::assert_json_snapshot!(metric.abc);
6257            },
6258        );
6259    }
6260
6261    // Issue #1102, PHP half. See
6262    // `cpp_ternary_operand_slots_count_as_unary_conditions` for the
6263    // rule. PHP's ABC dispatcher has no `?`-token arm — the grammar
6264    // does emit the token, but the `conditional_expression` node is
6265    // what carries the tally's +1 — so the arm keeps that increment and
6266    // adds the operand slots.
6267    #[cfg(feature = "php")]
6268    #[test]
6269    fn php_ternary_operand_slots_count_as_unary_conditions() {
6270        // ternary (1) + condition `$a` (1) + `!$b` (1) + `!$c` (1) = 4.
6271        check_metrics::<PhpParser>(
6272            "<?php\nfunction f() { $x = $a ? !$b : !$c; }\n",
6273            "foo.php",
6274            |metric| assert_eq!(metric.abc.conditions_sum(), 4),
6275        );
6276        // No-double-count pin: ternary (1) + `>` (1) = 2, unchanged by
6277        // the fix.
6278        check_metrics::<PhpParser>(
6279            "<?php\nfunction f() { $x = ($a > 0) ? $b : -$b; }\n",
6280            "foo.php",
6281            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
6282        );
6283        // Nested (PHP 8 requires the inner ternary parenthesised): two
6284        // ternary nodes plus the two bare-variable conditions = 4.
6285        check_metrics::<PhpParser>(
6286            "<?php\nfunction f() { $x = $a ? ($b ? $c : $d) : $e; }\n",
6287            "foo.php",
6288            |metric| assert_eq!(metric.abc.conditions_sum(), 4),
6289        );
6290        // A negated condition is the only input reaching the walker's
6291        // `else` fallback — see the C++ sibling for why. ternary (1) +
6292        // `!$a` (1) = 2.
6293        check_metrics::<PhpParser>(
6294            "<?php\nfunction f() { $x = !$a ? $b : $c; }\n",
6295            "foo.php",
6296            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
6297        );
6298    }
6299
6300    // PHP's short ternary `$a ?: $b` elides the consequence, which the
6301    // grammar names `body` (not `consequence`) and marks optional. The
6302    // alternative lands at child(3), so addressing the slot by field
6303    // name rather than a fixed child(4) is what keeps `!$b` counted.
6304    #[cfg(feature = "php")]
6305    #[test]
6306    fn php_elided_ternary_body_still_walks_the_alternative() {
6307        // ternary (1) + condition `$a` (1) + `!$b` (1) = 3.
6308        check_metrics::<PhpParser>(
6309            "<?php\nfunction f() { $x = $a ?: !$b; }\n",
6310            "foo.php",
6311            |metric| assert_eq!(metric.abc.conditions_sum(), 3),
6312        );
6313    }
6314
6315    // Issue #1276, PHP half. The `for` header's condition slot was the
6316    // one condition slot `PhpCode::compute` never dispatched, so a
6317    // bare / negated / parenthesised loop condition scored zero while
6318    // the identical predicate in an `if` header scored one. Each
6319    // fixture below is a shape only the new arm can classify — a
6320    // comparison-shaped condition proves nothing here, because the `<`
6321    // token arm counts it either way (grammar-dispatch §11).
6322    #[cfg(feature = "php")]
6323    #[test]
6324    fn php_for_condition_slot_counts_unary_conditions() {
6325        // Bare variable: the whole condition, no operator token.
6326        check_metrics::<PhpParser>(
6327            "<?php\nfunction f($a) { for (; $a; ) {} }\n",
6328            "foo.php",
6329            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
6330        );
6331        // Negation: the slot peels the `!` (`php_wrapper_operand`) and
6332        // pays for the operand beneath it.
6333        check_metrics::<PhpParser>(
6334            "<?php\nfunction f($a) { for (; !$a; ) {} }\n",
6335            "foo.php",
6336            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
6337        );
6338        // Parentheses: counted because the slot peels them
6339        // (`php_wrapper_operand`), the path #1276 found dead.
6340        check_metrics::<PhpParser>(
6341            "<?php\nfunction f($a) { for (; ($a); ) {} }\n",
6342            "foo.php",
6343            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
6344        );
6345        // No-double-count pin: the `<` token arm already counted this
6346        // shape before the fix, and the walker must not add a second.
6347        // Two assignments (`$i = 0`, `$i++`) confirm the header parsed
6348        // as the three-clause form rather than degenerating.
6349        check_metrics::<PhpParser>(
6350            "<?php\nfunction f($n) { for ($i = 0; $i < $n; $i++) {} }\n",
6351            "foo.php",
6352            |metric| {
6353                assert_eq!(metric.abc.conditions_sum(), 1);
6354                assert_eq!(metric.abc.assignments_sum(), 2);
6355            },
6356        );
6357        // Empty condition: no `condition` field, no decision, zero.
6358        check_metrics::<PhpParser>(
6359            "<?php\nfunction f() { for (;;) { break; } }\n",
6360            "foo.php",
6361            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
6362        );
6363    }
6364
6365    // --- Kotlin ABC tests -------------------------------------------------
6366
6367    #[cfg(feature = "kotlin")]
6368    #[test]
6369    fn kotlin_empty_class() {
6370        check_metrics::<KotlinParser>("class C {}", "foo.kt", |metric| {
6371            assert_eq!(metric.abc.assignments_sum(), 0);
6372            assert_eq!(metric.abc.branches_sum(), 0);
6373            assert_eq!(metric.abc.conditions_sum(), 0);
6374            insta::assert_json_snapshot!(metric.abc);
6375        });
6376    }
6377
6378    #[cfg(feature = "kotlin")]
6379    #[test]
6380    fn kotlin_val_declarations_are_not_assignments() {
6381        // `val` introduces an immutable binding — the `=` initialising it
6382        // is not an assignment in the ABC sense.
6383        check_metrics::<KotlinParser>(
6384            "class C {
6385                val a: Int = 1
6386                val b: Int = 2
6387                val c: Int = 3
6388            }",
6389            "foo.kt",
6390            |metric| {
6391                assert_eq!(metric.abc.assignments_sum(), 0);
6392                assert_eq!(metric.abc.branches_sum(), 0);
6393                insta::assert_json_snapshot!(metric.abc);
6394            },
6395        );
6396    }
6397
6398    #[cfg(feature = "kotlin")]
6399    #[test]
6400    fn kotlin_var_declarations_count_assignment() {
6401        // `var` initialisers count as assignments (mutable binding).
6402        check_metrics::<KotlinParser>(
6403            "class C {
6404                var a: Int = 1
6405                var b: Int = 2
6406            }",
6407            "foo.kt",
6408            |metric| {
6409                assert_eq!(metric.abc.assignments_sum(), 2);
6410                insta::assert_json_snapshot!(metric.abc);
6411            },
6412        );
6413    }
6414
6415    #[cfg(feature = "kotlin")]
6416    #[test]
6417    fn kotlin_val_then_assignments_count() {
6418        // Regression for #455: a `val` initialiser must not suppress the
6419        // standalone `=` assignments that follow it. tree-sitter-kotlin
6420        // emits no `SEMI` token (even for explicit semicolons), so the
6421        // pre-#455 `SEMI`-cleared declaration stack never cleared and the
6422        // immutable-`val` sentinel leaked, reporting A=0 here.
6423        check_metrics::<KotlinParser>(
6424            "fun f() {
6425                val cfg = 0
6426                a = 1
6427                b = 2
6428            }",
6429            "foo.kt",
6430            |metric| {
6431                // val initialiser suppressed; `a = 1` and `b = 2` count.
6432                assert_eq!(metric.abc.assignments_sum(), 2);
6433                insta::assert_json_snapshot!(metric.abc);
6434            },
6435        );
6436    }
6437
6438    #[cfg(feature = "kotlin")]
6439    #[test]
6440    fn kotlin_var_then_assignments_count() {
6441        // Companion to the #455 regression: a `var` declaration leaves a
6442        // mutable-binding sentinel that *permits* the `=` — this path
6443        // accidentally masked the leak (its `Var` sentinel never suppressed
6444        // anything), so it must keep counting both the initialiser and the
6445        // following standalone assignments.
6446        check_metrics::<KotlinParser>(
6447            "fun f() {
6448                var cfg = 0
6449                a = 1
6450                b = 2
6451            }",
6452            "foo.kt",
6453            |metric| {
6454                // var initialiser (+1) plus `a = 1` and `b = 2` (+2).
6455                assert_eq!(metric.abc.assignments_sum(), 3);
6456                insta::assert_json_snapshot!(metric.abc);
6457            },
6458        );
6459    }
6460
6461    #[cfg(feature = "kotlin")]
6462    #[test]
6463    fn kotlin_augmented_assignments_count() {
6464        // Augmented operators (+=, -=, etc.) and ++/-- always count.
6465        check_metrics::<KotlinParser>(
6466            "fun m() {
6467                var x = 0
6468                x += 1
6469                x -= 2
6470                x *= 3
6471                x++
6472                --x
6473            }",
6474            "foo.kt",
6475            |metric| {
6476                // var declaration (var x = 0): +1
6477                // x += 1, x -= 2, x *= 3, x++, --x: +5
6478                assert_eq!(metric.abc.assignments_sum(), 6);
6479                insta::assert_json_snapshot!(metric.abc);
6480            },
6481        );
6482    }
6483
6484    #[cfg(feature = "kotlin")]
6485    #[test]
6486    fn kotlin_branches_call_expression() {
6487        check_metrics::<KotlinParser>(
6488            "fun m() {
6489                println(\"a\")
6490                println(\"b\")
6491                println(\"c\")
6492            }",
6493            "foo.kt",
6494            |metric| {
6495                assert_eq!(metric.abc.branches_sum(), 3);
6496                insta::assert_json_snapshot!(metric.abc);
6497            },
6498        );
6499    }
6500
6501    #[cfg(feature = "kotlin")]
6502    #[test]
6503    fn kotlin_object_construction_branch() {
6504        // Kotlin's object construction is just `Foo()` — a `CallExpression`.
6505        check_metrics::<KotlinParser>(
6506            "class P(val x: Int)
6507            fun m(): P = P(1)",
6508            "foo.kt",
6509            |metric| {
6510                assert_eq!(metric.abc.branches_sum(), 1);
6511                insta::assert_json_snapshot!(metric.abc);
6512            },
6513        );
6514    }
6515
6516    #[cfg(feature = "kotlin")]
6517    #[test]
6518    fn kotlin_comparisons_count_conditions() {
6519        check_metrics::<KotlinParser>(
6520            "fun m(a: Int, b: Int): Boolean {
6521                val r1 = a < b
6522                val r2 = a > b
6523                val r3 = a <= b
6524                val r4 = a >= b
6525                val r5 = a == b
6526                val r6 = a != b
6527                return r1 || r2 || r3 || r4 || r5 || r6
6528            }",
6529            "foo.kt",
6530            |metric| {
6531                // Six comparison operators in the `val` initialisers
6532                // (<, >, <=, >=, ==, !=) → 6, plus the six bare-identifier
6533                // operands of the `r1 || … || r6` return chain, each a
6534                // Fitzpatrick Rule 9 unary condition (issue #557) → 6.
6535                // Total 12. Before the Kotlin walker was wired the chain
6536                // operands were silently dropped and this read 6.
6537                assert_eq!(metric.abc.conditions_sum(), 12);
6538                insta::assert_json_snapshot!(metric.abc);
6539            },
6540        );
6541    }
6542
6543    #[cfg(feature = "kotlin")]
6544    #[test]
6545    fn kotlin_identity_equality_conditions() {
6546        // `===` / `!==` are referential equality in Kotlin; they count too.
6547        check_metrics::<KotlinParser>(
6548            "fun m(a: Any, b: Any): Boolean {
6549                return a === b || a !== b
6550            }",
6551            "foo.kt",
6552            |metric| {
6553                assert_eq!(metric.abc.conditions_sum(), 2);
6554                insta::assert_json_snapshot!(metric.abc);
6555            },
6556        );
6557    }
6558
6559    #[cfg(feature = "kotlin")]
6560    #[test]
6561    fn kotlin_else_branch_counts() {
6562        check_metrics::<KotlinParser>(
6563            "fun m(x: Int): Int {
6564                return if (x > 0) 1 else -1
6565            }",
6566            "foo.kt",
6567            |metric| {
6568                // condition: > (1) + else (1) = 2
6569                assert_eq!(metric.abc.conditions_sum(), 2);
6570                insta::assert_json_snapshot!(metric.abc);
6571            },
6572        );
6573    }
6574
6575    #[cfg(feature = "kotlin")]
6576    #[test]
6577    fn kotlin_when_entries_count() {
6578        check_metrics::<KotlinParser>(
6579            "fun m(x: Int): Int {
6580                return when (x) {
6581                    1 -> 10
6582                    2 -> 20
6583                    else -> 0
6584                }
6585            }",
6586            "foo.kt",
6587            |metric| {
6588                // Non-`else` WhenEntry arms count; the `else ->` fallback
6589                // arm does not (issue #456). Two case arms + zero for the
6590                // `else` arm = 2.
6591                assert_eq!(metric.abc.conditions_sum(), 2);
6592                insta::assert_json_snapshot!(metric.abc);
6593            },
6594        );
6595    }
6596
6597    // Pins the `else ->` exclusion directly: a `when` whose only fallback
6598    // is `else ->` must not count that arm. Revert-verified — gating the
6599    // `WhenEntry` arm on `!kotlin_when_entry_is_else` is what drops this
6600    // from 3 to 2 (issue #456, lesson 11). Mirrors the cyclomatic gate.
6601    #[cfg(feature = "kotlin")]
6602    #[test]
6603    fn kotlin_when_else_not_a_condition() {
6604        check_metrics::<KotlinParser>(
6605            "fun m(x: Int): Int {
6606                return when (x) { 1 -> 10; 2 -> 20; else -> 0 }
6607            }",
6608            "foo.kt",
6609            |metric| {
6610                // case `1 ->` (+1) + case `2 ->` (+1) + `else ->` (+0) = 2.
6611                assert_eq!(metric.abc.conditions_sum(), 2);
6612            },
6613        );
6614    }
6615
6616    // The Kotlin binding of the two assertions every #1421 fixture needs,
6617    // coupled on purpose. A per-member score table is worth only as much
6618    // as the anchor proving the fixture still spells what it claims to
6619    // (`.claude/rules/testing.md`, "Perturb the fixture as well as the
6620    // production line"), and the `when_subject` count is the anchor no
6621    // score can stand in for: a `when` that grew a subject scores its
6622    // entries the pre-#1421 way with every other row still satisfied.
6623    // Each fixture is one class, so the members are `spaces[0]`'s.
6624    #[cfg(feature = "kotlin")]
6625    #[track_caller]
6626    fn assert_kotlin_class_members(
6627        src: &str,
6628        kinds: &[(u16, usize, &str)],
6629        expected: &[(&str, u64, u64)],
6630    ) {
6631        assert_fixture_spells::<KotlinParser>(src, "foo.kt", kinds);
6632        check_func_space::<KotlinParser, _>(src, "foo.kt", |space| {
6633            assert_members_score(&space.spaces[0], expected);
6634        });
6635    }
6636
6637    // #1421's headline. A subject-less `when` arm's condition is an
6638    // ordinary boolean expression, so its comparison operator is already
6639    // an ABC condition through the token arms; the `WhenEntry` arm added
6640    // a second one on top, and `two` reported 4 against a decision count
6641    // of 2.
6642    //
6643    // `bare` is the control that isolates the defect rather than merely
6644    // observing it: same arm count, same entry shape, no comparison
6645    // operator anywhere — so it scored the right 2 before the fix and
6646    // after it. A fix that changed both members alike would be attacking
6647    // the entry count, which was never wrong.
6648    //
6649    // `ge` and `eq` exist because the issue named `<` and `>` only. All
6650    // six comparison spellings share the defect: `>=` and `==` reach
6651    // `stats.conditions` through the `LTEQ | GTEQ | EQEQ | …` arm rather
6652    // than the parent-gated `LT | GT` arm, and both scored 2 against a
6653    // decision count of 1.
6654    //
6655    // `iff` is the member that legitimately sits *above* its decision
6656    // count, and it is here so the table cannot be read as "§8 parity
6657    // everywhere". ABC counts the `else` (Fitzpatrick Rule 5) and
6658    // cyclomatic does not, so 2 conditions against 1 decision is correct
6659    // and unchanged by this fix.
6660    //
6661    // The `when_subject` anchor is the load-bearing one: the whole test
6662    // turns on these `when`s having no subject, and nothing in the
6663    // measured numbers would notice a fixture that grew one — a
6664    // subject-ful `when (x) { … }` scores its entries the old way and
6665    // `two` would read 4 again with every other row still satisfied.
6666    #[cfg(feature = "kotlin")]
6667    #[test]
6668    fn kotlin_subjectless_when_arm_counts_its_condition_once() {
6669        let src = "class K {
6670                fun f(): Boolean = true
6671                fun two(x: Int): Int = when { x > 5 -> 1; x < 0 -> 2; else -> 0 }
6672                fun bare(x: Boolean, y: Boolean): Int = when { x -> 1; y -> 2; else -> 0 }
6673                fun ge(x: Int): Int = when { x >= 5 -> 1; else -> 0 }
6674                fun eq(x: Int): Int = when { x == 5 -> 1; else -> 0 }
6675                fun andd(a: Int, b: Int): Int = when { a > 1 && b < 2 -> 1; else -> 0 }
6676                fun call(): Int = when { f() -> 1; else -> 0 }
6677                fun iff(x: Int): Int = if (x > 5) 1 else 0
6678            }";
6679        assert_kotlin_class_members(
6680            src,
6681            &[
6682                (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
6683                (Kotlin::WhenEntry as u16, 14, "`when` entries"),
6684                (
6685                    Kotlin::BinaryExpression as u16,
6686                    8,
6687                    "comparison / chain expressions",
6688                ),
6689                (Kotlin::AMPAMP as u16, 1, "`andd`'s `&&`"),
6690            ],
6691            &[
6692                // (member, abc.conditions, cyclomatic)
6693                ("f", 0, 1),
6694                // Was 4: two entries, two comparisons, counted twice.
6695                ("two", 2, 3),
6696                // The no-comparison control — 2 before and after.
6697                ("bare", 2, 3),
6698                // Was 2 each: the `GTEQ` / `EQEQ` spellings.
6699                ("ge", 1, 2),
6700                ("eq", 1, 2),
6701                // Was 3. A compound condition keeps both of its
6702                // comparisons — suppressing the operators instead
6703                // would have collapsed this one to 1.
6704                ("andd", 2, 3),
6705                // The condition slot scores a call directly, so a
6706                // spelling with no operator in it still reads 1.
6707                ("call", 1, 2),
6708                // Above its decision count on purpose: ABC counts
6709                // the `else`, cyclomatic does not.
6710                ("iff", 2, 2),
6711            ],
6712        );
6713    }
6714
6715    // #1459, the regression #1421 left behind. Turning the `when`
6716    // entry's blanket count into a condition slot moved the payment from
6717    // the entry to the condition — so every shape the slot cannot
6718    // classify went from 1 to 0, silently. Two shapes did:
6719    //
6720    // - `a!!`. `unary_expression` is one kind for *both* Kotlin unary
6721    //   spellings, and the slot routed it to the peel, so this reads as
6722    //   handled at every call site. The peel then looked for the operand
6723    //   at child(1) — right for the prefix `!x`, wrong for a postfix
6724    //   `x!!`, whose child(1) is the `!!` token — and stopped.
6725    // - `a as Boolean`. `as_expression` was not a wrapper the peel knew
6726    //   at all, so the slot fell off the end of its `else if`.
6727    //
6728    // The controls are `bare` / `cmp` / `call`, one per surviving path
6729    // (terminal, comparison token, invocation): a fix that moved the
6730    // entry count back would take those to 2 and fail here, which is
6731    // what distinguishes repairing the peel from reverting #1421.
6732    //
6733    // `safe` is the §5 row and the one measurement decided rather than
6734    // taste. `as?` is already a condition *token*
6735    // (`src/metrics/abc/kotlin.rs`), so a peel that treated `as` and
6736    // `as?` alike would score it 2 — the double count that arm's own
6737    // fix was avoiding. `kotlin_wrapper_operand` excludes `as?` for
6738    // that reason, and this member is what fails if the exclusion goes:
6739    // `(a as? Boolean)!!` is valid Kotlin whose peel reaches the cast
6740    // through two other wrappers, so it needs no invalid fixture to
6741    // discriminate.
6742    //
6743    // `iff` carries the same `a!!` through the `if` slot. That half is
6744    // not #1421 fallout — `if (a!!)` has read 0 since the slot landed in
6745    // #773 — but it is the same peel and the same edit, and a fix
6746    // narrowed to `when` entries would leave it at 0.
6747    //
6748    // Every member is valid Kotlin: `a!!` on a `Boolean?`, `as` on an
6749    // `Any`, and `(a as? Boolean)!!` back to a `Boolean`. The kind
6750    // anchors are what stop a later edit from trimming a spelling out
6751    // and turning its member into a silent copy of `bare`.
6752    //
6753    // Since #1533 the slot pays for any operand it cannot see a
6754    // comparison or chain inside, so `bang`, `paren`, `cast` and `chain`
6755    // score 1 with or without the peel. `bangCmp` and `castCmp` are what
6756    // still need it: unpeeled, the wrapper pays as a value on top of the
6757    // `>` it hides, and they read 2.
6758    #[cfg(feature = "kotlin")]
6759    #[test]
6760    fn kotlin_condition_slot_peels_null_assertions_and_casts() {
6761        let src = "class K {
6762                fun g(x: Boolean): Boolean = true
6763                fun bare(a: Boolean): Int = when { a -> 1; else -> 0 }
6764                fun cmp(a: Int): Int = when { a > 5 -> 1; else -> 0 }
6765                fun call(a: Boolean): Int = when { g(a) -> 1; else -> 0 }
6766                fun bang(a: Boolean?): Int = when { a!! -> 1; else -> 0 }
6767                fun paren(a: Boolean?): Int = when { (a!!) -> 1; else -> 0 }
6768                fun cast(a: Any): Int = when { a as Boolean -> 1; else -> 0 }
6769                fun chain(a: Any?): Int = when { a!! as Boolean -> 1; else -> 0 }
6770                fun safe(a: Any): Int = when { (a as? Boolean)!! -> 1; else -> 0 }
6771                fun cmt(a: Boolean): Int = when { ! /*c*/ a -> 1; else -> 0 }
6772                fun iff(a: Boolean?): Int { if (a!!) { return 1 }; return 0 }
6773                fun bangCmp(a: Int): Int = when { (a > 5)!! -> 1; else -> 0 }
6774                fun castCmp(a: Int): Int = when { (a > 5) as Boolean -> 1; else -> 0 }
6775            }";
6776        assert_kotlin_class_members(
6777            src,
6778            &[
6779                (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
6780                (
6781                    Kotlin::BANGBANG as u16,
6782                    6,
6783                    "the `!!` null assertions of `bang` / `paren` / `chain` / `safe` / `iff` / \
6784                     `bangCmp`",
6785                ),
6786                (
6787                    Kotlin::AsExpression as u16,
6788                    4,
6789                    "the casts of `cast` / `chain` / `safe` / `castCmp`",
6790                ),
6791                (Kotlin::AsQMARK as u16, 1, "`safe`'s safe cast"),
6792                (
6793                    Kotlin::BlockComment as u16,
6794                    1,
6795                    "`cmt`'s interposed comment, spaces and all",
6796                ),
6797                (
6798                    Kotlin::ParenthesizedExpression as u16,
6799                    4,
6800                    "the parenthesised operands of `paren` / `safe` / `bangCmp` / `castCmp`",
6801                ),
6802            ],
6803            &[
6804                // (member, abc.conditions, cyclomatic)
6805                ("g", 0, 1),
6806                // The three controls, unchanged by this fix.
6807                ("bare", 1, 2),
6808                ("cmp", 1, 2),
6809                ("call", 1, 2),
6810                // Each was 0: the peel stopped on the wrapper.
6811                ("bang", 1, 2),
6812                ("paren", 1, 2),
6813                ("cast", 1, 2),
6814                ("chain", 1, 2),
6815                // 1, not 2 — the `as?` token owns this one, and the
6816                // peel declines to charge for it a second time.
6817                ("safe", 1, 2),
6818                // The peel reads its operand through the `operator` /
6819                // `argument` fields, so an `extra` between the two no
6820                // longer displaces it. The spacing is load-bearing and
6821                // must not be tidied away: `!/*c*/a` unspaced parses
6822                // with the comment somewhere a `child(1)` read still
6823                // skips, so only the spaced spelling can tell the field
6824                // read from the positional one. Measured both ways.
6825                ("cmt", 1, 2),
6826                // The `if` slot, which has read 0 since #773.
6827                ("iff", 1, 2),
6828                // The `>` pays; the peel keeps the wrapper from paying too.
6829                ("bangCmp", 1, 2),
6830                ("castCmp", 1, 2),
6831            ],
6832        );
6833    }
6834
6835    // The other half of #1421: a subject-ful entry must not move. Its
6836    // condition is a *pattern* matched against the subject, not an
6837    // independent boolean expression, so the implicit `subject ==
6838    // pattern` is the decision and the entry itself pays for it —
6839    // `range_test`, `type_test` and a bare constant carry no token the
6840    // comparison arms would count.
6841    //
6842    // `cmp` is the interesting row and the reason `nested` sits beside
6843    // it. `when (x) { y > 5 -> … }` is legal when `x` is `Boolean`, and
6844    // it reads 2 conditions against 1 decision — the entry's implicit
6845    // equality plus the `>` inside the operand. That is not the #1421
6846    // double-count reappearing: `nested` spells the same thing as an
6847    // `if` (`x == (y > 5)`) and ABC has always scored it 2 against the
6848    // same 1 decision. A comparison nested inside a comparison is two
6849    // comparisons; only one of them is a branch.
6850    #[cfg(feature = "kotlin")]
6851    #[test]
6852    fn kotlin_subjectful_when_arms_keep_the_per_entry_count() {
6853        let src = "class K {
6854                fun inn(a: Int): Int = when (a) { in 1..2 -> 1; else -> 0 }
6855                fun notin(a: Int): Int = when (a) { !in 1..2 -> 1; else -> 0 }
6856                fun iss(a: Any): Int = when (a) { is String -> 1; else -> 0 }
6857                fun konst(a: Int): Int = when (a) { 1 -> 10; 2 -> 20; else -> 0 }
6858                fun cmp(x: Boolean, y: Int): Int = when (x) { y > 5 -> 1; else -> 0 }
6859                fun nested(x: Boolean, y: Int): Int { if (x == (y > 5)) { return 1 }; return 0 }
6860            }";
6861        assert_kotlin_class_members(
6862            src,
6863            &[
6864                (Kotlin::WhenSubject as u16, 5, "`when` subjects"),
6865                (Kotlin::WhenEntry as u16, 11, "`when` entries"),
6866                (Kotlin::RangeTest as u16, 2, "`in` / `!in` patterns"),
6867                (Kotlin::TypeTest as u16, 1, "the `is` pattern"),
6868                (
6869                    Kotlin::BinaryExpression as u16,
6870                    3,
6871                    "`cmp`'s and `nested`'s comparisons",
6872                ),
6873            ],
6874            &[
6875                ("inn", 1, 2),
6876                ("notin", 1, 2),
6877                ("iss", 1, 2),
6878                ("konst", 2, 3),
6879                // Above its decision count, like `nested` below it
6880                // and for the same reason.
6881                ("cmp", 2, 2),
6882                ("nested", 2, 2),
6883            ],
6884        );
6885    }
6886
6887    // `kotlin_enclosing_when_has_subject` stops scanning at the `{`, so
6888    // that a subject-less `when` does not walk all N arms once per arm.
6889    // The bound has to clear the header first, and the header can carry
6890    // an `extra`: tree-sitter hangs `when /*c*/ (x)`'s comment off the
6891    // `when_expression` between the keyword and the subject, so a bound
6892    // one child tighter — `child(1)`, or a stop at the first *named*
6893    // child — reads `cmtSubject` as subject-less. It would then score
6894    // its `1` arm through the slot, where a `number_literal` is not a
6895    // Kotlin terminal, and report 0 conditions instead of 1. That is
6896    // what discriminates here; `cmtNone` is the other direction, where
6897    // the brace is all the bound has to stop on.
6898    //
6899    // The `block_comment` census is the fixture anchor: delete either
6900    // comment and the members still read 1, because a plain `when (x)`
6901    // and a plain `when {` both do. The count fails by name instead.
6902    #[cfg(feature = "kotlin")]
6903    #[test]
6904    fn kotlin_when_subject_scan_clears_a_comment_before_the_brace() {
6905        let src = "class K {
6906                fun cmtSubject(x: Int): Int = when /*c*/ (x) { 1 -> 1; else -> 0 }
6907                fun cmtNone(a: Boolean): Int = when /*c*/ { a -> 1; else -> 0 }
6908            }";
6909        assert_kotlin_class_members(
6910            src,
6911            &[
6912                (Kotlin::BlockComment as u16, 2, "the two header comments"),
6913                (Kotlin::WhenSubject as u16, 1, "`cmtSubject`'s subject"),
6914                (Kotlin::WhenEntry as u16, 4, "`when` entries"),
6915            ],
6916            &[("cmtSubject", 1, 2), ("cmtNone", 1, 2)],
6917        );
6918    }
6919
6920    // A subject-less condition reaches the slot through the same
6921    // `kotlin_wrapper_operand` peel an `if` predicate does, which
6922    // is what separates this design from suppressing the entry's
6923    // operator. `notted` and `paren` unwrap to a bare terminal and score
6924    // through the slot; `notCmp` and `parenCmp` unwrap to a
6925    // `binary_expression`, score nothing from the slot, and take their
6926    // one condition from the `>` the token arm already owned. Both pairs
6927    // land on 1, which no blanket `+1` can produce for the second pair —
6928    // they read 2 before the fix.
6929    //
6930    // `paren` once reached the slot only through a `WhenEntry` seed in the
6931    // retired walker's boolean-context set, and dropped to 0 without it.
6932    // On the shared slot (#1533) it pins that a `when` entry peels a
6933    // parenthesis as `if ((a))` does; `notted` cannot stand in for it,
6934    // since a `!` is a wrapper of its own.
6935    #[cfg(feature = "kotlin")]
6936    #[test]
6937    fn kotlin_subjectless_when_condition_wrappers_count_once() {
6938        let src = "class K {
6939                fun notted(a: Boolean): Int = when { !a -> 1; else -> 0 }
6940                fun paren(a: Boolean): Int = when { (a) -> 1; else -> 0 }
6941                fun notCmp(a: Int): Int = when { !(a > 1) -> 1; else -> 0 }
6942                fun parenCmp(a: Int): Int = when { (a > 1) -> 1; else -> 0 }
6943                fun nest(x: Int, y: Int): Int =
6944                    when { x > 1 -> when { y > 2 -> 1; else -> 2 }; else -> 0 }
6945                fun blockBody(x: Int): Int { when { x > 5 -> { return 1 } }; return 0 }
6946            }";
6947        assert_kotlin_class_members(
6948            src,
6949            &[
6950                // Paired, not bare: "no subjects" alone is satisfied by a
6951                // fixture that lost its `when`s altogether.
6952                (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
6953                (Kotlin::WhenEntry as u16, 13, "`when` entries"),
6954                (Kotlin::UnaryExpression as u16, 2, "the two `!` operands"),
6955                (
6956                    Kotlin::ParenthesizedExpression as u16,
6957                    3,
6958                    "the three parenthesised operands",
6959                ),
6960                (Kotlin::BinaryExpression as u16, 5, "comparisons"),
6961            ],
6962            &[
6963                ("notted", 1, 2),
6964                ("paren", 1, 2),
6965                // Both were 2: the entry's blanket count plus the `>`.
6966                ("notCmp", 1, 2),
6967                ("parenCmp", 1, 2),
6968                // Was 4 — one entry and one comparison per `when`,
6969                // each counted twice.
6970                ("nest", 2, 3),
6971                // The `when`-as-statement shape: a `block` body and no
6972                // `else` arm at all. Reading the condition from the
6973                // `condition` field rather than by position is what keeps
6974                // the block out of the slot; it was 2 before the fix.
6975                ("blockBody", 1, 2),
6976            ],
6977        );
6978    }
6979
6980    // The one place this design deliberately does not reach parity, put
6981    // under test so the choice cannot drift unremarked. A `when` entry
6982    // may list alternatives (`when { a, b -> … }`, an implicit `or`), and
6983    // the `condition` field is `multiple` — but cyclomatic scores the
6984    // whole entry as one decision, so `bareAlts` is at parity only
6985    // because the slot reads the *first* alternative and stops. Routing
6986    // every alternative through the slot would push it to 2.
6987    //
6988    // `alts` is the row that cannot be brought to parity from the ABC
6989    // side at all: the comparison-token arms count `>` and `<` wherever
6990    // they appear, so it reads 2 against 1 decision no matter what the
6991    // entry slot does. It was 3 before #1421 and the remaining gap is
6992    // cyclomatic's single count per multi-alternative entry, not a
6993    // double-count here.
6994    //
6995    // `subjAlts` is the subject-ful control, unchanged and at parity: its
6996    // alternatives are constants carrying no token to count.
6997    #[cfg(feature = "kotlin")]
6998    #[test]
6999    fn kotlin_subjectless_when_multi_alternative_entry() {
7000        let src = "class K {
7001                fun alts(x: Int, y: Int): Int = when { x > 5, y < 0 -> 1; else -> 0 }
7002                fun bareAlts(x: Boolean, y: Boolean): Int = when { x, y -> 1; else -> 0 }
7003                fun subjAlts(a: Int): Int = when (a) { 1, 2 -> 10; else -> 0 }
7004            }";
7005        assert_kotlin_class_members(
7006            src,
7007            &[
7008                (Kotlin::WhenSubject as u16, 1, "`subjAlts`'s subject"),
7009                (Kotlin::WhenEntry as u16, 6, "`when` entries"),
7010                (
7011                    Kotlin::BinaryExpression as u16,
7012                    2,
7013                    "`alts`'s two comparisons",
7014                ),
7015            ],
7016            &[
7017                // Above its decision count: cyclomatic scores the entry
7018                // once, the token arms score both comparisons. Was 3.
7019                ("alts", 2, 2),
7020                ("bareAlts", 1, 2),
7021                ("subjAlts", 1, 2),
7022            ],
7023        );
7024    }
7025
7026    // `is_expression` and `in_expression` are the two relational forms
7027    // tree-sitter-kotlin-ng spells as their own production rather than as
7028    // a `binary_expression`, so no comparison-token arm sees them and
7029    // nothing counted them: `if (a is String)` scored 0 against a
7030    // decision count of 1, and the same test in a subject-less `when`
7031    // scored 1 only because the entry's blanket `+1` happened to cover
7032    // it. Removing that blanket without counting these two somewhere
7033    // would have regressed `whenIs` and `whenIn` to 0, and `isAnd` —
7034    // where the `is` test is one operand of a `&&` chain and so reaches
7035    // the walker rather than the slot — from 2 to 1.
7036    //
7037    // The `if` members are here because the fix was never `when`-
7038    // specific: they are the call sites that prove it reaches the `if` /
7039    // `while` predicate slot too.
7040    //
7041    // #1421 counted them through the former
7042    // `kotlin_bool_terminal_kinds!()` and
7043    // #1461 moved them to an unconditional arm, which leaves every
7044    // number below unchanged — the slot no longer counts the test and
7045    // the arm does. `kotlin_is_and_in_score_outside_a_boolean_slot` is
7046    // the half this fixture cannot see, every member of it being inside
7047    // a slot.
7048    #[cfg(feature = "kotlin")]
7049    #[test]
7050    fn kotlin_is_and_in_expressions_are_unary_conditions() {
7051        let src = "class K {
7052                fun ifIs(a: Any): Int { if (a is String) { return 1 }; return 0 }
7053                fun ifIn(a: Int): Int { if (a in 1..2) { return 1 }; return 0 }
7054                fun whenIs(a: Any): Int = when { a is String -> 1; else -> 0 }
7055                fun whenIn(a: Int): Int = when { a in 1..2 -> 1; else -> 0 }
7056                fun isAnd(a: Any, b: Boolean): Int = when { a is String && b -> 1; else -> 0 }
7057            }";
7058        assert_kotlin_class_members(
7059            src,
7060            &[
7061                // Paired, not bare — see the wrappers test above.
7062                (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
7063                (Kotlin::WhenEntry as u16, 6, "`when` entries"),
7064                (Kotlin::IsExpression as u16, 3, "`is` tests"),
7065                (Kotlin::InExpression as u16, 2, "`in` tests"),
7066                (Kotlin::AMPAMP as u16, 1, "`isAnd`'s `&&`"),
7067            ],
7068            &[
7069                // Both were 0 — the predicate slot saw a kind it did
7070                // not classify and the token arms saw no operator.
7071                ("ifIs", 1, 2),
7072                ("ifIn", 1, 2),
7073                ("whenIs", 1, 2),
7074                ("whenIn", 1, 2),
7075                ("isAnd", 2, 3),
7076            ],
7077        );
7078    }
7079
7080    // Kotlin spells boolean `and` / `or` / `xor` as infix *functions*, so
7081    // `a and b` parses as `infix_expression` — not `binary_expression`,
7082    // and not any token arm. A slot pays for it for the same reason it
7083    // pays for a `call_expression`: `a and b` is `a.and(b)`.
7084    //
7085    // This was a regression of #1421, not a pre-existing gap, and it is
7086    // the third shape the blanket per-entry count turned out to be
7087    // propping up — after `is` / `in` and bare parens, which that fix
7088    // caught. Found by the whole-branch review. Measured on both sides:
7089    // `when { a and b -> … }` scored 1 before and 0 after, while the
7090    // `if (a and b)` it is supposed to agree with still scored 1.
7091    //
7092    // The `if` members are the anchor rather than a second `when`: the
7093    // whole premise of #1421 is that a subject-less arm scores like an
7094    // `if` predicate, so a fixture where the two disagree is the
7095    // statement of the bug. `andAmp` holds the pair level against the
7096    // `&&` spelling, which never regressed and would otherwise be the
7097    // only form under test.
7098    #[cfg(feature = "kotlin")]
7099    #[test]
7100    fn kotlin_infix_boolean_functions_are_unary_conditions() {
7101        let src = "class K {
7102                fun whenAnd(a: Boolean, b: Boolean): Int = when { a and b -> 1; else -> 0 }
7103                fun whenOr(a: Boolean, b: Boolean): Int = when { a or b -> 1; else -> 0 }
7104                fun whenXor(a: Boolean, b: Boolean): Int = when { a xor b -> 1; else -> 0 }
7105                fun whenAmp(a: Boolean, b: Boolean): Int = when { a && b -> 1; else -> 0 }
7106                fun ifAnd(a: Boolean, b: Boolean): Int { if (a and b) { return 1 }; return 0 }
7107            }";
7108        assert_kotlin_class_members(
7109            src,
7110            &[
7111                (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
7112                (Kotlin::InfixExpression as u16, 4, "the infix calls"),
7113                (Kotlin::AMPAMP as u16, 1, "`whenAmp`'s `&&`"),
7114            ],
7115            &[
7116                // 0 before this fix: the predicate slot saw a kind it
7117                // did not classify and no token arm matched.
7118                ("whenAnd", 1, 2),
7119                ("whenOr", 1, 2),
7120                ("whenXor", 1, 2),
7121                // Never regressed — `&&` is a `binary_expression` whose
7122                // operator the token arm owns. Holds the others level.
7123                ("whenAmp", 2, 3),
7124                // The agreement #1421 exists to establish: a
7125                // subject-less arm scores what the equivalent `if`
7126                // predicate scores.
7127                ("ifAnd", 1, 2),
7128            ],
7129        );
7130    }
7131
7132    #[cfg(feature = "kotlin")]
7133    #[test]
7134    fn kotlin_catch_block_counts() {
7135        check_metrics::<KotlinParser>(
7136            "fun m() {
7137                try {
7138                    println(\"ok\")
7139                } catch (e: Exception) {
7140                    println(\"err\")
7141                }
7142            }",
7143            "foo.kt",
7144            |metric| {
7145                // `try` (+1) and `catch` (+1) each contribute one condition,
7146                // matching Java / C# / C++ / Groovy (Fitzpatrick counts both
7147                // keywords). Before #696 Kotlin counted only the catch block.
7148                assert_eq!(metric.abc.conditions_sum(), 2);
7149                insta::assert_json_snapshot!(metric.abc);
7150            },
7151        );
7152    }
7153
7154    #[cfg(feature = "kotlin")]
7155    #[test]
7156    fn kotlin_elvis_and_safe_cast() {
7157        // `?:` (elvis) and `as?` (safe cast) are condition-like.
7158        check_metrics::<KotlinParser>(
7159            "fun m(s: String?): Int {
7160                val n = (s as? Int) ?: 0
7161                return n
7162            }",
7163            "foo.kt",
7164            |metric| {
7165                // as? (+1) + ?: (+1) = 2 conditions.
7166                assert_eq!(metric.abc.conditions_sum(), 2);
7167                insta::assert_json_snapshot!(metric.abc);
7168            },
7169        );
7170    }
7171
7172    #[cfg(feature = "kotlin")]
7173    #[test]
7174    fn kotlin_generic_brackets_not_conditions() {
7175        // `<` / `>` used as type-parameter brackets must not be counted.
7176        check_metrics::<KotlinParser>(
7177            "class Box<T>(val v: T)
7178            fun <T> wrap(x: T): Box<T> = Box(x)",
7179            "foo.kt",
7180            |metric| {
7181                // No comparisons — only generic brackets.
7182                assert_eq!(metric.abc.conditions_sum(), 0);
7183                insta::assert_json_snapshot!(metric.abc);
7184            },
7185        );
7186    }
7187
7188    // #1297: a qualified super call disambiguates its supertype with
7189    // the same two bare tokens a comparison uses — `super<A>.g()` is a
7190    // `super_expression`, which the previous denylist
7191    // (`type_arguments` / `type_parameters`) did not name, so it scored
7192    // two conditions. One `super<A>` against one genuine `a < b`
7193    // separates every mis-aim: 3 pre-fix, 1 once the arm allows
7194    // `BinaryExpression`, 0 if it allows the wrong parent or the
7195    // fixture stops parsing.
7196    #[cfg(feature = "kotlin")]
7197    #[test]
7198    fn kotlin_super_type_argument_is_not_a_condition() {
7199        check_metrics::<KotlinParser>(
7200            "class B : A() {
7201                override fun g(a: Int, b: Int): Int {
7202                    if (a < b) { return super<A>.g(a, b) }
7203                    return 0
7204                }
7205            }",
7206            "foo.kt",
7207            |metric| {
7208                assert_eq!(metric.abc.conditions_sum(), 1);
7209                // Non-vacuity guard: 1 is also what a body whose `if`
7210                // survived but whose super call did not would score, so
7211                // pin the call itself. Measured: dropping
7212                // `super<A>.g(a, b)` takes `branches_sum()` from 2 to 1 —
7213                // the remaining branch is the `A()` primary-constructor
7214                // delegation in the class header, which #1384 taught this
7215                // metric to count.
7216                assert_eq!(metric.abc.branches_sum(), 2);
7217            },
7218        );
7219    }
7220
7221    #[cfg(feature = "kotlin")]
7222    #[test]
7223    fn kotlin_class_with_methods_and_branches() {
7224        check_metrics::<KotlinParser>(
7225            "class C {
7226                var counter: Int = 0
7227                fun bump() {
7228                    counter += 1
7229                    println(counter)
7230                }
7231            }",
7232            "foo.kt",
7233            |metric| {
7234                // assignments: var counter = 0 (+1), counter += 1 (+1) = 2
7235                // branches: println(counter) = 1
7236                assert_eq!(metric.abc.assignments_sum(), 2);
7237                assert_eq!(metric.abc.branches_sum(), 1);
7238                assert_eq!(metric.abc.conditions_sum(), 0);
7239                insta::assert_json_snapshot!(metric.abc);
7240            },
7241        );
7242    }
7243
7244    #[cfg(feature = "kotlin")]
7245    #[test]
7246    fn kotlin_object_singleton_abc() {
7247        check_metrics::<KotlinParser>(
7248            "object Util {
7249                fun work(x: Int): Int {
7250                    var y = x
7251                    y += 1
7252                    if (y > 0) {
7253                        return y
7254                    }
7255                    return -1
7256                }
7257            }",
7258            "foo.kt",
7259            |metric| {
7260                // assignments: var y = x (+1), y += 1 (+1) = 2
7261                // branches: 0 (return is not a call)
7262                // conditions: y > 0 (+1) = 1
7263                assert_eq!(metric.abc.assignments_sum(), 2);
7264                assert_eq!(metric.abc.branches_sum(), 0);
7265                assert_eq!(metric.abc.conditions_sum(), 1);
7266                insta::assert_json_snapshot!(metric.abc);
7267            },
7268        );
7269    }
7270
7271    #[cfg(feature = "kotlin")]
7272    #[test]
7273    fn kotlin_interface_abc() {
7274        // Pure-abstract interface with no bodies — all-zero.
7275        check_metrics::<KotlinParser>(
7276            "interface I {
7277                fun work(): Int
7278                fun describe(): String
7279            }",
7280            "foo.kt",
7281            |metric| {
7282                assert_eq!(metric.abc.assignments_sum(), 0);
7283                assert_eq!(metric.abc.branches_sum(), 0);
7284                assert_eq!(metric.abc.conditions_sum(), 0);
7285                insta::assert_json_snapshot!(metric.abc);
7286            },
7287        );
7288    }
7289
7290    #[cfg(feature = "kotlin")]
7291    #[test]
7292    fn kotlin_nested_class_abc() {
7293        check_metrics::<KotlinParser>(
7294            "class Outer {
7295                var o: Int = 0
7296                class Nested {
7297                    var n: Int = 0
7298                    fun bump() { n += 1 }
7299                }
7300            }",
7301            "foo.kt",
7302            |metric| {
7303                // Outer: var o = 0 (+1)
7304                // Nested: var n = 0 (+1), n += 1 (+1) = 2
7305                // total assignments = 3
7306                assert_eq!(metric.abc.assignments_sum(), 3);
7307                insta::assert_json_snapshot!(metric.abc);
7308            },
7309        );
7310    }
7311
7312    #[cfg(feature = "kotlin")]
7313    #[test]
7314    fn kotlin_data_class_abc() {
7315        // `data class` with primary-constructor `val`s — no assignments
7316        // (vals don't count) and no body conditions.
7317        check_metrics::<KotlinParser>(
7318            "data class Point(val x: Int, val y: Int)",
7319            "foo.kt",
7320            |metric| {
7321                assert_eq!(metric.abc.assignments_sum(), 0);
7322                assert_eq!(metric.abc.branches_sum(), 0);
7323                assert_eq!(metric.abc.conditions_sum(), 0);
7324                insta::assert_json_snapshot!(metric.abc);
7325            },
7326        );
7327    }
7328
7329    #[cfg(feature = "kotlin")]
7330    #[test]
7331    fn kotlin_primary_constructor_default_value_not_assignment() {
7332        // Regression: default values on primary-constructor `val`
7333        // parameters are initialisers, not assignments. Without
7334        // `ClassParameter` pushing a declaration sentinel, the `=` token
7335        // here would be counted unconditionally as a standalone
7336        // assignment.
7337        check_metrics::<KotlinParser>("class C(val a: Int = 5)", "foo.kt", |metric| {
7338            // `val a = 5` → suppressed (Const sentinel).
7339            assert_eq!(metric.abc.assignments_sum(), 0);
7340            insta::assert_json_snapshot!(metric.abc);
7341        });
7342    }
7343
7344    #[cfg(feature = "kotlin")]
7345    #[test]
7346    fn kotlin_unary_conditions_in_chain() {
7347        // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
7348        // `&&` / `||` chain is one condition. `a && b || c` → a, b, c each
7349        // contribute one; the `&&` / `||` operators contribute nothing.
7350        // expected: 3 unary conditions, no comparisons, no `if`-keyword
7351        // condition in Kotlin (matches the Java byte-equivalent of 3).
7352        check_metrics::<KotlinParser>(
7353            "fun f(a: Boolean, b: Boolean, c: Boolean) {
7354                if (a && b || c) { println(\"x\") }
7355            }",
7356            "foo.kt",
7357            |metric| {
7358                assert_eq!(metric.abc.conditions_sum(), 3);
7359            },
7360        );
7361    }
7362
7363    #[cfg(feature = "kotlin")]
7364    #[test]
7365    fn kotlin_comparison_operands_add_nothing() {
7366        // Isolation check: comparison operands of a `&&` chain are nested
7367        // `binary_expression` nodes, not bare boolean leaves, so the
7368        // walker adds nothing — only the two `>` comparisons count.
7369        // expected: 2 (the two `>` tokens), walker contributes 0.
7370        check_metrics::<KotlinParser>(
7371            "fun g(x: Int, y: Int) {
7372                if (x > 0 && y > 0) { println(\"x\") }
7373            }",
7374            "foo.kt",
7375            |metric| {
7376                assert_eq!(metric.abc.conditions_sum(), 2);
7377            },
7378        );
7379    }
7380
7381    #[cfg(feature = "kotlin")]
7382    #[test]
7383    fn kotlin_negated_operand_is_unary_condition() {
7384        // A `!`-negated operand is still a unary condition: `a && !b`
7385        // unwraps the `unary_expression` to reach the inner identifier.
7386        // expected: 2 (`a` and the `!b` operand).
7387        check_metrics::<KotlinParser>(
7388            "fun f(a: Boolean, b: Boolean) {
7389                if (a && !b) { println(\"x\") }
7390            }",
7391            "foo.kt",
7392            |metric| {
7393                assert_eq!(metric.abc.conditions_sum(), 2);
7394            },
7395        );
7396    }
7397
7398    // The wrapper peel has two callers, and #1459's fixture members all
7399    // reach it through the *slot* (`kotlin_count_condition`). This is the
7400    // other one: the `&&` / `||` walker (`count_field_operands`),
7401    // which hands each operand to the same slot. The two wrappers #1459
7402    // taught it therefore have a
7403    // second, structurally independent path into the count, and a
7404    // fixture covering only the slot leaves it untested
7405    // (`.claude/rules/grammar-dispatch.md` §11).
7406    //
7407    // Each chain is one identifier plus one wrapped comparison, so 2 is
7408    // "the walker peeled the wrapper to the comparison, which pays for
7409    // itself" and 3 is "it paid the wrapper as a value as well". Since
7410    // the slot pays for any operand it cannot see a comparison inside
7411    // (#1533), a wrapped bare identifier scores 2 either way, so only a
7412    // wrapped comparison tells the peel from its absence. The bare
7413    // `a && b` control is `kotlin_unary_conditions_in_chain`'s shape at
7414    // 2, so a regression cannot be read as the chain itself changing.
7415    #[cfg(feature = "kotlin")]
7416    #[test]
7417    fn kotlin_chain_operands_peel_null_assertions_and_casts() {
7418        // Anchored per row, because the assertion alone cannot tell the
7419        // wrapper from its operand: `a && x > 1` scores the same 2 / 3 as
7420        // every row here, so trimming the `!!` or the cast out of a
7421        // fixture would leave this green with its subject gone
7422        // (`.claude/rules/testing.md`, "Perturb the fixture as well as
7423        // the production line").
7424        for (chain, spelling, anchors) in [
7425            (
7426                "a && (x > 1)!!",
7427                "null assertion",
7428                &[(Kotlin::BANGBANG as u16, 1usize, "the `!!`")][..],
7429            ),
7430            (
7431                "a && (x > 1) as Boolean",
7432                "cast",
7433                &[(Kotlin::AsExpression as u16, 1, "the `as` cast")][..],
7434            ),
7435            (
7436                "a && ((x > 1)!!)",
7437                "parenthesised null assertion",
7438                &[
7439                    (Kotlin::BANGBANG as u16, 1, "the `!!`"),
7440                    (Kotlin::ParenthesizedExpression as u16, 2, "the parens"),
7441                ][..],
7442            ),
7443        ] {
7444            let src = format!("fun f(a: Boolean, x: Int) {{ if ({chain}) {{ println(\"x\") }} }}");
7445            assert_fixture_spells::<KotlinParser>(&src, "foo.kt", anchors);
7446            check_func_space::<KotlinParser, _>(&src, "foo.kt", |space| {
7447                let f = child_space(&space, "f");
7448                assert_eq!(
7449                    f.metrics.abc.conditions(),
7450                    2,
7451                    "`{chain}`: the identifier plus the comparison under the {spelling}"
7452                );
7453                assert_eq!(f.metrics.cyclomatic.cyclomatic(), 3, "`{chain}`: decisions");
7454            });
7455        }
7456    }
7457
7458    #[cfg(feature = "kotlin")]
7459    #[test]
7460    fn kotlin_bare_if_predicate_is_one_condition() {
7461        // Issue #773: a bare-boolean `if` predicate (`if (flag)`) is one
7462        // Fitzpatrick unary condition. Before the Phase-2B arm it counted
7463        // 0, so `if (flag) 1 else -1` scored 1 (only the `else`) instead of
7464        // 2, dropping below Kotlin's own cyclomatic decision count.
7465        // expected: predicate (1) + else (1) = 2.
7466        check_metrics::<KotlinParser>(
7467            "fun m(flag: Boolean): Int { return if (flag) 1 else -1 }",
7468            "foo.kt",
7469            |metric| {
7470                assert_eq!(metric.abc.conditions_sum(), 2);
7471            },
7472        );
7473    }
7474
7475    #[cfg(feature = "kotlin")]
7476    #[test]
7477    fn kotlin_bare_while_predicate_is_one_condition() {
7478        // Issue #773: the bare predicate of a `while` loop counts one
7479        // condition via the `condition` field. expected: 1.
7480        check_metrics::<KotlinParser>(
7481            "fun m(running: Boolean) { while (running) { println(\"x\") } }",
7482            "foo.kt",
7483            |metric| {
7484                assert_eq!(metric.abc.conditions_sum(), 1);
7485            },
7486        );
7487    }
7488
7489    #[cfg(feature = "kotlin")]
7490    #[test]
7491    fn kotlin_bare_do_while_predicate_is_one_condition() {
7492        // Issue #773: the bare predicate of a `do`/`while` loop counts one
7493        // condition. expected: 1.
7494        check_metrics::<KotlinParser>(
7495            "fun m(ok: Boolean) { do { println(\"x\") } while (ok) }",
7496            "foo.kt",
7497            |metric| {
7498                assert_eq!(metric.abc.conditions_sum(), 1);
7499            },
7500        );
7501    }
7502
7503    #[cfg(feature = "kotlin")]
7504    #[test]
7505    fn kotlin_comparison_predicate_not_double_counted() {
7506        // Double-count guard (#773): a comparison predicate (`if (a == b)`)
7507        // is a nested `binary_expression` already counted by the `==` token
7508        // arm, so the Phase-2B condition-slot arm must add nothing here.
7509        // expected: `==` (1) + else (1) = 2 — unchanged by the new arm.
7510        check_metrics::<KotlinParser>(
7511            "fun m(a: Int, b: Int): Int { return if (a == b) 1 else -1 }",
7512            "foo.kt",
7513            |metric| {
7514                assert_eq!(metric.abc.conditions_sum(), 2);
7515            },
7516        );
7517    }
7518
7519    #[cfg(feature = "kotlin")]
7520    #[test]
7521    fn kotlin_short_circuit_predicate_not_double_counted() {
7522        // Double-count guard (#773): an `&&`/`||` predicate is counted by
7523        // the Rule 9 chain walker (each operand once); the Phase-2B arm
7524        // must add nothing for it. expected: `x` (1) + `y` (1) + else (1)
7525        // = 3 — unchanged by the new arm.
7526        check_metrics::<KotlinParser>(
7527            "fun m(x: Boolean, y: Boolean): Int { return if (x && y) 1 else -1 }",
7528            "foo.kt",
7529            |metric| {
7530                assert_eq!(metric.abc.conditions_sum(), 3);
7531            },
7532        );
7533    }
7534
7535    #[cfg(feature = "kotlin")]
7536    #[test]
7537    fn kotlin_parenthesised_bare_predicate_is_one_condition() {
7538        // A parenthesised bare predicate (`if ((flag))`) is unwrapped by
7539        // `kotlin_wrapper_operand` and still counts one condition (#773).
7540        // expected: 1.
7541        check_metrics::<KotlinParser>(
7542            "fun m(flag: Boolean) { if ((flag)) { println(\"x\") } }",
7543            "foo.kt",
7544            |metric| {
7545                assert_eq!(metric.abc.conditions_sum(), 1);
7546            },
7547        );
7548    }
7549
7550    // --- TypeScript / TSX ABC tests --------------------------------------
7551    //
7552    // Assignment, branch, condition counting per Fitzpatrick:
7553    // - Augmented assignment / `++` / `--` always count.
7554    // - Plain `=` counts unless inside `const` declaration.
7555    // - `call_expression` / `new_expression` count as branches.
7556    // - Comparison / equality operators, ternary `?`, `??`, control-flow
7557    //   arms (`else`, `case`, `default`, `catch`, `try`, `instanceof`),
7558    //   and `<`/`>` (outside `type_arguments` / `type_parameters`) count
7559    //   as conditions.
7560
7561    #[cfg(feature = "typescript")]
7562    #[test]
7563    fn typescript_assignments_basic() {
7564        check_metrics::<TypescriptParser>(
7565            "class C {
7566                m(): void {
7567                    let x = 0;          // +1 — only a `const` initializer is suppressed
7568                    x = 1;              // +1
7569                    x += 2;             // +1
7570                    x++;                // +1
7571                }
7572            }",
7573            "foo.ts",
7574            |metric| {
7575                assert_eq!(metric.abc.assignments_sum(), 4);
7576                insta::assert_json_snapshot!(metric.abc);
7577            },
7578        );
7579    }
7580
7581    #[cfg(feature = "typescript")]
7582    #[test]
7583    fn typescript_const_excluded_from_assignments() {
7584        check_metrics::<TypescriptParser>(
7585            "class C {
7586                m(): void {
7587                    const a = 1;        // suppressed (`const` initializer)
7588                    const b = 2;        // suppressed
7589                    let c = 3;          // +1 — `let` initializers count
7590                }
7591            }",
7592            "foo.ts",
7593            |metric| {
7594                assert_eq!(metric.abc.assignments_sum(), 1);
7595                insta::assert_json_snapshot!(metric.abc);
7596            },
7597        );
7598    }
7599
7600    // Regression cluster for #1277. The pre-fix implementation decided
7601    // "is this `=` a `const` initializer?" from a sentinel stack cleared
7602    // only on a `SEMI` token, so the answer for one statement depended on
7603    // the *previous* statement's terminator. Automatic semicolon
7604    // insertion makes that terminator optional in all four JS-family
7605    // languages. The replacement is structural — see
7606    // `impl_js_family_const_binding!` in `src/metrics/abc/js_family.rs`.
7607
7608    #[cfg(feature = "typescript")]
7609    #[test]
7610    fn typescript_asi_const_does_not_suppress_later_assignments() {
7611        check_metrics::<TypescriptParser>(
7612            "function f() {
7613                const a = 1
7614                x = 2
7615                y = 3
7616            }",
7617            "foo.ts",
7618            |metric| {
7619                // `const a = 1` suppressed; `x = 2` and `y = 3` count.
7620                // Pre-#1277 this reported 0: the unterminated `const`
7621                // never popped its sentinel.
7622                assert_eq!(metric.abc.assignments_sum(), 2);
7623            },
7624        );
7625    }
7626
7627    #[cfg(feature = "typescript")]
7628    #[test]
7629    fn typescript_semicolon_const_does_not_suppress_later_assignments() {
7630        // The semicolon-terminated spelling of the fixture above, which
7631        // must score the same as it. Pre-#1277 they scored 2 and 0 — the
7632        // pair is what pins the terminator out of the answer.
7633        check_metrics::<TypescriptParser>(
7634            "function f() {
7635                const a = 1;
7636                x = 2;
7637                y = 3;
7638            }",
7639            "foo.ts",
7640            |metric| {
7641                assert_eq!(metric.abc.assignments_sum(), 2);
7642            },
7643        );
7644    }
7645
7646    #[cfg(feature = "typescript")]
7647    #[test]
7648    fn typescript_as_const_does_not_suppress_later_assignments() {
7649        // The sentinel stack was also reachable from the other side: the
7650        // `const` token of a TypeScript `x as const` assertion promoted a
7651        // live `let` slot to `Const` and suppressed every `=` until the
7652        // next `;`. Structurally that `const` is a child of an
7653        // `as_expression`, not a declaration keyword.
7654        check_metrics::<TypescriptParser>(
7655            "function f(x: number) {
7656                let y = x as const
7657                w = 3
7658            }",
7659            "foo.ts",
7660            |metric| {
7661                // `let` initializer (+1) and `w = 3` (+1); pre-#1277: 1.
7662                assert_eq!(metric.abc.assignments_sum(), 2);
7663            },
7664        );
7665    }
7666
7667    #[cfg(feature = "typescript")]
7668    #[test]
7669    fn typescript_const_declarator_shapes_stay_suppressed() {
7670        // Shapes the sentinel stack handled implicitly, which the
7671        // structural predicate must reproduce: *every* declarator under a
7672        // `const`-bearing `lexical_declaration` is suppressed, including
7673        // the second element of a multi-declarator list, a destructuring
7674        // pattern, and the defaults inside one — `c = 5`, the nested
7675        // `e = 6` and its `= {}`, `g = 7`, and the rest element's `h = 8`,
7676        // each one more pattern layer the predicate's climb has to cross
7677        // — while `for (const x of xs)` carries no `=` at all. Only
7678        // `let i = 3` and `var j = 4` count — the deliberate deviation
7679        // documented on `js_abc_compute!`. Gating on
7680        // `variable_declaration` instead of `lexical_declaration`,
7681        // dropping the `const` check, or stopping the climb at the
7682        // declarator's own `=`, each flips one of these rows. The
7683        // `for`-of row is the exception: it carries no `=`, so no change
7684        // to the predicate can move it. It pins the grammar shape instead
7685        // — a future grammar emitting a `variable_declarator` there would
7686        // start suppressing something that never counted.
7687        check_metrics::<TypescriptParser>(
7688            "function f(o: any, xs: number[]) {
7689                const a = 1, b = 2
7690                const {c = 5, d: {e = 6} = {}} = o
7691                const [g = 7, ...[h = 8]] = xs
7692                let i = 3
7693                var j = 4
7694                for (const x of xs) { k(x) }
7695            }",
7696            "foo.ts",
7697            |metric| {
7698                assert_eq!(metric.abc.assignments_sum(), 2);
7699            },
7700        );
7701    }
7702
7703    #[cfg(feature = "typescript")]
7704    #[test]
7705    fn typescript_const_initializer_value_assignments_still_count() {
7706        // TypeScript half of
7707        // `javascript_const_initializer_value_assignments_still_count`.
7708        check_metrics::<TypescriptParser>(
7709            "function m(o: any, a: any, b: any) { const x = (o.p = 1); const y = a || (b = 2); }",
7710            "foo.ts",
7711            |metric| {
7712                assert_eq!(metric.abc.assignments_sum(), 2);
7713            },
7714        );
7715    }
7716
7717    #[cfg(feature = "typescript")]
7718    #[test]
7719    fn typescript_branches_function_calls() {
7720        check_metrics::<TypescriptParser>(
7721            "class C {
7722                m(): void {
7723                    foo();              // +1
7724                    bar(1, 2);          // +1
7725                    new Date();         // +1
7726                }
7727            }",
7728            "foo.ts",
7729            |metric| {
7730                assert_eq!(metric.abc.branches_sum(), 3);
7731                insta::assert_json_snapshot!(metric.abc);
7732            },
7733        );
7734    }
7735
7736    #[cfg(feature = "typescript")]
7737    #[test]
7738    fn typescript_conditions_comparison_operators() {
7739        check_metrics::<TypescriptParser>(
7740            "class C {
7741                m(x: number, y: number): boolean {
7742                    return x == y       // +1
7743                        || x === y      // +1
7744                        || x != y       // +1
7745                        || x !== y      // +1
7746                        || x < y        // +1
7747                        || x <= y       // +1
7748                        || x > y        // +1
7749                        || x >= y;      // +1
7750                }
7751            }",
7752            "foo.ts",
7753            |metric| {
7754                assert_eq!(metric.abc.conditions_sum(), 8);
7755                insta::assert_json_snapshot!(metric.abc);
7756            },
7757        );
7758    }
7759
7760    #[cfg(feature = "typescript")]
7761    #[test]
7762    fn typescript_conditions_control_flow_arms() {
7763        check_metrics::<TypescriptParser>(
7764            "class C {
7765                m(x: number): number {
7766                    try {                       // +1 (try)
7767                        if (x > 0) {            // +1 (>)
7768                            return 1;
7769                        } else {                // +1 (else)
7770                            return -1;
7771                        }
7772                    } catch (e) {               // +1 (catch)
7773                        return 0;
7774                    }
7775                }
7776            }",
7777            "foo.ts",
7778            |metric| {
7779                assert_eq!(metric.abc.conditions_sum(), 4);
7780                insta::assert_json_snapshot!(metric.abc);
7781            },
7782        );
7783    }
7784
7785    #[cfg(feature = "typescript")]
7786    #[test]
7787    fn typescript_conditions_switch_case() {
7788        check_metrics::<TypescriptParser>(
7789            "class C {
7790                m(x: number): number {
7791                    switch (x) {
7792                        case 1:                 // +1
7793                            return 1;
7794                        case 2:                 // +1
7795                            return 2;
7796                        default:                // +0 (fallthrough, #469)
7797                            return 0;
7798                    }
7799                }
7800            }",
7801            "foo.ts",
7802            |metric| {
7803                assert_eq!(metric.abc.conditions_sum(), 2);
7804                insta::assert_json_snapshot!(metric.abc);
7805            },
7806        );
7807    }
7808
7809    #[cfg(feature = "typescript")]
7810    #[test]
7811    fn typescript_ternary_and_nullish() {
7812        check_metrics::<TypescriptParser>(
7813            "class C {
7814                m(x: number | null): number {
7815                    return x !== null           // +1 (!==)
7816                        ? x                     // +1 (ternary ?)
7817                        : 0;
7818                }
7819                n(x: number | null): number {
7820                    return x ?? 0;              // +1 (??)
7821                }
7822            }",
7823            "foo.ts",
7824            |metric| {
7825                assert_eq!(metric.abc.conditions_sum(), 3);
7826                insta::assert_json_snapshot!(metric.abc);
7827            },
7828        );
7829    }
7830
7831    #[cfg(feature = "typescript")]
7832    #[test]
7833    fn typescript_instanceof_counts_as_condition() {
7834        check_metrics::<TypescriptParser>(
7835            "class C {
7836                m(o: unknown): boolean {
7837                    return o instanceof C;      // +1
7838                }
7839            }",
7840            "foo.ts",
7841            |metric| {
7842                assert_eq!(metric.abc.conditions_sum(), 1);
7843                insta::assert_json_snapshot!(metric.abc);
7844            },
7845        );
7846    }
7847
7848    #[cfg(feature = "typescript")]
7849    #[test]
7850    fn typescript_generic_lt_gt_not_a_condition() {
7851        // `<T>` in `class C<T>` and `Array<number>` should not contribute
7852        // to conditions even though the tokens are `<` and `>`.
7853        check_metrics::<TypescriptParser>(
7854            "class C<T> {
7855                xs: Array<number> = [];
7856                m(): void {
7857                    const arr: Array<string> = [];   // suppressed const
7858                    void arr;
7859                }
7860            }",
7861            "foo.ts",
7862            |metric| {
7863                assert_eq!(metric.abc.conditions_sum(), 0);
7864                insta::assert_json_snapshot!(metric.abc);
7865            },
7866        );
7867    }
7868
7869    // #1297, TypeScript half: the `<` / `>` arm flipped from denying
7870    // `type_arguments` / `type_parameters` to allowing
7871    // `binary_expression`, and this pins the direction that flip could
7872    // have broken. `Array<string>` keeps a generic in scope so the
7873    // fixture proves both sides at once, and the assertion is the
7874    // grammar-dispatch §8 pin: on the function's own space the ABC
7875    // condition count equals the cyclomatic decision count
7876    // (`cyclomatic()` minus the per-space base of 1). Both are 2, one
7877    // per `if`.
7878    //
7879    // As with the Lua fixture, `Array<string>` leaves no ABC trace once
7880    // excluded, so nothing here fails if a later edit drops the
7881    // generic; that the generic reaches the arm is established by the
7882    // revert test, which fails this test when the gate is removed.
7883    //
7884    // TypeScript gets no JSX fixture because the `.ts` dialect has no
7885    // JSX to exercise: it lexes `return <div>…</div>` as a type
7886    // assertion and a comparison chain, sometimes with `ERROR` nodes and
7887    // sometimes without, depending on what follows the tag. Either way
7888    // the tokens land under `type_arguments` and `binary_expression`
7889    // rather than the `jsx_*` productions, so the construct that
7890    // motivated #1297 is only reachable through
7891    // `tsx_jsx_elements_are_not_conditions` below.
7892    //
7893    // That mis-parse still over-counts — a `.ts` file containing JSX
7894    // scores conditions this gate cannot exclude, because the grammar
7895    // genuinely reports a `binary_expression`. It is a wrong-dialect
7896    // input rather than a defect in this arm, and no allowlist can
7897    // distinguish it; see the same caveat on Kotlin's arm.
7898    #[cfg(feature = "typescript")]
7899    #[test]
7900    fn typescript_comparison_operators_still_count_alongside_generics() {
7901        check_func_space::<TypescriptParser, _>(
7902            "function m(xs: Array<string>, a: number, b: number): number {
7903                if (a < b) { return 1; }
7904                if (a > b) { return 2; }
7905                void xs;
7906                return 0;
7907            }",
7908            "foo.ts",
7909            |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
7910        );
7911    }
7912
7913    // #1275, TypeScript half. Eleven grammar productions emit a bare
7914    // `?` and only `ternary_expression` is a decision; the other ten are
7915    // type syntax. This fixture exercises five of them — `optional_
7916    // parameter`, `property_signature`, `method_signature`,
7917    // `abstract_method_signature` and `public_field_definition`, plus an
7918    // `optional_type` in a tuple — against one real ternary over one
7919    // real `>`.
7920    //
7921    // Six type-syntax `?` means the numbers separate cleanly: 8 pre-fix,
7922    // 2 once the allowlist is aimed at `TernaryExpression`, 1 if it is
7923    // aimed at anything else (the ternary stops counting too), 0 if the
7924    // fixture stops parsing. Any partial gate — one that named some of
7925    // the type-syntax parents in a denylist instead — lands between 3
7926    // and 7 and is equally visible.
7927    #[cfg(feature = "typescript")]
7928    #[test]
7929    fn typescript_optional_type_syntax_is_not_a_condition() {
7930        check_metrics::<TypescriptParser>(
7931            "interface I { a?: string; m?(x: number): void; }
7932            abstract class K { f?: number; abstract g?(): void; }
7933            type Tup = [number, string?];
7934            function h(x?: number, y: number = 0): number { return y > 1 ? 1 : 2; }",
7935            "foo.ts",
7936            |metric| {
7937                assert_eq!(metric.abc.conditions_sum(), 2);
7938            },
7939        );
7940    }
7941
7942    // The explicit half of #1275's TypeScript decision: a conditional
7943    // type (`T extends U ? X : Y`) is resolved by the type checker and
7944    // erased before runtime, so its `?` is not a condition. That falls
7945    // out of the `TernaryExpression` allowlist rather than being named,
7946    // which is exactly why it needs its own test — the choice reads as
7947    // an omission otherwise, and nothing else here would notice if a
7948    // later edit added `ConditionalType` to the allowlist "for
7949    // symmetry".
7950    //
7951    // The real ternary below keeps the expectation off zero: 3 pre-fix,
7952    // 2 with the conditional type excluded, 1 if the ternary is
7953    // swallowed too.
7954    #[cfg(feature = "typescript")]
7955    #[test]
7956    fn typescript_conditional_type_is_not_a_condition() {
7957        check_metrics::<TypescriptParser>(
7958            "type Cond<T> = T extends string ? number : boolean;
7959            function pick(a: number, b: number): number { return a > b ? 1 : 2; }",
7960            "foo.ts",
7961            |metric| {
7962                assert_eq!(metric.abc.conditions_sum(), 2);
7963            },
7964        );
7965    }
7966
7967    // grammar-dispatch §2. `Typescript::QMARK2` is the enum entry for
7968    // the external scanner's `_ternary_qmark` token, and
7969    // tree-sitter-typescript's public symbol map folds it back onto
7970    // `anon_sym_QMARK`, so `kind_id()` never reports it — every ternary
7971    // `?` in the fixture below arrives as plain `QMARK`. Listing
7972    // `QMARK2` in the gated arm would therefore be dead code, and
7973    // leaving it out is safe only for as long as that mapping holds.
7974    // This pins both halves so a grammar bump that starts exposing the
7975    // alias fails here rather than silently zeroing every TypeScript
7976    // ternary.
7977    //
7978    // The fixture's only `?` is the ternary's, deliberately. An optional
7979    // parameter (`b?: number`) would emit a `QMARK` of its own and
7980    // satisfy the positive assertion on its own, leaving it true no
7981    // matter what id the ternary's `?` came back as — decoration rather
7982    // than the non-vacuity guard it is here for.
7983    #[cfg(feature = "typescript")]
7984    #[test]
7985    fn typescript_ternary_qmark_alias_stays_unreachable() {
7986        let parser = TypescriptParser::new(
7987            "function f(a: boolean): number { return a ? 1 : 2; }\n"
7988                .as_bytes()
7989                .to_vec(),
7990            std::path::Path::new("foo.ts"),
7991            None,
7992        );
7993        assert!(ast_has_kind_id(&parser, Typescript::QMARK as u16));
7994        assert!(!ast_has_kind_id(&parser, Typescript::QMARK2 as u16));
7995    }
7996
7997    #[cfg(feature = "typescript")]
7998    #[test]
7999    fn typescript_abstract_class_abc() {
8000        // Abstract methods have no body — they contribute nothing.
8001        check_metrics::<TypescriptParser>(
8002            "abstract class C {
8003                abstract a(): void;
8004                m(x: number): number {
8005                    if (x > 0) return 1;        // +1 condition
8006                    return 0;
8007                }
8008            }",
8009            "foo.ts",
8010            |metric| {
8011                assert_eq!(metric.abc.conditions_sum(), 1);
8012                assert_eq!(metric.abc.branches_sum(), 0);
8013                insta::assert_json_snapshot!(metric.abc);
8014            },
8015        );
8016    }
8017
8018    #[cfg(feature = "typescript")]
8019    #[test]
8020    fn typescript_interface_abc_zero() {
8021        check_metrics::<TypescriptParser>(
8022            "interface I {
8023                a(): void;
8024                b(): number;
8025                p: string;
8026            }",
8027            "foo.ts",
8028            |metric| {
8029                assert_eq!(metric.abc.assignments_sum(), 0);
8030                assert_eq!(metric.abc.branches_sum(), 0);
8031                assert_eq!(metric.abc.conditions_sum(), 0);
8032                insta::assert_json_snapshot!(metric.abc);
8033            },
8034        );
8035    }
8036
8037    #[cfg(feature = "typescript")]
8038    #[test]
8039    fn typescript_arrow_field_contributes_abc() {
8040        // Arrow function class members are function spaces; their
8041        // assignments/branches/conditions are counted.
8042        check_metrics::<TypescriptParser>(
8043            "class C {
8044                arrow = (x: number) => {
8045                    if (x > 0) {                // +1 condition
8046                        return foo();           // +1 branch
8047                    }
8048                    return 0;
8049                };
8050            }",
8051            "foo.ts",
8052            |metric| {
8053                assert_eq!(metric.abc.conditions_sum(), 1);
8054                assert_eq!(metric.abc.branches_sum(), 1);
8055                insta::assert_json_snapshot!(metric.abc);
8056            },
8057        );
8058    }
8059
8060    #[cfg(feature = "typescript")]
8061    #[test]
8062    fn typescript_parameter_property_init_not_assignment() {
8063        // Parameter properties don't introduce a `=` token themselves;
8064        // only the explicit `let z = 0` body assignment is counted.
8065        // The class field initializer `f: number = 0` likewise has a `=`
8066        // that DOES count (matches `typescript_assignments_basic`).
8067        check_metrics::<TypescriptParser>(
8068            "class C {
8069                f: number = 0;
8070                constructor(public x: number, private y: string) {
8071                    let z = 0;
8072                }
8073            }",
8074            "foo.ts",
8075            |metric| {
8076                // f's initializer + `let z = 0` = 2 assignments; the
8077                // parameter properties contribute zero.
8078                assert_eq!(metric.abc.assignments_sum(), 2);
8079                insta::assert_json_snapshot!(metric.abc);
8080            },
8081        );
8082    }
8083
8084    // TSX parity
8085
8086    #[cfg(feature = "typescript")]
8087    #[test]
8088    fn tsx_assignments_basic() {
8089        check_metrics::<TsxParser>(
8090            "class C {
8091                m(): void {
8092                    let x = 0;
8093                    x = 1;
8094                    x += 2;
8095                    x++;
8096                }
8097            }",
8098            "foo.tsx",
8099            |metric| {
8100                assert_eq!(metric.abc.assignments_sum(), 4);
8101                insta::assert_json_snapshot!(metric.abc);
8102            },
8103        );
8104    }
8105
8106    #[cfg(feature = "typescript")]
8107    #[test]
8108    fn tsx_const_excluded_from_assignments() {
8109        check_metrics::<TsxParser>(
8110            "class C {
8111                m(): void {
8112                    const a = 1;
8113                    let b = 2;
8114                }
8115            }",
8116            "foo.tsx",
8117            |metric| {
8118                assert_eq!(metric.abc.assignments_sum(), 1);
8119                insta::assert_json_snapshot!(metric.abc);
8120            },
8121        );
8122    }
8123
8124    #[cfg(feature = "typescript")]
8125    #[test]
8126    fn tsx_branches_function_calls() {
8127        check_metrics::<TsxParser>(
8128            "class C {
8129                m(): void {
8130                    foo();
8131                    new Date();
8132                }
8133            }",
8134            "foo.tsx",
8135            |metric| {
8136                assert_eq!(metric.abc.branches_sum(), 2);
8137                insta::assert_json_snapshot!(metric.abc);
8138            },
8139        );
8140    }
8141
8142    #[cfg(feature = "typescript")]
8143    #[test]
8144    fn tsx_conditions_comparison_operators() {
8145        check_metrics::<TsxParser>(
8146            "class C {
8147                m(x: number, y: number): boolean {
8148                    return x == y || x < y || x >= y;
8149                }
8150            }",
8151            "foo.tsx",
8152            |metric| {
8153                assert_eq!(metric.abc.conditions_sum(), 3);
8154                insta::assert_json_snapshot!(metric.abc);
8155            },
8156        );
8157    }
8158
8159    #[cfg(feature = "typescript")]
8160    #[test]
8161    fn tsx_conditions_control_flow_arms() {
8162        check_metrics::<TsxParser>(
8163            "class C {
8164                m(x: number): number {
8165                    try {
8166                        if (x > 0) return 1;
8167                        else return -1;
8168                    } catch (e) {
8169                        return 0;
8170                    }
8171                }
8172            }",
8173            "foo.tsx",
8174            |metric| {
8175                assert_eq!(metric.abc.conditions_sum(), 4);
8176                insta::assert_json_snapshot!(metric.abc);
8177            },
8178        );
8179    }
8180
8181    #[cfg(feature = "typescript")]
8182    #[test]
8183    fn tsx_conditions_switch_case() {
8184        check_metrics::<TsxParser>(
8185            "class C {
8186                m(x: number): number {
8187                    switch (x) {
8188                        case 1: return 1;       // +1
8189                        case 2: return 2;       // +1
8190                        default: return 0;      // +0 (fallthrough, #469)
8191                    }
8192                }
8193            }",
8194            "foo.tsx",
8195            |metric| {
8196                assert_eq!(metric.abc.conditions_sum(), 2);
8197                insta::assert_json_snapshot!(metric.abc);
8198            },
8199        );
8200    }
8201
8202    #[cfg(feature = "typescript")]
8203    #[test]
8204    fn tsx_ternary_and_nullish() {
8205        check_metrics::<TsxParser>(
8206            "class C {
8207                m(x: number | null): number {
8208                    return x !== null ? x : 0;
8209                }
8210                n(x: number | null): number { return x ?? 0; }
8211            }",
8212            "foo.tsx",
8213            |metric| {
8214                assert_eq!(metric.abc.conditions_sum(), 3);
8215                insta::assert_json_snapshot!(metric.abc);
8216            },
8217        );
8218    }
8219
8220    #[cfg(feature = "typescript")]
8221    #[test]
8222    fn tsx_instanceof_counts_as_condition() {
8223        check_metrics::<TsxParser>(
8224            "class C { m(o: unknown): boolean { return o instanceof C; } }",
8225            "foo.tsx",
8226            |metric| {
8227                assert_eq!(metric.abc.conditions_sum(), 1);
8228                insta::assert_json_snapshot!(metric.abc);
8229            },
8230        );
8231    }
8232
8233    #[cfg(feature = "typescript")]
8234    #[test]
8235    fn tsx_generic_lt_gt_not_a_condition() {
8236        check_metrics::<TsxParser>(
8237            "class C<T> { xs: Array<number> = []; }",
8238            "foo.tsx",
8239            |metric| {
8240                assert_eq!(metric.abc.conditions_sum(), 0);
8241                insta::assert_json_snapshot!(metric.abc);
8242            },
8243        );
8244    }
8245
8246    // #1297: a JSX tag delimiter is markup punctuation, not a
8247    // comparison. The `<` / `>` arm denied only `type_arguments` and
8248    // `type_parameters`, so every tag bracket scored a condition —
8249    // `jsx_opening_element` contributes both tokens, `jsx_closing_element`
8250    // a `>` (its `</` is one token) and `jsx_self_closing_element` a `<`
8251    // (its `/>` is one token).
8252    //
8253    // The fixture pairs seven such brackets — two elements' opening and
8254    // closing tags, plus one self-closing tag — against one genuine
8255    // `a < b`, so every way of mis-aiming the gate lands on its own
8256    // number: 8 pre-fix, 1 once it allows `BinaryExpression`, 0 if it
8257    // allows the wrong parent or the fixture stops parsing. Asserting 0
8258    // on a JSX-only body would not have separated those last two.
8259    #[cfg(feature = "typescript")]
8260    #[test]
8261    fn tsx_jsx_elements_are_not_conditions() {
8262        check_metrics::<TsxParser>(
8263            "function f(a: number, b: number) {
8264                if (a < b) { return <div className=\"x\"><span>hi</span></div>; }
8265                return <br />;
8266            }",
8267            "foo.tsx",
8268            |metric| {
8269                assert_eq!(metric.abc.conditions_sum(), 1);
8270                // Fixture-presence anchor: the `className="x"` attribute
8271                // `=` is this fixture's only assignment, so the test
8272                // fails if a later edit drops the JSX. Without it the
8273                // surviving `a < b` alone also scores 1, and the
8274                // conditions assertion would pass while proving nothing.
8275                assert_eq!(metric.abc.assignments_sum(), 1);
8276            },
8277        );
8278    }
8279
8280    // #1275 in the second expansion of `ts_abc_compute!`. TSX shares
8281    // TypeScript's `?` productions and its own `TernaryExpression` /
8282    // `QMARK` ids, so the gate is a distinct instantiation and needs its
8283    // own fixture — a passing TypeScript test says nothing about the
8284    // macro's other expansion. Four type-syntax `?` plus one `>` and one
8285    // ternary: 6 pre-fix, 2 after, 1 if the allowlist is misaimed.
8286    #[cfg(feature = "typescript")]
8287    #[test]
8288    fn tsx_optional_type_syntax_is_not_a_condition() {
8289        check_metrics::<TsxParser>(
8290            "interface I { a?: string; m?(x: number): void; }
8291            class K { f?: number; }
8292            function h(x?: number, y: number = 0): number { return y > 1 ? 1 : 2; }",
8293            "foo.tsx",
8294            |metric| {
8295                assert_eq!(metric.abc.conditions_sum(), 2);
8296            },
8297        );
8298    }
8299
8300    // The TSX half of `typescript_conditional_type_is_not_a_condition`.
8301    // `ts_abc_compute!` expands twice and the two expansions are
8302    // independent code (grammar-dispatch §11): adding `ConditionalType`
8303    // to the allowlist "for symmetry" fails only the TypeScript test
8304    // without this one, which reads as the TSX expansion being fine
8305    // rather than untested. `conditional_type` is in the tsx grammar's
8306    // `?` set exactly as it is in typescript's.
8307    #[cfg(feature = "typescript")]
8308    #[test]
8309    fn tsx_conditional_type_is_not_a_condition() {
8310        check_metrics::<TsxParser>(
8311            "type Cond<T> = T extends string ? number : boolean;
8312            function pick(a: number, b: number): number { return a > b ? 1 : 2; }",
8313            "foo.tsx",
8314            |metric| {
8315                assert_eq!(metric.abc.conditions_sum(), 2);
8316            },
8317        );
8318    }
8319
8320    // The TSX half of `typescript_ternary_qmark_alias_stays_unreachable`
8321    // — the tsx grammar declares the same `_ternary_qmark` external and
8322    // maps it back onto `anon_sym_QMARK` at its own id. Same
8323    // single-`?` fixture rule; see that test for why.
8324    #[cfg(feature = "typescript")]
8325    #[test]
8326    fn tsx_ternary_qmark_alias_stays_unreachable() {
8327        let parser = TsxParser::new(
8328            "function f(a: boolean): number { return a ? 1 : 2; }\n"
8329                .as_bytes()
8330                .to_vec(),
8331            std::path::Path::new("foo.tsx"),
8332            None,
8333        );
8334        assert!(ast_has_kind_id(&parser, Tsx::QMARK as u16));
8335        assert!(!ast_has_kind_id(&parser, Tsx::QMARK2 as u16));
8336    }
8337
8338    #[cfg(feature = "typescript")]
8339    #[test]
8340    fn tsx_abstract_class_abc() {
8341        check_metrics::<TsxParser>(
8342            "abstract class C {
8343                abstract a(): void;
8344                m(x: number): number {
8345                    if (x > 0) return 1;
8346                    return 0;
8347                }
8348            }",
8349            "foo.tsx",
8350            |metric| {
8351                assert_eq!(metric.abc.conditions_sum(), 1);
8352                assert_eq!(metric.abc.branches_sum(), 0);
8353                insta::assert_json_snapshot!(metric.abc);
8354            },
8355        );
8356    }
8357
8358    #[cfg(feature = "typescript")]
8359    #[test]
8360    fn tsx_interface_abc_zero() {
8361        check_metrics::<TsxParser>(
8362            "interface I { a(): void; p: string; }",
8363            "foo.tsx",
8364            |metric| {
8365                assert_eq!(metric.abc.assignments_sum(), 0);
8366                assert_eq!(metric.abc.branches_sum(), 0);
8367                assert_eq!(metric.abc.conditions_sum(), 0);
8368                insta::assert_json_snapshot!(metric.abc);
8369            },
8370        );
8371    }
8372
8373    #[cfg(feature = "typescript")]
8374    #[test]
8375    fn tsx_arrow_field_contributes_abc() {
8376        check_metrics::<TsxParser>(
8377            "class C {
8378                arrow = (x: number) => {
8379                    if (x > 0) return foo();
8380                    return 0;
8381                };
8382            }",
8383            "foo.tsx",
8384            |metric| {
8385                assert_eq!(metric.abc.conditions_sum(), 1);
8386                assert_eq!(metric.abc.branches_sum(), 1);
8387                insta::assert_json_snapshot!(metric.abc);
8388            },
8389        );
8390    }
8391
8392    #[cfg(feature = "typescript")]
8393    #[test]
8394    fn tsx_asi_const_does_not_suppress_later_assignments() {
8395        // TSX half of the #1277 cluster; see
8396        // `typescript_asi_const_does_not_suppress_later_assignments`.
8397        // TSX has its own `Const` / `VariableDeclarator` /
8398        // `LexicalDeclaration` kind ids, so the predicate is generated
8399        // separately and needs its own fixture.
8400        check_metrics::<TsxParser>(
8401            "function f() {
8402                const a = 1
8403                x = 2
8404                y = 3
8405            }",
8406            "foo.tsx",
8407            |metric| {
8408                assert_eq!(metric.abc.assignments_sum(), 2);
8409            },
8410        );
8411    }
8412
8413    #[cfg(feature = "typescript")]
8414    #[test]
8415    fn tsx_const_declarator_shapes_stay_suppressed() {
8416        // TSX half of `typescript_const_declarator_shapes_stay_suppressed`.
8417        check_metrics::<TsxParser>(
8418            "function f(o: any, xs: number[]) {
8419                const a = 1, b = 2
8420                const {c = 5, d: {e = 6} = {}} = o
8421                const [g = 7, ...[h = 8]] = xs
8422                let i = 3
8423                var j = 4
8424                for (const x of xs) { k(x) }
8425            }",
8426            "foo.tsx",
8427            |metric| {
8428                assert_eq!(metric.abc.assignments_sum(), 2);
8429            },
8430        );
8431    }
8432
8433    #[cfg(feature = "typescript")]
8434    #[test]
8435    fn tsx_parameter_property_init_not_assignment() {
8436        // Parameter properties contribute no `=`; the body's `let z = 0`
8437        // and the field initializer do.
8438        check_metrics::<TsxParser>(
8439            "class C {
8440                f: number = 0;
8441                constructor(public x: number) { let z = 0; }
8442            }",
8443            "foo.tsx",
8444            |metric| {
8445                assert_eq!(metric.abc.assignments_sum(), 2);
8446                insta::assert_json_snapshot!(metric.abc);
8447            },
8448        );
8449    }
8450
8451    // --- Ruby ABC tests ---------------------------------------------------
8452    //
8453    // Each Ruby `assignment` / `operator_assignment` is one assignment
8454    // regardless of whether the LHS is a local, instance, or class
8455    // variable. Every `call` / `super` / `yield` is one branch. Every
8456    // comparison-operator token inside a `binary` node plus each
8457    // `else` / `elsif` / `when` / `then` / `?` / `rescue` clause is
8458    // one condition.
8459
8460    #[cfg(feature = "ruby")]
8461    #[test]
8462    fn ruby_zero_abc() {
8463        check_metrics::<RubyParser>("\n", "foo.rb", |metric| {
8464            assert_eq!(metric.abc.assignments_sum(), 0);
8465            assert_eq!(metric.abc.branches_sum(), 0);
8466            assert_eq!(metric.abc.conditions_sum(), 0);
8467            insta::assert_json_snapshot!(metric.abc);
8468        });
8469    }
8470
8471    #[cfg(feature = "ruby")]
8472    #[test]
8473    fn ruby_simple_assignment() {
8474        check_metrics::<RubyParser>("def f\n  a = 1\n  b = 2\nend\n", "foo.rb", |metric| {
8475            assert_eq!(metric.abc.assignments_sum(), 2);
8476            assert_eq!(metric.abc.branches_sum(), 0);
8477            assert_eq!(metric.abc.conditions_sum(), 0);
8478            insta::assert_json_snapshot!(metric.abc);
8479        });
8480    }
8481
8482    #[cfg(feature = "ruby")]
8483    #[test]
8484    fn ruby_augmented_assignment() {
8485        // `+=`, `-=`, `*=` are `operator_assignment` nodes — each is
8486        // one assignment. Plain `=` to set the initial value adds one
8487        // more.
8488        check_metrics::<RubyParser>(
8489            "def f(x)\n  a = 0\n  a += x\n  a -= 1\n  a *= 2\nend\n",
8490            "foo.rb",
8491            |metric| {
8492                assert_eq!(metric.abc.assignments_sum(), 4);
8493                insta::assert_json_snapshot!(metric.abc);
8494            },
8495        );
8496    }
8497
8498    #[cfg(feature = "ruby")]
8499    #[test]
8500    fn ruby_logical_augmented_assignment() {
8501        // `||=` and `&&=` are also `operator_assignment` nodes.
8502        check_metrics::<RubyParser>("def f\n  @x ||= 0\n  @x &&= 1\nend\n", "foo.rb", |metric| {
8503            assert_eq!(metric.abc.assignments_sum(), 2);
8504            insta::assert_json_snapshot!(metric.abc);
8505        });
8506    }
8507
8508    #[cfg(feature = "ruby")]
8509    #[test]
8510    fn ruby_method_call_branch() {
8511        // Each method invocation is one branch.
8512        check_metrics::<RubyParser>(
8513            "def f(obj)\n  foo()\n  obj.bar(1)\nend\n",
8514            "foo.rb",
8515            |metric| {
8516                assert_eq!(metric.abc.branches_sum(), 2);
8517                insta::assert_json_snapshot!(metric.abc);
8518            },
8519        );
8520    }
8521
8522    #[cfg(feature = "ruby")]
8523    #[test]
8524    fn ruby_super_and_yield_branches() {
8525        // `super` and `yield` both count as branches (control-pass).
8526        check_metrics::<RubyParser>("def f\n  super\n  yield\nend\n", "foo.rb", |metric| {
8527            assert_eq!(metric.abc.branches_sum(), 2);
8528            assert_eq!(metric.abc.assignments_sum(), 0);
8529            insta::assert_json_snapshot!(metric.abc);
8530        });
8531    }
8532
8533    #[cfg(feature = "ruby")]
8534    #[test]
8535    fn ruby_attr_macro_is_branch() {
8536        // `attr_accessor` is a `Call3` node and registers as a branch
8537        // like any method invocation.
8538        check_metrics::<RubyParser>("class A\n  attr_accessor :x\nend\n", "foo.rb", |metric| {
8539            assert_eq!(metric.abc.branches_sum(), 1);
8540            insta::assert_json_snapshot!(metric.abc);
8541        });
8542    }
8543
8544    #[cfg(feature = "ruby")]
8545    #[test]
8546    fn ruby_comparison_conditions() {
8547        // Each comparison operator is one condition.
8548        check_metrics::<RubyParser>(
8549            "def f(a, b)\n  a == b\n  a != b\n  a < b\n  a > b\n  a <= b\n  a >= b\nend\n",
8550            "foo.rb",
8551            |metric| {
8552                assert_eq!(metric.abc.conditions_sum(), 6);
8553                insta::assert_json_snapshot!(metric.abc);
8554            },
8555        );
8556    }
8557
8558    #[cfg(feature = "ruby")]
8559    #[test]
8560    fn ruby_superclass_clause_is_not_a_condition() {
8561        // Regression for #1280: a superclass clause spells its `<` with the
8562        // same `LT` token as a comparison, but parents it under
8563        // `superclass` rather than `binary`, so the parent gate excludes
8564        // it. Before the gate every subclass declaration scored a phantom
8565        // condition.
8566        // expected: 0 conditions — the file contains no conditional at all;
8567        // the single assignment is `x = 1`.
8568        check_metrics::<RubyParser>(
8569            "class Foo < Bar\n  def plain\n    x = 1\n  end\nend\n",
8570            "foo.rb",
8571            |metric| {
8572                assert_eq!(metric.abc.conditions_sum(), 0);
8573                assert_eq!(metric.abc.assignments_sum(), 1);
8574            },
8575        );
8576    }
8577
8578    #[cfg(feature = "ruby")]
8579    #[test]
8580    fn ruby_operator_method_name_is_not_a_condition() {
8581        // The `<` naming an operator method parents under `operator`, which
8582        // the `binary` gate likewise excludes (#1280). The body carries a
8583        // real comparison so the expected value is not the all-zero default:
8584        // 1 discriminates "only the name token is excluded" from both "the
8585        // gate excludes everything" (0) and "nothing is gated" (2).
8586        // expected: 1 condition — the `@v < other` comparison, not the `def <`.
8587        check_metrics::<RubyParser>("def <(other)\n  @v < other\nend\n", "foo.rb", |metric| {
8588            assert_eq!(metric.abc.conditions_sum(), 1);
8589        });
8590    }
8591
8592    #[cfg(feature = "ruby")]
8593    #[test]
8594    fn ruby_every_comparison_operator_method_name_is_not_a_condition() {
8595        // The sibling half of #1280. `<` is not special: every comparison
8596        // and equality token Ruby lets you `def` parents under `operator`
8597        // in that position, so gating only `LT` / `GT` left `def ==`,
8598        // `def <=`, `def >=`, `def <=>`, `def !=` and `def =~` each
8599        // scoring a phantom condition — measured at 1 apiece for a body
8600        // containing no conditional at all.
8601        // expected: 0 conditions per definition; only the name token is on
8602        // the line, so a single non-zero total localises the regression.
8603        check_metrics::<RubyParser>(
8604            "def ==(o)\n  1\nend\n\
8605             def !=(o)\n  1\nend\n\
8606             def <=(o)\n  1\nend\n\
8607             def >=(o)\n  1\nend\n\
8608             def <=>(o)\n  1\nend\n\
8609             def =~(o)\n  1\nend\n\
8610             def <(o)\n  1\nend\n\
8611             def >(o)\n  1\nend\n",
8612            "foo.rb",
8613            |metric| {
8614                assert_eq!(metric.abc.conditions_sum(), 0);
8615            },
8616        );
8617        // The positive control: the same tokens inside a `binary` are real
8618        // comparisons and still count, so the gate is not blanket
8619        // suppression.
8620        check_metrics::<RubyParser>(
8621            "def cmp(a, b)\n  a == b || a <= b || a <=> b\nend\n",
8622            "foo.rb",
8623            |metric| {
8624                assert_eq!(metric.abc.conditions_sum(), 3);
8625            },
8626        );
8627    }
8628
8629    #[cfg(feature = "ruby")]
8630    #[test]
8631    fn ruby_case_match_in_arms_are_conditions() {
8632        // Regression for #977: each non-wildcard `case … in` arm is one
8633        // ABC condition, matching Python's `case_clause` handling. Using
8634        // literal patterns (no comparison operators) isolates the
8635        // `in_clause` contribution from any operand tokens.
8636        // expected: 2 conditions — one per `in 1` / `in 2` arm.
8637        check_metrics::<RubyParser>(
8638            "def f(x)\n  case x\n  in 1 then :one\n  in 2 then :two\n  end\nend\n",
8639            "foo.rb",
8640            |metric| {
8641                assert_eq!(metric.abc.conditions_sum(), 2);
8642            },
8643        );
8644    }
8645
8646    #[cfg(feature = "ruby")]
8647    #[test]
8648    fn ruby_case_match_guarded_wildcard_is_a_condition() {
8649        // Regression for #977: a guarded wildcard arm `in _ if x` is not a
8650        // bare default and counts as one ABC condition, while the trailing
8651        // bare `in _` adds none.
8652        // expected: 2 conditions — the guarded `in _ if x` arm, plus its
8653        // guard slot. The guard predicate is a bare identifier, so before
8654        // #1454 it contributed nothing and the total was 1; the slot is
8655        // what makes `if x`, `if x.even?` and `if x > 0` agree.
8656        check_metrics::<RubyParser>(
8657            "def f(x)\n  case x\n  in _ if x then :y\n  in _ then :default\n  end\nend\n",
8658            "foo.rb",
8659            |metric| {
8660                assert_eq!(metric.abc.conditions_sum(), 2);
8661            },
8662        );
8663    }
8664
8665    #[cfg(feature = "ruby")]
8666    #[test]
8667    fn ruby_case_match_bare_wildcard_is_not_a_condition() {
8668        // Regression for #977: a `case … in` whose only arm is the bare
8669        // wildcard `in _` (no guard) is the default arm and contributes no
8670        // ABC condition, keeping ABC and cyclomatic in lockstep on the
8671        // same construct.
8672        // expected: 0 conditions.
8673        check_metrics::<RubyParser>(
8674            "def f(x)\n  case x\n  in _ then :default\n  end\nend\n",
8675            "foo.rb",
8676            |metric| {
8677                assert_eq!(metric.abc.conditions_sum(), 0);
8678            },
8679        );
8680    }
8681
8682    #[cfg(feature = "ruby")]
8683    #[test]
8684    fn ruby_bare_predicate_control_flow_counts_one_condition() {
8685        // Regression for #696: idiomatic Ruby bare predicates
8686        // (`if flag` / `while flag` / `unless flag` / `until flag`) each
8687        // count one unary condition, matching Rust / C# / PHP / Python. The
8688        // condition field is read for both block and modifier forms.
8689        //
8690        // expected: 8 conditions — four block forms (`if`/`unless`/`while`/
8691        // `until`) plus the same four as modifiers, one each.
8692        check_metrics::<RubyParser>(
8693            "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",
8694            "foo.rb",
8695            |metric| {
8696                assert_eq!(metric.abc.conditions_sum(), 8);
8697            },
8698        );
8699    }
8700
8701    #[cfg(feature = "ruby")]
8702    #[test]
8703    fn ruby_bare_predicate_does_not_double_count_comparison_or_chain() {
8704        // `if a == b` counts only the `==` comparison (the condition field
8705        // is a `binary` node, adding nothing). `if a && b` counts the two
8706        // chain operands via the `&&` walker, again with the condition-field
8707        // arm adding nothing — so neither shape is double-counted (#696).
8708        //
8709        // expected: 3 — `==` (1) + the `a`,`b` operands of `&&` (2).
8710        check_metrics::<RubyParser>(
8711            "def f(a, b)\n  if a == b\n    x\n  end\n  if a && b\n    y\n  end\nend\n",
8712            "foo.rb",
8713            |metric| {
8714                assert_eq!(metric.abc.conditions_sum(), 3);
8715            },
8716        );
8717    }
8718
8719    #[cfg(feature = "ruby")]
8720    #[test]
8721    fn ruby_spaceship_and_case_equality() {
8722        // `<=>` and `===` are comparison operators (conditions).
8723        check_metrics::<RubyParser>(
8724            "def f(a, b)\n  a <=> b\n  a === b\nend\n",
8725            "foo.rb",
8726            |metric| {
8727                assert_eq!(metric.abc.conditions_sum(), 2);
8728                insta::assert_json_snapshot!(metric.abc);
8729            },
8730        );
8731    }
8732
8733    #[cfg(feature = "ruby")]
8734    #[test]
8735    fn ruby_ternary_condition() {
8736        // The `?` ternary marker is one condition; the inner `==` is
8737        // another.
8738        check_metrics::<RubyParser>("def f(x)\n  x == 0 ? :z : :nz\nend\n", "foo.rb", |metric| {
8739            assert_eq!(metric.abc.conditions_sum(), 2);
8740            insta::assert_json_snapshot!(metric.abc);
8741        });
8742    }
8743
8744    // Issue #1161. Ruby's ternary carried only the `?` token arm, so
8745    // `a ? !b : !c` scored 1 against the 4 that Java, C#, Groovy, the C
8746    // family, the JS family, PHP and Perl all report for the same
8747    // expression (#1102).
8748    //
8749    // Every expectation below is the value its C++ sibling
8750    // (`cpp_ternary_operand_slots_count_as_unary_conditions`) already
8751    // asserts for the same expression, so the two read as one table.
8752    #[cfg(feature = "ruby")]
8753    #[test]
8754    fn ruby_ternary_operand_slots_count_as_unary_conditions() {
8755        // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4.
8756        check_metrics::<RubyParser>("def f\n  x = a ? !b : !c\nend\n", "foo.rb", |metric| {
8757            assert_eq!(metric.abc.conditions_sum(), 4);
8758        });
8759        // No-double-count pin, and the assertion that catches the trap
8760        // this grammar sets: `-b` and `!b` are the SAME node kind
8761        // (`unary:284`), separated only by their operator. Scoring a
8762        // branch only when the peel proves it boolean — a `!` / `not`
8763        // layer, not the `unary` kind — is what keeps this at 2. An
8764        // implementation keying on `Unary` reads 3 here.
8765        // `?` (1) + `>` (1) = 2, unchanged by the fix: the parenthesised
8766        // condition unwraps to a comparison its own token already
8767        // counts, and neither branch is negated.
8768        check_metrics::<RubyParser>("def f\n  x = (a > 0) ? b : -b\nend\n", "foo.rb", |metric| {
8769            assert_eq!(metric.abc.conditions_sum(), 2);
8770        });
8771        // Nested — Ruby needs the inner ternary parenthesised. Outer `?`
8772        // (1) + outer condition `a` (1) + inner `?` (1) + inner
8773        // condition `b` (1) = 4. The outer consequence unwraps to the
8774        // inner `conditional`, which proves nothing boolean and is no
8775        // further paren / `!` layer, so it adds nothing on its own; the
8776        // inner ternary is reached by the walk, not by descent.
8777        check_metrics::<RubyParser>(
8778            "def f\n  x = a ? (b ? c : d) : e\nend\n",
8779            "foo.rb",
8780            |metric| {
8781                assert_eq!(metric.abc.conditions_sum(), 4);
8782            },
8783        );
8784        // A parenthesised condition, pinning the paren unwrap on the
8785        // condition slot: `(a)` is `parenthesized_statements`, which the
8786        // peel must look through.
8787        // `?` (1) + `(a)` (1) + `!b` (1) + `!c` (1) = 4.
8788        check_metrics::<RubyParser>("def f\n  x = (a) ? !b : !c\nend\n", "foo.rb", |metric| {
8789            assert_eq!(metric.abc.conditions_sum(), 4);
8790        });
8791        // A negated condition takes the same fallback through the `!`
8792        // unwrap rather than the paren one. `?` (1) + `!a` (1) = 2.
8793        check_metrics::<RubyParser>("def f\n  x = !a ? b : c\nend\n", "foo.rb", |metric| {
8794            assert_eq!(metric.abc.conditions_sum(), 2);
8795        });
8796    }
8797
8798    // The boolean-context seed must discriminate between the condition
8799    // slot and the two branch slots — not merely exist. None of the
8800    // fixtures above can tell the difference: their branches are either
8801    // `!`-unaries (which set the flag inside the unwrap loop regardless
8802    // of the seed) or kinds the loop breaks on before any terminal test.
8803    // A seed that returned `true` for every slot of a `Conditional`
8804    // leaves all five at their asserted values and fails nothing.
8805    //
8806    // A parenthesised *branch* is the input that separates them: the
8807    // unwrap reaches a bare terminal, so only the seed decides whether
8808    // it counts. `?` (1) + condition `a` (1) = 2 in both directions.
8809    #[cfg(feature = "ruby")]
8810    #[test]
8811    fn ruby_ternary_branch_operands_are_not_double_counted() {
8812        check_metrics::<RubyParser>("def f\n  x = a ? (b) : c\nend\n", "foo.rb", |metric| {
8813            assert_eq!(metric.abc.conditions_sum(), 2);
8814        });
8815        check_metrics::<RubyParser>("def f\n  x = a ? b : (c)\nend\n", "foo.rb", |metric| {
8816            assert_eq!(metric.abc.conditions_sum(), 2);
8817        });
8818        // The same pair with a comment before the operand. Comments are
8819        // tree-sitter `extras`, so they become the branch's previous
8820        // sibling — which is why the seed asks the grammar which child
8821        // is the `condition` field rather than testing that sibling for
8822        // `?` / `:` as the C family does. Under the token form both of
8823        // these read 3.
8824        check_metrics::<RubyParser>(
8825            "def f\n  x = a ?\n    # note\n    (b) : c\nend\n",
8826            "foo.rb",
8827            |metric| {
8828                assert_eq!(metric.abc.conditions_sum(), 2);
8829            },
8830        );
8831        check_metrics::<RubyParser>(
8832            "def f\n  x = a ? b :\n    # note\n    (c)\nend\n",
8833            "foo.rb",
8834            |metric| {
8835                assert_eq!(metric.abc.conditions_sum(), 2);
8836            },
8837        );
8838    }
8839
8840    #[cfg(feature = "ruby")]
8841    #[test]
8842    fn ruby_case_when_arms() {
8843        // Each `when` named clause and the `else` clause count as one
8844        // condition each; the `case` head and the implicit `then`
8845        // wrappers do not.
8846        check_metrics::<RubyParser>(
8847            "def f(x)\n  case x\n  when 1 then 'one'\n  when 2 then 'two'\n  else 'other'\n  end\nend\n",
8848            "foo.rb",
8849            |metric| {
8850                // 2 `when` + 1 `else` = 3 conditions.
8851                assert_eq!(metric.abc.conditions_sum(), 3);
8852                insta::assert_json_snapshot!(metric.abc);
8853            },
8854        );
8855    }
8856
8857    #[cfg(feature = "ruby")]
8858    #[test]
8859    fn ruby_elsif_and_else() {
8860        // `elsif` and `else` named clauses are conditions; their inner
8861        // `then` wrappers are not.
8862        check_metrics::<RubyParser>(
8863            "def f(x)\n  if x > 0\n    1\n  elsif x < 0\n    -1\n  else\n    0\n  end\nend\n",
8864            "foo.rb",
8865            |metric| {
8866                // `>`(1) + `elsif`(1) + `<`(1) + `else`(1) = 4.
8867                assert_eq!(metric.abc.conditions_sum(), 4);
8868                insta::assert_json_snapshot!(metric.abc);
8869            },
8870        );
8871    }
8872
8873    #[cfg(feature = "ruby")]
8874    #[test]
8875    fn ruby_rescue_clause_condition() {
8876        // The `rescue` named clause is one condition; the `rescue`
8877        // keyword token (`Rescue2`) is not counted on its own.
8878        // `do_it` without parens is an `identifier`, not a `call`, so
8879        // it contributes no branch. `handle(e)` is a `call` (1 branch).
8880        check_metrics::<RubyParser>(
8881            "def f\n  begin\n    do_it\n  rescue StandardError => e\n    handle(e)\n  end\nend\n",
8882            "foo.rb",
8883            |metric| {
8884                assert_eq!(metric.abc.conditions_sum(), 1);
8885                assert_eq!(metric.abc.branches_sum(), 1);
8886                insta::assert_json_snapshot!(metric.abc);
8887            },
8888        );
8889    }
8890
8891    #[cfg(feature = "ruby")]
8892    #[test]
8893    fn ruby_class_complex_function() {
8894        // Mixed: assignment(=), branch(call), conditions(`>` and `==`).
8895        check_metrics::<RubyParser>(
8896            "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",
8897            "foo.rb",
8898            |metric| {
8899                assert_eq!(metric.abc.assignments_sum(), 1);
8900                assert_eq!(metric.abc.branches_sum(), 1);
8901                // `>`(1) + `==`(1) = 2 conditions. `if` is not a
8902                // token; `&&` is `AMPAMP` and is not counted (see
8903                // the module-level `Stats` doc-comment for the
8904                // cross-language policy; #395, walker tracked in
8905                // #403).
8906                assert_eq!(metric.abc.conditions_sum(), 2);
8907                insta::assert_json_snapshot!(metric.abc);
8908            },
8909        );
8910    }
8911
8912    #[cfg(feature = "ruby")]
8913    #[test]
8914    fn ruby_unary_conditions_in_chain() {
8915        // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
8916        // `&&` / `||` chain is one condition. `a && b || c` → a, b, c each
8917        // contribute one. Ruby's `if` keyword is not a condition token.
8918        // expected: 3 unary conditions (matches the Java byte-equivalent).
8919        check_metrics::<RubyParser>(
8920            "def f(a, b, c)\n  if a && b || c\n    puts \"x\"\n  end\nend\n",
8921            "foo.rb",
8922            |metric| {
8923                assert_eq!(metric.abc.conditions_sum(), 3);
8924            },
8925        );
8926    }
8927
8928    #[cfg(feature = "ruby")]
8929    #[test]
8930    fn ruby_keyword_and_or_chain_counts_operands() {
8931        // The keyword forms `and` / `or` get the same Rule 9 treatment as
8932        // `&&` / `||`. expected: 3 unary conditions (a, b, c).
8933        check_metrics::<RubyParser>(
8934            "def f(a, b, c)\n  if a and b or c\n    puts \"x\"\n  end\nend\n",
8935            "foo.rb",
8936            |metric| {
8937                assert_eq!(metric.abc.conditions_sum(), 3);
8938            },
8939        );
8940    }
8941
8942    #[cfg(feature = "ruby")]
8943    #[test]
8944    fn ruby_negated_operand_is_unary_condition() {
8945        // A `!`-negated operand unwraps the `unary` node to the inner
8946        // identifier. expected: 2 (`a` and the `!b` operand).
8947        check_metrics::<RubyParser>(
8948            "def f(a, b)\n  if a && !b\n    puts \"x\"\n  end\nend\n",
8949            "foo.rb",
8950            |metric| {
8951                assert_eq!(metric.abc.conditions_sum(), 2);
8952            },
8953        );
8954    }
8955
8956    #[cfg(feature = "ruby")]
8957    #[test]
8958    fn ruby_comparison_operands_add_nothing() {
8959        // Isolation for Rule 9 (issue #557): when the `&&` operands are
8960        // themselves comparisons, the unary-condition walker must add
8961        // nothing on top of the two `>` comparisons already counted as
8962        // conditions — distinguishing the gap (bare boolean operands)
8963        // from ordinary relational conditions. Mirrors the Kotlin and
8964        // Elixir isolation tests. expected: 2 (the two `>` comparisons).
8965        check_metrics::<RubyParser>(
8966            "def f(x, y)\n  if x > 0 && y > 0\n    puts \"x\"\n  end\nend\n",
8967            "foo.rb",
8968            |metric| {
8969                assert_eq!(metric.abc.conditions_sum(), 2);
8970            },
8971        );
8972    }
8973
8974    // The two anchors every #1453 fixture needs, coupled on purpose as
8975    // `assert_kotlin_class_members` couples them for #1421. `subjects` is
8976    // the load-bearing one: the whole distinction is whether a `case`
8977    // carries a `value` field, no node kind spells that, and a fixture
8978    // that grew a subject would score its clauses the pre-#1453 way with
8979    // every other row still satisfied. Each fixture is top-level `def`s,
8980    // so the members are the unit space's.
8981    #[cfg(feature = "ruby")]
8982    fn assert_ruby_case_members(
8983        src: &str,
8984        subjects: usize,
8985        kinds: &[(u16, usize, &str)],
8986        expected: &[(&str, u64, u64)],
8987    ) {
8988        let parser = RubyParser::new(
8989            src.as_bytes().to_vec(),
8990            std::path::Path::new("foo.rb"),
8991            None,
8992        );
8993        let found = parser
8994            .root()
8995            .preorder()
8996            .filter(|n| n.kind_id() == Ruby::Case && n.child_by_field_name("value").is_some())
8997            .count();
8998        assert_eq!(found, subjects, "`case` subjects — the fixture moved");
8999        assert_fixture_spells::<RubyParser>(src, "foo.rb", kinds);
9000        check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
9001            assert_members_score(&space, expected);
9002        });
9003    }
9004
9005    // #1453's headline. A subject-less `when` clause's condition is an
9006    // ordinary boolean expression, so a comparison in it is already an
9007    // ABC condition through the token arm; the clause added a blanket one
9008    // on top, and `gt` scored 3 against its `if` analogue `iff`'s 2.
9009    //
9010    // `bare` and `call` are the controls that isolate the defect: same
9011    // clause shape, no operator for any other arm to count, so the clause
9012    // must still pay for its decision — 2 and 1 before the fix and after.
9013    // A fix that moved them would be attacking the clause count, which
9014    // was never wrong.
9015    //
9016    // `gt`, `bare` and `iff` sit one above cyclomatic's decision count
9017    // because ABC counts the `else` (Fitzpatrick Rule 5) and cyclomatic
9018    // does not; every other row is at `conditions == cyclomatic - 1`.
9019    // `cmt` puts a comment where the subject would go: an extra is not the
9020    // `value` field, so the `case` stays subject-less.
9021    #[cfg(feature = "ruby")]
9022    #[test]
9023    fn ruby_subjectless_case_when_counts_its_condition_once() {
9024        let src = "def gt(x)
9025  case
9026  when x > 5 then 1
9027  else 0
9028  end
9029end
9030def iff(x)
9031  if x > 5 then 1 else 0 end
9032end
9033def two(x)
9034  case
9035  when x > 5 then 1
9036  when x < 0 then 2
9037  end
9038end
9039def ge(x)
9040  case
9041  when x >= 5 then 1
9042  end
9043end
9044def andd(a, b)
9045  case
9046  when a > 1 && b < 2 then 1
9047  end
9048end
9049def orr(a, b)
9050  case
9051  when a || b then 1
9052  end
9053end
9054def kw_or(a, b)
9055  case
9056  when a or b then 1
9057  end
9058end
9059def bare(b)
9060  case
9061  when b then 1
9062  else 0
9063  end
9064end
9065def call(x)
9066  case
9067  when x.even? then 1
9068  end
9069end
9070def cmt(x)
9071  case # no subject
9072  when x > 5 then 1
9073  end
9074end
9075";
9076        assert_ruby_case_members(
9077            src,
9078            0,
9079            &[
9080                (Ruby::When as u16, 10, "`when` clauses"),
9081                (Ruby::GT as u16, 5, "`>` comparisons"),
9082                (Ruby::AMPAMP as u16, 1, "`andd`'s `&&`"),
9083                (Ruby::PIPEPIPE as u16, 1, "`orr`'s `||`"),
9084                (Ruby::Or as u16, 1, "`kw_or`'s `or`"),
9085                (Ruby::Comment as u16, 1, "`cmt`'s comment"),
9086            ],
9087            &[
9088                // (member, abc.conditions, cyclomatic)
9089                // Was 3: the `>` and the clause, for one decision.
9090                ("gt", 2, 2),
9091                ("iff", 2, 2),
9092                // Was 4.
9093                ("two", 2, 3),
9094                // Was 2: `>=` reaches the same token arm.
9095                ("ge", 1, 2),
9096                // Was 3. The compound condition keeps both comparisons —
9097                // suppressing the operators instead would collapse it.
9098                ("andd", 2, 3),
9099                // The Rule 9 walker scores both operands of a bare chain,
9100                // in both spellings; the clause adds nothing on top.
9101                ("orr", 2, 3),
9102                ("kw_or", 2, 3),
9103                ("bare", 2, 2),
9104                ("call", 1, 2),
9105                // Was 2.
9106                ("cmt", 1, 2),
9107            ],
9108        );
9109    }
9110
9111    // Wrappers and the shapes no other arm owns. `not_cmp`, `paren_cmp`,
9112    // `tern` and `test_pat` peel to an expression another arm already
9113    // scored, and each read one above parity before the fix. `paren` and
9114    // `notted` peel to a bare operand nothing else counts, so the clause
9115    // pays exactly as an `if` predicate would.
9116    //
9117    // `scope`, `neg`, `splat` and `arith` are why the clause asks "is this
9118    // already paid for" rather than "is this a known terminal": the `if`
9119    // slot asked the second question and scored each of these 0 until
9120    // #1520 moved it onto the clause's question — see
9121    // `ruby_condition_slot_pays_what_no_other_arm_counted`.
9122    #[cfg(feature = "ruby")]
9123    #[test]
9124    fn ruby_subjectless_case_when_condition_wrappers_count_once() {
9125        let src = "def paren(b)
9126  case
9127  when (b) then 1
9128  end
9129end
9130def notted(b)
9131  case
9132  when !b then 1
9133  end
9134end
9135def not_cmp(x)
9136  case
9137  when not(x > 1) then 1
9138  end
9139end
9140def paren_cmp(x)
9141  case
9142  when (x > 1) then 1
9143  end
9144end
9145def tern(x, a, b)
9146  case
9147  when x ? a : b then 1
9148  end
9149end
9150def test_pat(x)
9151  case
9152  when (x in Integer) then 1
9153  end
9154end
9155def scope
9156  case
9157  when Foo::Bar then 1
9158  end
9159end
9160def neg(x)
9161  case
9162  when -x then 1
9163  end
9164end
9165def splat(xs)
9166  case
9167  when *xs then 1
9168  end
9169end
9170def arith(x)
9171  case
9172  when x + 1 then 1
9173  end
9174end
9175";
9176        assert_ruby_case_members(
9177            src,
9178            0,
9179            &[
9180                (Ruby::When as u16, 10, "`when` clauses"),
9181                (
9182                    Ruby::ParenthesizedStatements as u16,
9183                    4,
9184                    "parenthesised operands",
9185                ),
9186                (Ruby::Conditional as u16, 1, "`tern`'s ternary"),
9187                (Ruby::TestPattern as u16, 1, "`test_pat`'s pattern test"),
9188                (Ruby::ScopeResolution2 as u16, 1, "`scope`'s `Foo::Bar`"),
9189                (Ruby::SplatArgument as u16, 1, "`splat`'s `*xs`"),
9190            ],
9191            &[
9192                ("paren", 1, 2),
9193                ("notted", 1, 2),
9194                // Each was 2.
9195                ("not_cmp", 1, 2),
9196                ("paren_cmp", 1, 2),
9197                ("test_pat", 1, 2),
9198                // Was 3. The ternary's own `?` and its condition `x` are
9199                // its two conditions, as in `if x ? a : b`.
9200                ("tern", 2, 3),
9201                ("scope", 1, 2),
9202                ("neg", 1, 2),
9203                ("splat", 1, 2),
9204                ("arith", 1, 2),
9205            ],
9206        );
9207    }
9208
9209    // A clause may list several patterns: an implicit `||`, so each is
9210    // one operand of that chain and scores once unless its own arm already
9211    // counted it — every row here must equal its `if … || …` twin in the
9212    // `or_*` defs. `bare_first` and `cmp_first` differ only in pattern
9213    // order and must agree; reading the first pattern alone, as Kotlin's
9214    // `kotlin_count_when_entry` does, would score them 2 and 1. None can
9215    // reach `conditions == cyclomatic - 1`: cyclomatic scores the clause
9216    // as one decision, and the `||` twins sit just as far above it.
9217    #[cfg(feature = "ruby")]
9218    #[test]
9219    fn ruby_subjectless_case_when_multiple_patterns() {
9220        let src = "def bare_alts(a, b)
9221  case
9222  when a, b then 1
9223  end
9224end
9225def alts(x)
9226  case
9227  when x > 1, x < -1 then 1
9228  end
9229end
9230def bare_first(a, x)
9231  case
9232  when a, x > 1 then 1
9233  end
9234end
9235def cmp_first(a, x)
9236  case
9237  when x > 1, a then 1
9238  end
9239end
9240def or_bare(a, b)
9241  if a || b then 1 end
9242end
9243def or_cmp(x)
9244  if x > 1 || x < -1 then 1 end
9245end
9246def or_mixed(a, x)
9247  if a || x > 1 then 1 end
9248end
9249";
9250        assert_ruby_case_members(
9251            src,
9252            0,
9253            &[
9254                (Ruby::When as u16, 4, "`when` clauses"),
9255                (Ruby::Pattern as u16, 8, "two patterns per clause"),
9256            ],
9257            &[
9258                // Was 1: one pattern went unscored.
9259                ("bare_alts", 2, 2),
9260                ("or_bare", 2, 3),
9261                // Was 3.
9262                ("alts", 2, 2),
9263                ("or_cmp", 2, 3),
9264                ("bare_first", 2, 2),
9265                ("cmp_first", 2, 2),
9266                ("or_mixed", 2, 3),
9267            ],
9268        );
9269    }
9270
9271    // An `elsif` is Java's `else if`: one condition for the `else`
9272    // (Rule 5) plus an `if` predicate slot. It paid only the `else`, so
9273    // `eb`, `eself`, `eneg` and `eparen` scored 2 where
9274    // `if (a) {} else if (b) {}` scores 3, while `ecmp` matched only
9275    // because its `>` arm paid the predicate. Each row now equals its Java
9276    // twin (`eelse` 4 / 3: Rule 5 counts the `else` arm, cyclomatic
9277    // does not), and `ecmp` / `eand` must not move, or the slot is paying
9278    // on top of the operator arms (#1520).
9279    #[cfg(feature = "ruby")]
9280    #[test]
9281    fn ruby_elsif_predicate_is_a_condition_slot() {
9282        let src = "def eb(a, b)
9283  if a then 1 elsif b then 2 end
9284end
9285def ecmp(a, x)
9286  if a then 1 elsif x > 0 then 2 end
9287end
9288def eself(a)
9289  if a then 1 elsif self then 2 end
9290end
9291def eneg(a, b)
9292  if a then 1 elsif !b then 2 end
9293end
9294def eparen(a, b)
9295  if a then 1 elsif (b) then 2 end
9296end
9297def eand(a, b, c)
9298  if a then 1 elsif b && c then 2 end
9299end
9300def eelse(a, b)
9301  if a then 1 elsif b then 2 else 3 end
9302end
9303";
9304        assert_fixture_spells::<RubyParser>(
9305            src,
9306            "foo.rb",
9307            &[
9308                (Ruby::Elsif as u16, 7, "one `elsif` per member"),
9309                (Ruby::Else as u16, 1, "`eelse`'s `else`"),
9310            ],
9311        );
9312        check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
9313            assert_members_score(
9314                &space,
9315                &[
9316                    // (member, abc.conditions, cyclomatic)
9317                    // Was 2.
9318                    ("eb", 3, 3),
9319                    ("ecmp", 3, 3),
9320                    // Was 2.
9321                    ("eself", 3, 3),
9322                    // Was 2.
9323                    ("eneg", 3, 3),
9324                    // Was 2.
9325                    ("eparen", 3, 3),
9326                    ("eand", 4, 4),
9327                    // Was 3.
9328                    ("eelse", 4, 3),
9329                ],
9330            );
9331        });
9332    }
9333
9334    // #1520: every Ruby condition slot — block and modifier `if` /
9335    // `unless` / `while` / `until`, a `case … in` guard, a ternary
9336    // condition — pays one condition unless another arm already charged
9337    // the same decision. The slot used to pay only for a predicate that
9338    // peeled down to a known boolean terminal, so the first block below
9339    // scored 0 for one decision each, while the same expression as a
9340    // subject-less `when` scored 1 (`ruby_subjectless_case_when_condition_
9341    // wrappers_count_once`). Every row sits at `conditions == cyclomatic
9342    // - 1` except `tern` and `ifelse`, which agree with each other one
9343    // above it: ABC counts the `?` / `else` arm and cyclomatic does not.
9344    // `chain` and `chain_neg` sit level with each other, as a Rule 9
9345    // chain scores one per operand.
9346    //
9347    // The second block must not move, or the slot is paying twice: each
9348    // predicate's comparison or chain operand is already counted by its
9349    // own arm. `begin_cmp` and `assign_cmp` are why the peel looks through
9350    // `begin … end` and an assignment's value — without that the slot
9351    // would see a non-terminal and pay on top of the `>`. `chain_neg`'s
9352    // operands are slots of their own: the Rule 9 walker pays for `-a`
9353    // and `-b` as it does for `a` and `b`, and the `if` adds nothing
9354    // (#1529; before it, the walker skipped both and the `if` paid one).
9355    #[cfg(feature = "ruby")]
9356    #[test]
9357    fn ruby_condition_slot_pays_what_no_other_arm_counted() {
9358        let src = "def scope
9359  if Foo::Bar then 1 end
9360end
9361def zelf
9362  if self then 1 end
9363end
9364def neg(x)
9365  if -x then 1 end
9366end
9367def defd(x)
9368  if defined?(x) then 1 end
9369end
9370def assign(x)
9371  if (y = x) then y end
9372end
9373def arith(x)
9374  if x + 1 then 1 end
9375end
9376def blk(b)
9377  g if begin b end
9378end
9379def unl
9380  g unless Foo::Bar
9381end
9382def til(x)
9383  g until -x
9384end
9385def whl
9386  while self do g end
9387end
9388def guard(x)
9389  case x
9390  in [y] if -y then 1
9391  end
9392end
9393def tern(x)
9394  -x ? 1 : 2
9395end
9396def ifelse(x)
9397  if -x then 1 else 2 end
9398end
9399def cmp(x)
9400  if x > 1 then 1 end
9401end
9402def begin_cmp(x)
9403  if begin x > 1 end then 1 end
9404end
9405def assign_cmp(x)
9406  if (y = x > 1) then y end
9407end
9408def notted(x)
9409  if !x then 1 end
9410end
9411def chain(a, b)
9412  if a && b then 1 end
9413end
9414def chain_neg(a, b)
9415  if -a && -b then 1 end
9416end
9417";
9418        assert_fixture_spells::<RubyParser>(
9419            src,
9420            "foo.rb",
9421            &[
9422                (Ruby::ScopeResolution2 as u16, 2, "`Foo::Bar`"),
9423                (Ruby::Zelf as u16, 2, "`self`"),
9424                (Ruby::Begin as u16, 2, "`begin … end`"),
9425                (Ruby::Assignment as u16, 2, "parenthesised assignments"),
9426                (Ruby::Conditional as u16, 1, "`tern`'s ternary"),
9427                (Ruby::IfGuard as u16, 1, "`guard`'s guard"),
9428            ],
9429        );
9430        check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
9431            assert_members_score(
9432                &space,
9433                &[
9434                    // (member, abc.conditions, cyclomatic); each of the
9435                    // first thirteen was one lower.
9436                    ("scope", 1, 2),
9437                    ("zelf", 1, 2),
9438                    ("neg", 1, 2),
9439                    ("defd", 1, 2),
9440                    ("assign", 1, 2),
9441                    ("arith", 1, 2),
9442                    ("blk", 1, 2),
9443                    ("unl", 1, 2),
9444                    ("til", 1, 2),
9445                    ("whl", 1, 2),
9446                    ("guard", 2, 3),
9447                    ("tern", 2, 2),
9448                    ("ifelse", 2, 2),
9449                    ("cmp", 1, 2),
9450                    ("begin_cmp", 1, 2),
9451                    ("assign_cmp", 1, 2),
9452                    ("notted", 1, 2),
9453                    ("chain", 2, 3),
9454                    // Was 0, then 1 until #1529.
9455                    ("chain_neg", 2, 3),
9456                ],
9457            );
9458        });
9459    }
9460
9461    // The other half of #1453: a subject-ful clause must not move. It
9462    // lists a pattern matched with `pattern === subject`, a comparison
9463    // written nowhere, so the clause itself pays for the decision.
9464    // `subj_cmp` is the subject-ful counterpart of `gt` above and keeps
9465    // the clause's count *and* its `>`: `y > 5` is a value compared
9466    // against `x`, two comparisons for one branch. `in_arms` (the
9467    // `InClause` arm), `unl` and `elsif_` share no code with `When` and
9468    // are here to pin that the fix left them alone.
9469    #[cfg(feature = "ruby")]
9470    #[test]
9471    fn ruby_subjectful_case_when_keeps_the_per_clause_count() {
9472        let src = "def konst(x)
9473  case x
9474  when 1 then 1
9475  when 2 then 2
9476  else 0
9477  end
9478end
9479def subj_alts(x)
9480  case x
9481  when 1, 2 then 1
9482  else 0
9483  end
9484end
9485def subj_cmp(x, y)
9486  case x
9487  when y > 5 then 1
9488  end
9489end
9490def in_arms(x)
9491  case x
9492  in Integer then 1
9493  in String then 2
9494  else 0
9495  end
9496end
9497def unl(x)
9498  unless x > 5 then 1 else 0 end
9499end
9500def elsif_(x)
9501  if x > 5 then 1 elsif x < 0 then 2 else 0 end
9502end
9503";
9504        // Three: `in_arms` is a `case_match`, a separate production.
9505        assert_ruby_case_members(
9506            src,
9507            3,
9508            &[
9509                (Ruby::CaseMatch as u16, 1, "`in_arms`'s `case`"),
9510                (Ruby::When as u16, 4, "`when` clauses"),
9511                (Ruby::InClause as u16, 2, "`in_arms`'s clauses"),
9512                (Ruby::GT as u16, 3, "`>` comparisons"),
9513            ],
9514            &[
9515                ("konst", 3, 3),
9516                ("subj_alts", 2, 2),
9517                ("subj_cmp", 2, 2),
9518                ("in_arms", 3, 3),
9519                ("unl", 2, 2),
9520                ("elsif_", 4, 3),
9521            ],
9522        );
9523    }
9524
9525    // ---------------------------------------------------------------
9526    // Default-impl placeholder smoke tests (audited in #188).
9527    //
9528    // These tests assert that the *current* default-impl languages
9529    // return ABC = 0/0/0 for source that DOES contain branches,
9530    // conditions, and assignments. When the real impl lands for any
9531    // of these languages, the corresponding assertion below will fire
9532    // — the implementer must update the expected values, which is the
9533    // gate. Tag the follow-up issue in each test.
9534    // ---------------------------------------------------------------
9535
9536    // --- Python ABC ---------------------------------------------------
9537
9538    #[cfg(feature = "python")]
9539    #[test]
9540    fn python_empty_module_zero() {
9541        check_metrics::<PythonParser>("", "empty.py", |metric| {
9542            assert_eq!(metric.abc.assignments_sum(), 0);
9543            assert_eq!(metric.abc.branches_sum(), 0);
9544            assert_eq!(metric.abc.conditions_sum(), 0);
9545            insta::assert_json_snapshot!(metric.abc);
9546        });
9547    }
9548
9549    #[cfg(feature = "python")]
9550    #[test]
9551    fn python_plain_assignments_count() {
9552        // Three plain `=` assignments → A=3. No branches, no conditions.
9553        check_metrics::<PythonParser>("x = 1\ny = 2\nz = x\n", "foo.py", |metric| {
9554            assert_eq!(metric.abc.assignments_sum(), 3);
9555            assert_eq!(metric.abc.branches_sum(), 0);
9556            assert_eq!(metric.abc.conditions_sum(), 0);
9557            insta::assert_json_snapshot!(metric.abc);
9558        });
9559    }
9560
9561    #[cfg(feature = "python")]
9562    #[test]
9563    fn python_typed_assignment_counts_bare_annotation_does_not() {
9564        // `x: int = 1` carries an `=`, so it counts.
9565        // `y: int` is a bare annotation (no `=`) — declares a type but
9566        // binds nothing; it must NOT inflate the assignment count.
9567        check_metrics::<PythonParser>("x: int = 1\ny: int\n", "foo.py", |metric| {
9568            assert_eq!(metric.abc.assignments_sum(), 1);
9569            insta::assert_json_snapshot!(metric.abc);
9570        });
9571    }
9572
9573    #[cfg(feature = "python")]
9574    #[test]
9575    fn python_augmented_assignments_count() {
9576        // Each augmented op counts once.
9577        check_metrics::<PythonParser>("x = 0\nx += 1\nx -= 1\nx *= 2\n", "foo.py", |metric| {
9578            // 1 plain `=` + 3 augmented = 4 assignments.
9579            assert_eq!(metric.abc.assignments_sum(), 4);
9580            insta::assert_json_snapshot!(metric.abc);
9581        });
9582    }
9583
9584    #[cfg(feature = "python")]
9585    #[test]
9586    fn python_walrus_counts_as_assignment() {
9587        // `x := 10` is a `NamedExpression` (PEP 572). It binds a value
9588        // → one assignment under Fitzpatrick's rule.
9589        check_metrics::<PythonParser>("if (n := 10) > 5:\n    pass\n", "foo.py", |metric| {
9590            // 1 assignment (walrus) + 1 condition (`> 5` is a
9591            // ComparisonOperator).
9592            assert_eq!(metric.abc.assignments_sum(), 1);
9593            assert_eq!(metric.abc.conditions_sum(), 1);
9594            insta::assert_json_snapshot!(metric.abc);
9595        });
9596    }
9597
9598    #[cfg(feature = "python")]
9599    #[test]
9600    fn python_calls_are_branches() {
9601        // `foo()`, `bar()`, `Baz()` (constructor) all parse as `Call`
9602        // → three branches.
9603        check_metrics::<PythonParser>(
9604            "def foo():\n    pass\ndef bar():\n    pass\nclass Baz:\n    pass\nfoo()\nbar()\nBaz()\n",
9605            "foo.py",
9606            |metric| {
9607                assert_eq!(metric.abc.branches_sum(), 3);
9608                assert_eq!(metric.abc.assignments_sum(), 0);
9609                insta::assert_json_snapshot!(metric.abc);
9610            },
9611        );
9612    }
9613
9614    #[cfg(feature = "python")]
9615    #[test]
9616    fn python_comparisons_count_conditions() {
9617        // `x > 0`, `x == y`, `x is None` are each a single
9618        // `ComparisonOperator` node — three conditions.
9619        check_metrics::<PythonParser>(
9620            "def f(x, y):\n    a = x > 0\n    b = x == y\n    c = x is None\n",
9621            "foo.py",
9622            |metric| {
9623                assert_eq!(metric.abc.conditions_sum(), 3);
9624                // 3 plain assignments; the comparisons are operands.
9625                assert_eq!(metric.abc.assignments_sum(), 3);
9626                insta::assert_json_snapshot!(metric.abc);
9627            },
9628        );
9629    }
9630
9631    #[cfg(feature = "python")]
9632    #[test]
9633    fn python_chained_comparison_counts_once() {
9634        // tree-sitter-python collapses `0 < x < 10` into a single
9635        // `ComparisonOperator` — one condition, not two.
9636        check_metrics::<PythonParser>("def f(x):\n    return 0 < x < 10\n", "foo.py", |metric| {
9637            assert_eq!(metric.abc.conditions_sum(), 1);
9638            insta::assert_json_snapshot!(metric.abc);
9639        });
9640    }
9641
9642    #[cfg(feature = "python")]
9643    #[test]
9644    fn python_number_truthy_condition_counts() {
9645        // Regression for #772: Python treats every non-zero number as
9646        // truthy, so `if 5:` and `x and 5` should each count their
9647        // numeric literal as a Fitzpatrick unary condition. Pre-fix
9648        // the former `python_bool_terminal_kinds!()` listed `True` /
9649        // `False` but omitted `Integer` / `Float`, so the walker dropped every
9650        // numeric-truthy operand (mirrors the Lua `Number` fix).
9651        check_metrics::<PythonParser>(
9652            "def f(a):\n    if 5:\n        pass\n    return a and 2\n",
9653            "foo.py",
9654            |metric| {
9655                // `if 5:` → walker counts the Integer literal (+1).
9656                // `a and 2` → `and` walker counts both operands:
9657                //   identifier `a` (+1), Integer `2` (+1).
9658                // Total: 3.
9659                assert_eq!(metric.abc.conditions_sum(), 3);
9660                insta::assert_json_snapshot!(metric.abc);
9661            },
9662        );
9663    }
9664
9665    #[cfg(feature = "python")]
9666    #[test]
9667    fn python_boolean_operators_not_counted_directly() {
9668        // Python's `and` / `or` are not counted as conditions on
9669        // their own (Fitzpatrick Rule 5; #395). Each operand is
9670        // instead counted as a unary conditional by the walker
9671        // (Rule 9; #403). `if a and b or c:` parses left-to-right
9672        // with `or` lower precedence: `(a and b) or c`. Walker
9673        // tallies: inner `and` counts `a`, `b` (+2); outer `or`
9674        // counts only the new outer operand `c` (+1; the inner
9675        // `(a and b)` BooleanOperator is not a terminal). Total
9676        // C = 3.
9677        check_metrics::<PythonParser>(
9678            "def f(a, b, c):\n    if a and b or c:\n        pass\n",
9679            "foo.py",
9680            |metric| {
9681                assert_eq!(metric.abc.conditions_sum(), 3);
9682                insta::assert_json_snapshot!(metric.abc);
9683            },
9684        );
9685    }
9686
9687    /// Python's unary `not` operator parses as `NotOperator` and now
9688    /// counts as one condition, matching Java's `!x` rule. Closes
9689    /// the parity gap noted in #214: without this, `if not flag:`
9690    /// reported 0 conditions while the Java equivalent reports 1.
9691    #[cfg(feature = "python")]
9692    #[test]
9693    fn python_unary_not_counts_as_condition() {
9694        check_metrics::<PythonParser>(
9695            "def f(flag):\n    if not flag:\n        return 1\n    return 0\n",
9696            "foo.py",
9697            |metric| {
9698                // One `NotOperator` -> 1 condition. The `if` itself
9699                // is structural and doesn't add an Abc condition.
9700                assert_eq!(metric.abc.conditions_sum(), 1);
9701                insta::assert_json_snapshot!(metric.abc);
9702            },
9703        );
9704    }
9705
9706    /// `return not flag` — the unary `not` is the entire return
9707    /// expression. Without `NotOperator` counted, this reports zero
9708    /// conditions; with it, one. Java's `return !flag;` is one.
9709    #[cfg(feature = "python")]
9710    #[test]
9711    fn python_return_unary_not_counts() {
9712        check_metrics::<PythonParser>("def f(flag):\n    return not flag\n", "foo.py", |metric| {
9713            assert_eq!(metric.abc.conditions_sum(), 1);
9714            insta::assert_json_snapshot!(metric.abc);
9715        });
9716    }
9717
9718    /// `foo(not ready, value)` — the unary `not` inside an argument
9719    /// list still contributes. Mirrors Java's `java_count_negated`
9720    /// walk over argument lists.
9721    #[cfg(feature = "python")]
9722    #[test]
9723    fn python_unary_not_in_argument_list_counts() {
9724        check_metrics::<PythonParser>(
9725            "def f(ready, value):\n    log(not ready, value)\n",
9726            "foo.py",
9727            |metric| {
9728                // 1 Call (log) -> 1 branch.
9729                // 1 NotOperator (not ready) -> 1 condition.
9730                assert_eq!(metric.abc.branches_sum(), 1);
9731                assert_eq!(metric.abc.conditions_sum(), 1);
9732                insta::assert_json_snapshot!(metric.abc);
9733            },
9734        );
9735    }
9736
9737    /// A negated comparison is one condition. `not (x > 0)` parses as
9738    /// `NotOperator(ParenthesizedExpression(ComparisonOperator))`; the
9739    /// comparison's arm pays and the `not` adds nothing, as Java's
9740    /// `!(x > 0)` scores 1 (#1526). This test asserted 2 on the false
9741    /// premise that Java scores 2.
9742    #[cfg(feature = "python")]
9743    #[test]
9744    fn python_unary_not_with_comparison_counts_each_once() {
9745        check_metrics::<PythonParser>(
9746            "def f(x):\n    if not (x > 0):\n        return 1\n    return 0\n",
9747            "foo.py",
9748            |metric| {
9749                // ComparisonOperator (1); the `not` negates a counted
9750                // comparison and adds nothing.
9751                assert_eq!(metric.abc.conditions_sum(), 1);
9752                insta::assert_json_snapshot!(metric.abc);
9753            },
9754        );
9755    }
9756
9757    /// `not x and y` parses as `BooleanOperator(NotOperator(x), and,
9758    /// y)`. The `and` itself is NOT counted (Fitzpatrick Rule 5
9759    /// lists only comparison operators); the `NotOperator` is
9760    /// counted at the top level (Rule 7); and the `y` operand is
9761    /// counted by the Rule 9 walker (issue #403). Total: 2.
9762    /// `NotOperator` is intentionally not walked-into a second
9763    /// time — the walker skips it to avoid double-counting.
9764    #[cfg(feature = "python")]
9765    #[test]
9766    fn python_unary_not_with_boolean_combinator_counts_each() {
9767        check_metrics::<PythonParser>(
9768            "def f(x, y):\n    if not x and y:\n        return 1\n    return 0\n",
9769            "foo.py",
9770            |metric| {
9771                // NotOperator (1) + walker on `and` finds `y` (1) = 2.
9772                assert_eq!(metric.abc.conditions_sum(), 2);
9773                insta::assert_json_snapshot!(metric.abc);
9774            },
9775        );
9776    }
9777
9778    #[cfg(feature = "python")]
9779    #[test]
9780    fn python_control_flow_arms_count_conditions() {
9781        // `elif`, `else`, `except`, `finally`, `case` each contribute
9782        // one condition. The comparisons in the `if`/`elif`/`while`
9783        // headers contribute their own ComparisonOperator counts.
9784        check_metrics::<PythonParser>(
9785            "def f(x):\n    if x > 0:\n        a = 1\n    elif x > -1:\n        a = 2\n    else:\n        a = 3\n",
9786            "foo.py",
9787            |metric| {
9788                // 2 ComparisonOperator (`x > 0`, `x > -1`) + 1
9789                // ElifClause + 1 ElseClause = 4 conditions.
9790                assert_eq!(metric.abc.conditions_sum(), 4);
9791                insta::assert_json_snapshot!(metric.abc);
9792            },
9793        );
9794    }
9795
9796    #[cfg(feature = "python")]
9797    #[test]
9798    fn python_ternary_counts_as_condition() {
9799        // `a if c else b` is `ConditionalExpression` → 1 condition.
9800        // `c > 0` adds 1 more (ComparisonOperator).
9801        check_metrics::<PythonParser>(
9802            "def f(c):\n    return 1 if c > 0 else 0\n",
9803            "foo.py",
9804            |metric| {
9805                assert_eq!(metric.abc.conditions_sum(), 2);
9806                insta::assert_json_snapshot!(metric.abc);
9807            },
9808        );
9809    }
9810
9811    // Issue #1161. Python counted the `conditional_expression` node but
9812    // never its condition slot, so `a if c() else b` reported 1 where
9813    // the equivalent `c() ? a : b` reports 2 everywhere else — and
9814    // the former `python_inspect_container`'s `ConditionalExpression`
9815    // boolean-context seed was unreachable, no call site having passed
9816    // that parent.
9817    #[cfg(feature = "python")]
9818    #[test]
9819    fn python_ternary_condition_slot_counts_as_a_unary_condition() {
9820        // ternary (1) + condition `c()` (1) = 2. `c()` is a `Call`, a
9821        // boolean terminal; it also adds one *branch*, not a condition.
9822        check_metrics::<PythonParser>(
9823            "def f(a, b, c):\n    return a if c() else b\n",
9824            "foo.py",
9825            |metric| {
9826                assert_eq!(metric.abc.conditions_sum(), 2);
9827            },
9828        );
9829        // A parenthesised condition: `(c)` is a `parenthesized_expression`,
9830        // so only the peel (`python_wrapper_operand`) can resolve it to
9831        // `c`, which the slot then pays for.
9832        // ternary (1) + `(c)` (1) = 2.
9833        check_metrics::<PythonParser>(
9834            "def f(a, b, c):\n    return a if (c) else b\n",
9835            "foo.py",
9836            |metric| {
9837                assert_eq!(metric.abc.conditions_sum(), 2);
9838            },
9839        );
9840        // A negated condition is *not* counted by the new slot: it is a
9841        // `NotOperator`, which already has its own top-level dispatcher
9842        // arm. ternary (1) + `not c` (1) = 2, not 3.
9843        check_metrics::<PythonParser>(
9844            "def f(a, b, c):\n    return a if not c else b\n",
9845            "foo.py",
9846            |metric| {
9847                assert_eq!(metric.abc.conditions_sum(), 2);
9848            },
9849        );
9850        // The cross-language reference case. `(not b) if a else (not c)`
9851        // is the exact semantic equivalent of `a ? !b : !c`, which every
9852        // other language reports as 4: ternary (1) + condition `a` (1) +
9853        // two `NotOperator`s (2). The negated operands come from
9854        // Python's own arm, the condition from the slot added here.
9855        //
9856        // #1161's resolution plan predicted this would stay 3, having
9857        // measured the condition slot's contribution against the pre-fix
9858        // total. 3 would have left Python disagreeing with every other
9859        // language on the reference expression — the gap the issue was
9860        // filed about.
9861        check_metrics::<PythonParser>(
9862            "def f(a, b, c):\n    return (not b) if a else (not c)\n",
9863            "foo.py",
9864            |metric| {
9865                assert_eq!(metric.abc.conditions_sum(), 4);
9866            },
9867        );
9868    }
9869
9870    // The double-count pin, and the reason Python gets a condition-slot
9871    // helper rather than a copy of `cpp_walk_ternary`: Python's branch
9872    // operands are counted by the top-level `NotOperator` /
9873    // `ComparisonOperator` arms, a different mechanism from every other
9874    // language's walker. Routing the branch slots through a negated-
9875    // operand walk as the C family does (`python_inspect_container`
9876    // before #1526) would have counted a parenthesised operand that
9877    // the identical unparenthesised expression scores at zero.
9878    //
9879    // Both fixtures below are 2 today and were 4 under such a copy, so a
9880    // later "make Python consistent with the others" change cannot land
9881    // silently.
9882    #[cfg(feature = "python")]
9883    #[test]
9884    fn python_ternary_branch_operands_are_not_double_counted() {
9885        // ternary (1) + condition `a` (1) = 2. The two parenthesised
9886        // operands add nothing — an unnegated branch is type-free.
9887        check_metrics::<PythonParser>(
9888            "def f(a, b, c):\n    return (b) if a else (c)\n",
9889            "foo.py",
9890            |metric| {
9891                assert_eq!(metric.abc.conditions_sum(), 2);
9892            },
9893        );
9894        // The unparenthesised form must agree: nothing about `(b)`
9895        // versus `b` is a condition.
9896        check_metrics::<PythonParser>(
9897            "def f(a, b, c):\n    return b if a else c\n",
9898            "foo.py",
9899            |metric| {
9900                assert_eq!(metric.abc.conditions_sum(), 2);
9901            },
9902        );
9903    }
9904
9905    // Comments are tree-sitter `extras`, so they arrive as direct
9906    // children of `conditional_expression` and shift every positional
9907    // index after them. `python_count_ternary_condition` therefore
9908    // anchors on the `if` keyword and skips comments after it; both
9909    // halves are needed and each fixture below fails without one.
9910    #[cfg(feature = "python")]
9911    #[test]
9912    fn python_ternary_condition_survives_an_interposed_comment() {
9913        // Comment before the keyword: `child(2)` is the `if` token here,
9914        // so a positional lookup reads 1. ternary (1) + `f()` (1) = 2.
9915        check_metrics::<PythonParser>(
9916            "def f(b, c):\n    return (b\n            # why\n            if f() else c)\n",
9917            "foo.py",
9918            |metric| {
9919                assert_eq!(metric.abc.conditions_sum(), 2);
9920            },
9921        );
9922        // Comment after the keyword: the child immediately following
9923        // `if` is the comment, so taking the first rather than the first
9924        // non-comment reads 1. ternary (1) + `f()` (1) = 2.
9925        check_metrics::<PythonParser>(
9926            "def f(b, c):\n    return (b if\n            # why\n            f() else c)\n",
9927            "foo.py",
9928            |metric| {
9929                assert_eq!(metric.abc.conditions_sum(), 2);
9930            },
9931        );
9932    }
9933
9934    #[cfg(feature = "python")]
9935    #[test]
9936    fn python_try_except_finally_count_conditions() {
9937        // ExceptClause + FinallyClause → 2 conditions.
9938        check_metrics::<PythonParser>(
9939            "def f():\n    try:\n        pass\n    except ValueError:\n        pass\n    finally:\n        pass\n",
9940            "foo.py",
9941            |metric| {
9942                assert_eq!(metric.abc.conditions_sum(), 2);
9943                insta::assert_json_snapshot!(metric.abc);
9944            },
9945        );
9946    }
9947
9948    #[cfg(feature = "python")]
9949    #[test]
9950    fn python_match_case_counts_conditions() {
9951        // Each non-wildcard `CaseClause` → 1 condition. The bare
9952        // `case _:` arm is the language-neutral `default:` equivalent
9953        // and is excluded (matches Rust's bare-`_` MatchArm filter and
9954        // Java/C#'s `default:` rule). Source has `case 1:` (counts) +
9955        // `case _:` (excluded) → C = 1.
9956        check_metrics::<PythonParser>(
9957            "def f(x):\n    match x:\n        case 1:\n            pass\n        case _:\n            pass\n",
9958            "foo.py",
9959            |metric| {
9960                assert_eq!(metric.abc.conditions_sum(), 1);
9961                insta::assert_json_snapshot!(metric.abc);
9962            },
9963        );
9964    }
9965
9966    #[cfg(feature = "python")]
9967    #[test]
9968    fn python_match_case_guarded_wildcard_counts() {
9969        // `case _ if g:` is NOT a bare wildcard — the guard
9970        // contributes real branching, so the arm counts as a
9971        // condition. Mirrors Rust's `_ if g => ...` behavior.
9972        // Source: `case 1:` (counts) + `case _ if x > 0:` (guarded
9973        // wildcard, counts) + `case _:` (bare wildcard, excluded) →
9974        // C from CaseClause = 2; the guard's `x > 0` adds one
9975        // ComparisonOperator → total C = 3.
9976        check_metrics::<PythonParser>(
9977            "def f(x):\n    match x:\n        case 1:\n            pass\n        case _ if x > 0:\n            pass\n        case _:\n            pass\n",
9978            "foo.py",
9979            |metric| {
9980                assert_eq!(metric.abc.conditions_sum(), 3);
9981                insta::assert_json_snapshot!(metric.abc);
9982            },
9983        );
9984    }
9985
9986    #[cfg(feature = "python")]
9987    #[test]
9988    fn python_complex_function_abc() {
9989        // Mixed-shape regression: assignments, calls, conditions all in
9990        // a single function.
9991        check_metrics::<PythonParser>(
9992            "def f(items, threshold):\n\
9993             \x20   result = []\n\
9994             \x20   for item in items:\n\
9995             \x20       if item > threshold:\n\
9996             \x20           result.append(item)\n\
9997             \x20   return result\n",
9998            "foo.py",
9999            |metric| {
10000                // assignments: `result = []` → 1
10001                // branches: `result.append(item)` is one call → 1
10002                // conditions: `item > threshold` is one
10003                // ComparisonOperator → 1
10004                assert_eq!(metric.abc.assignments_sum(), 1);
10005                assert_eq!(metric.abc.branches_sum(), 1);
10006                assert_eq!(metric.abc.conditions_sum(), 1);
10007                insta::assert_json_snapshot!(metric.abc);
10008            },
10009        );
10010    }
10011
10012    #[cfg(feature = "python")]
10013    #[test]
10014    fn python_if_multiple_conditions() {
10015        // Fitzpatrick Rule 9 walker on `and` / `or` (issue #403).
10016        //   - `if a or b or c or d:` → 4 (each operand counted once)
10017        //   - `if a and b and c:`    → 3
10018        //   - `if not a and not b:`  → 2 (two `NotOperator`s counted
10019        //     by the top-level dispatcher arm; the walker SKIPS
10020        //     `NotOperator` children to avoid double-counting)
10021        // Total: 4 + 3 + 2 = 9.
10022        check_metrics::<PythonParser>(
10023            "def f(a, b, c, d):\n\
10024             \x20   if a or b or c or d:           # +4c\n\
10025             \x20       pass\n\
10026             \x20   if a and b and c:              # +3c\n\
10027             \x20       pass\n\
10028             \x20   if not a and not b:            # +2c (NotOperator x2)\n\
10029             \x20       pass\n",
10030            "foo.py",
10031            |metric| {
10032                assert_eq!(metric.abc.conditions_sum(), 9);
10033                insta::assert_json_snapshot!(metric.abc);
10034            },
10035        );
10036    }
10037
10038    #[cfg(feature = "python")]
10039    #[test]
10040    fn python_while_conditions() {
10041        // Python has no `do { ... } while(cond);` construct, so this
10042        // mirrors only the `while` half of the Java suite. The
10043        // walker fires on each `and` / `or` token inside the loop
10044        // header.
10045        check_metrics::<PythonParser>(
10046            "def f(a, b):\n\
10047             \x20   while a or b:                  # +2c\n\
10048             \x20       break\n\
10049             \x20   while a and not b:             # +2c (a + NotOperator)\n\
10050             \x20       break\n",
10051            "foo.py",
10052            |metric| {
10053                assert_eq!(metric.abc.conditions_sum(), 4);
10054                insta::assert_json_snapshot!(metric.abc);
10055            },
10056        );
10057    }
10058
10059    #[cfg(feature = "python")]
10060    #[test]
10061    fn python_short_circuit_with_boolean_literal_operand() {
10062        // `a and True` reports 2 conditions: one identifier, one
10063        // True literal. Confirms `True` / `False` are in the walker
10064        // terminal set.
10065        check_metrics::<PythonParser>("def f(a):\n    return a and True\n", "foo.py", |metric| {
10066            assert_eq!(metric.abc.conditions_sum(), 2);
10067            insta::assert_json_snapshot!(metric.abc);
10068        });
10069    }
10070
10071    #[cfg(feature = "python")]
10072    #[test]
10073    fn python_await_expression_condition_counts() {
10074        // Regression for findings.md round-2 #2 (Python):
10075        // `if await ready(): pass` parses with `await` as the
10076        // condition node. Adding `Python::Await` to the
10077        // terminal-bool set mirrors the C# reference (lesson 19).
10078        check_metrics::<PythonParser>(
10079            "async def ready(): return True\n\
10080             async def f():\n    if await ready(): pass\n",
10081            "foo.py",
10082            |metric| {
10083                // ready() is a call (1 branch); await is the
10084                // condition (1).
10085                assert_eq!(metric.abc.branches_sum(), 1);
10086                assert_eq!(metric.abc.conditions_sum(), 1);
10087                insta::assert_json_snapshot!(metric.abc);
10088            },
10089        );
10090    }
10091
10092    #[cfg(feature = "python")]
10093    #[test]
10094    fn python_if_call_terminal_condition_counts_once() {
10095        // Pins the Phase-2B behaviour for Python's `Call` terminal-bool
10096        // kind: `if foo():` is a Fitzpatrick Rule 6 unary conditional
10097        // (a bare boolean-evaluating call as the if-condition). The
10098        // walker's terminal-at-top check fires once per call-condition;
10099        // the call itself separately contributes 1 branch. Surfaced
10100        // (and verified intentional) by the code-review pass on
10101        // Phase 2B.
10102        check_metrics::<PythonParser>("def f():\n    if foo(): pass\n", "foo.py", |metric| {
10103            assert_eq!(metric.abc.branches_sum(), 1);
10104            assert_eq!(metric.abc.conditions_sum(), 1);
10105            insta::assert_json_snapshot!(metric.abc);
10106        });
10107    }
10108
10109    #[cfg(feature = "python")]
10110    #[test]
10111    fn python_if_boolean_literal_condition() {
10112        // Phase 2B (issue #403): bare-boolean conditions count once.
10113        // Python has no paren wrap around if-conditions, so the
10114        // condition node is checked directly. The existing
10115        // NotOperator / ComparisonOperator arms continue to fire
10116        // for those shapes; only the bare-terminal cases (Identifier,
10117        // True, False, etc.) are added by the new arm.
10118        check_metrics::<PythonParser>(
10119            "def f(a):\n\
10120             \x20   if True: pass        # +1c\n\
10121             \x20   if False: pass       # +1c\n\
10122             \x20   while True: break    # +1c\n\
10123             \x20   if a: pass           # +1c (Rule 6 — bare identifier as condition)\n",
10124            "foo.py",
10125            |metric| {
10126                assert_eq!(metric.abc.conditions_sum(), 4);
10127                insta::assert_json_snapshot!(metric.abc);
10128            },
10129        );
10130    }
10131
10132    #[cfg(feature = "python")]
10133    #[test]
10134    fn python_methods_arguments_with_conditions() {
10135        // `m(not a, not b)` reports 2 conditions — both `NotOperator`
10136        // nodes are counted by Python's pre-existing top-level
10137        // NotOperator dispatcher arm. The argument-list walker does
10138        // not need a separate Python arm.
10139        check_metrics::<PythonParser>(
10140            "def f(a, b):\n\
10141             \x20   m(a, b)             # +1b\n\
10142             \x20   m(not a, not b)     # +1b +2c\n",
10143            "foo.py",
10144            |metric| {
10145                assert_eq!(metric.abc.branches_sum(), 2);
10146                assert_eq!(metric.abc.conditions_sum(), 2);
10147                insta::assert_json_snapshot!(metric.abc);
10148            },
10149        );
10150    }
10151
10152    #[cfg(feature = "python")]
10153    #[test]
10154    fn python_return_with_conditions() {
10155        // Phase 2B (issue #403). Python uses the pre-existing top-
10156        // level NotOperator / ComparisonOperator arms for return
10157        // expressions; no dedicated ReturnStatement walker arm is
10158        // needed.
10159        check_metrics::<PythonParser>(
10160            "def m1(z): return not (z >= 0)\n\
10161             def m2(x): return (((not x)))\n\
10162             def m3(x, y): return x and y\n",
10163            "foo.py",
10164            |metric| {
10165                // m1: ComparisonOperator (1); the `not` adds nothing,
10166                //     as Java's `return !(z >= 0)` scores 1 (#1526).
10167                // m2: NotOperator (1).
10168                // m3: walker on `and` counts both operands = 2.
10169                // Sum: 4.
10170                assert_eq!(metric.abc.conditions_sum(), 4);
10171                insta::assert_json_snapshot!(metric.abc);
10172            },
10173        );
10174    }
10175
10176    #[cfg(feature = "rust")]
10177    #[test]
10178    fn rust_empty_unit_zero() {
10179        // No code at all → A=B=C=0. Establishes the trait is wired up
10180        // and the per-language compute is reachable.
10181        check_metrics::<RustParser>("", "empty.rs", |metric| {
10182            assert_eq!(metric.abc.assignments_sum(), 0);
10183            assert_eq!(metric.abc.branches_sum(), 0);
10184            assert_eq!(metric.abc.conditions_sum(), 0);
10185            insta::assert_json_snapshot!(metric.abc);
10186        });
10187    }
10188
10189    #[cfg(feature = "rust")]
10190    #[test]
10191    fn rust_assignments_let_init_plain_and_compound() {
10192        // `let mut x = 0` is a `let_declaration` carrying an `=`
10193        // initializer → counts as 1 (matches Fitzpatrick's literal
10194        // "every `=` is an assignment" rule and the JS impl's
10195        // treatment of `let x = 5`). `x = 5` and `x = 7` are plain
10196        // `=` assignments → 2. `x += 2` is a compound assignment → 1.
10197        // Total A = 4.
10198        check_metrics::<RustParser>(
10199            "fn f() { let mut x = 0; x = 5; x += 2; x = 7; }",
10200            "foo.rs",
10201            |metric| {
10202                assert_eq!(metric.abc.assignments_sum(), 4);
10203                assert_eq!(metric.abc.branches_sum(), 0);
10204                assert_eq!(metric.abc.conditions_sum(), 0);
10205                insta::assert_json_snapshot!(metric.abc);
10206            },
10207        );
10208    }
10209
10210    #[cfg(feature = "rust")]
10211    #[test]
10212    fn rust_let_without_initializer_does_not_count() {
10213        // `let a;` is a `let_declaration` with NO `=` and no `value`
10214        // field — the binding is uninitialised. The arm only fires
10215        // when `value` is present, so this contributes zero to A.
10216        // `let _b;` is the same shape (the `_` pattern is still a
10217        // pattern, not a wildcard suppression of the binding).
10218        // Regression test for issue #393: only `=` counts, not the
10219        // bare declaration.
10220        check_metrics::<RustParser>(
10221            "fn f() { let a: i32; let _b: i32; a = 5; }",
10222            "foo.rs",
10223            |metric| {
10224                // Only `a = 5` (assignment_expression) → A = 1.
10225                assert_eq!(metric.abc.assignments_sum(), 1);
10226                insta::assert_json_snapshot!(metric.abc);
10227            },
10228        );
10229    }
10230
10231    #[cfg(feature = "rust")]
10232    #[test]
10233    fn rust_let_initializers_immutable_and_mutable_count() {
10234        // Issue #393: `let a = 1;`, `let b = 2;`, `let c = a + b;`,
10235        // `let mut d = 0;` are all `let_declaration` nodes carrying
10236        // an `=` initializer — each counts as 1 (Option B in the
10237        // issue body: literal Fitzpatrick, both `let` and `let mut`
10238        // count). `d = 5;` is one plain assignment_expression, `d
10239        // += 1;` is one compound. Total A = 4 + 1 + 1 = 6.
10240        check_metrics::<RustParser>(
10241            "fn f() { let a=1; let b=2; let c=a+b; let mut d=0; d=5; d+=1; }",
10242            "foo.rs",
10243            |metric| {
10244                assert_eq!(metric.abc.assignments_sum(), 6);
10245                insta::assert_json_snapshot!(metric.abc);
10246            },
10247        );
10248    }
10249
10250    #[cfg(feature = "rust")]
10251    #[test]
10252    fn rust_calls_are_branches() {
10253        // Free function call + method call (parses as call_expression
10254        // with a field_expression callee) + associated-fn call. All
10255        // three are `call_expression` → B = 3. Macro invocations like
10256        // `println!` parse as `macro_invocation`, NOT `call_expression`,
10257        // so they are not branches.
10258        check_metrics::<RustParser>(
10259            "fn f() { g(); 1.to_string(); String::new(); }\nfn g() {}\n",
10260            "foo.rs",
10261            |metric| {
10262                assert_eq!(metric.abc.branches_sum(), 3);
10263                assert_eq!(metric.abc.assignments_sum(), 0);
10264                assert_eq!(metric.abc.conditions_sum(), 0);
10265                insta::assert_json_snapshot!(metric.abc);
10266            },
10267        );
10268    }
10269
10270    #[cfg(feature = "rust")]
10271    #[test]
10272    fn rust_try_operator_is_branch() {
10273        // `?` parses as `try_expression` and counts as one branch
10274        // (short-circuit return on Err / None). The `Err(())` call
10275        // contributes one branch in addition (call_expression).
10276        check_metrics::<RustParser>(
10277            "fn f() -> Result<i32, ()> { let r: Result<i32, ()> = Err(()); Ok(r?) }",
10278            "foo.rs",
10279            |metric| {
10280                // Err(()) + Ok(...) + r? → 2 calls + 1 try = 3 branches.
10281                assert_eq!(metric.abc.branches_sum(), 3);
10282                insta::assert_json_snapshot!(metric.abc);
10283            },
10284        );
10285    }
10286
10287    #[cfg(feature = "rust")]
10288    #[test]
10289    fn rust_comparisons_count_conditions() {
10290        // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six
10291        // comparisons → C = 6.
10292        check_metrics::<RustParser>(
10293            "fn f(a: i32, b: i32) -> bool { a < b || a > b || a <= b || a >= b || a == b || a != b }",
10294            "foo.rs",
10295            |metric| {
10296                assert_eq!(metric.abc.conditions_sum(), 6);
10297                insta::assert_json_snapshot!(metric.abc);
10298            },
10299        );
10300    }
10301
10302    #[cfg(feature = "rust")]
10303    #[test]
10304    fn rust_generic_brackets_not_conditions() {
10305        // `<` / `>` in `Vec<i32>` are TypeArguments delimiters, not
10306        // comparison operators. The parent-check in the LT/GT arms
10307        // must filter them out. Expected C = 0.
10308        check_metrics::<RustParser>(
10309            "fn f() -> Vec<i32> { Vec::<i32>::new() }",
10310            "foo.rs",
10311            |metric| {
10312                assert_eq!(metric.abc.conditions_sum(), 0);
10313                insta::assert_json_snapshot!(metric.abc);
10314            },
10315        );
10316    }
10317
10318    #[cfg(feature = "rust")]
10319    #[test]
10320    fn rust_if_let_counts_as_condition() {
10321        // `if let Some(v) = opt { ... }` introduces a `let_condition`
10322        // → 1 condition. The `if` keyword itself does not add another
10323        // count — Fitzpatrick counts conditions, not branch keywords.
10324        check_metrics::<RustParser>(
10325            "fn f(opt: Option<i32>) { if let Some(_v) = opt { } }",
10326            "foo.rs",
10327            |metric| {
10328                assert_eq!(metric.abc.conditions_sum(), 1);
10329                insta::assert_json_snapshot!(metric.abc);
10330            },
10331        );
10332    }
10333
10334    #[cfg(feature = "rust")]
10335    #[test]
10336    fn rust_while_let_counts_as_condition() {
10337        // `while let Some(y) = it.next() { ... }` is also a
10338        // `let_condition` (the `while` form). One condition; the
10339        // `it.next()` call adds one branch.
10340        check_metrics::<RustParser>(
10341            "fn f(mut it: std::vec::IntoIter<i32>) { while let Some(_y) = it.next() { } }",
10342            "foo.rs",
10343            |metric| {
10344                assert_eq!(metric.abc.conditions_sum(), 1);
10345                assert_eq!(metric.abc.branches_sum(), 1);
10346                insta::assert_json_snapshot!(metric.abc);
10347            },
10348        );
10349    }
10350
10351    #[cfg(feature = "rust")]
10352    #[test]
10353    fn rust_match_arms_count_conditions_wildcard_excluded() {
10354        // Three arms: `0 => 1`, `n if n > 0 => n`, `_ => -1`. The
10355        // bare wildcard is the `default:` equivalent and is skipped.
10356        // The guarded arm has a `n if n > 0` pattern (more than one
10357        // child in the match_pattern) and still counts. Two non-wildcard
10358        // arms → C = 2 from MatchArm. Plus the comparison `n > 0`
10359        // adds one more → C = 3.
10360        check_metrics::<RustParser>(
10361            "fn f(x: i32) -> i32 { match x { 0 => 1, n if n > 0 => n, _ => -1, } }",
10362            "foo.rs",
10363            |metric| {
10364                assert_eq!(metric.abc.conditions_sum(), 3);
10365                insta::assert_json_snapshot!(metric.abc);
10366            },
10367        );
10368    }
10369
10370    #[cfg(feature = "rust")]
10371    #[test]
10372    fn rust_else_counts_as_condition() {
10373        // `if a > b { ... } else { ... }` → `a > b` is one condition,
10374        // `else` is one condition → C = 2.
10375        check_metrics::<RustParser>(
10376            "fn f(a: i32, b: i32) -> i32 { if a > b { a } else { b } }",
10377            "foo.rs",
10378            |metric| {
10379                assert_eq!(metric.abc.conditions_sum(), 2);
10380                insta::assert_json_snapshot!(metric.abc);
10381            },
10382        );
10383    }
10384
10385    #[cfg(feature = "rust")]
10386    #[test]
10387    fn rust_let_chain2_hidden_rule_drift_marker() {
10388        // Drift marker (findings.md round-2 #3): `Rust::LetChain2`
10389        // maps to the hidden grammar rule `_let_chain`. At the
10390        // pinned tree-sitter-rust version it is never emitted as a
10391        // concrete node — the visible `LetChain` (= 352) carries
10392        // every let-chain. We list `LetChain2` defensively in
10393        // `rust_condition_scores_itself` and `rust_count_chain_operands`
10394        // (lesson 34); if a future grammar bump promotes
10395        // `_let_chain` to a visible rule, this assertion fails
10396        // loudly so the maintainer knows to verify the walker still
10397        // counts correctly for the new shape.
10398        let src = "fn f(a: bool, b: Option<i32>) {\n\
10399                   \x20   if a && let Some(_) = b { }\n\
10400                   }\n";
10401        let parser = RustParser::new(
10402            src.as_bytes().to_vec(),
10403            &std::path::PathBuf::from("foo.rs"),
10404            None,
10405        );
10406        assert!(!ast_has_kind_id(&parser, Rust::LetChain2 as u16));
10407    }
10408
10409    #[cfg(feature = "rust")]
10410    #[test]
10411    fn rust_scoped_identifier_condition_counts() {
10412        // Regression for findings.md round-2 #1 (Rust):
10413        // `if crate::FLAG {}` parses with `scoped_identifier` as the
10414        // condition node. Pre-fix, the former `rust_bool_terminal_kinds!()`
10415        // listed only `Identifier` so the walker reached the
10416        // `scoped_identifier` child, found it non-terminal /
10417        // non-paren / non-unary, and broke without counting.
10418        // Mirrors the C# fix in #372 (lesson 19) for
10419        // `MemberAccessExpression`.
10420        check_metrics::<RustParser>("fn f() { if crate::FLAG { } }\n", "foo.rs", |metric| {
10421            assert_eq!(metric.abc.conditions_sum(), 1);
10422            insta::assert_json_snapshot!(metric.abc);
10423        });
10424    }
10425
10426    #[cfg(feature = "rust")]
10427    #[test]
10428    fn rust_await_expression_condition_counts() {
10429        // Regression for findings.md round-2 #2 (Rust):
10430        // `if ready().await {}` parses with `await_expression` as
10431        // the condition node. Adding `Rust::AwaitExpression` to the
10432        // former terminal-bool set closed the parity gap with the C#
10433        // reference (the former `csharp_bool_terminal_kinds!()`).
10434        check_metrics::<RustParser>(
10435            "async fn ready() -> bool { true }\n\
10436             async fn f() { if ready().await { } }\n",
10437            "foo.rs",
10438            |metric| {
10439                // ready() is a call (1 branch); `ready().await` is
10440                // the unary boolean condition (1).
10441                assert_eq!(metric.abc.branches_sum(), 1);
10442                assert_eq!(metric.abc.conditions_sum(), 1);
10443                insta::assert_json_snapshot!(metric.abc);
10444            },
10445        );
10446    }
10447
10448    #[cfg(feature = "rust")]
10449    #[test]
10450    fn rust_complex_function_abc() {
10451        // Mixed-shape regression: assignments, calls, conditions, `?`,
10452        // `if let`, `match` in one body. Verified by hand:
10453        // - assignments: `let mut x = 0` (let init), `x = 5`, `x += 2`,
10454        //   `let _ = ...` (let init), `let r: ... = Err(())` (let init),
10455        //   `let _v = r?` (let init) → A = 6 (post-#393: every `=`
10456        //   initializer in a `let_declaration` is one assignment, in
10457        //   line with the literal Fitzpatrick reading).
10458        // - branches: `xs.iter()`, `.next()`, `Err(())`, `r?` → B = 4
10459        //   (3 calls + 1 try).
10460        // - conditions: `if let Some(v) = opt` → 1, `match x` arms
10461        //   `0`, `n if n>0` (wildcard excluded) → 2, `n > 0` → 1.
10462        //   Total C = 4.
10463        check_metrics::<RustParser>(
10464            "fn f(opt: Option<i32>, xs: Vec<i32>) -> Result<i32, ()> {\n\
10465             \x20   let mut x = 0;\n\
10466             \x20   x = 5;\n\
10467             \x20   x += 2;\n\
10468             \x20   if let Some(_v) = opt { }\n\
10469             \x20   let _ = xs.iter().next();\n\
10470             \x20   let r: Result<i32, ()> = Err(());\n\
10471             \x20   let _v = r?;\n\
10472             \x20   Ok(match x {\n\
10473             \x20       0 => 1,\n\
10474             \x20       n if n > 0 => n,\n\
10475             \x20       _ => -1,\n\
10476             \x20   })\n\
10477             }\n",
10478            "foo.rs",
10479            |metric| {
10480                assert_eq!(metric.abc.assignments_sum(), 6);
10481                // calls: xs.iter(), .next(), Err(()), Ok(...) → 4 calls
10482                // plus 1 try (`r?`) → 5 branches.
10483                assert_eq!(metric.abc.branches_sum(), 5);
10484                // 1 let_condition + 2 non-wildcard match_arms + 1
10485                // comparison (`n > 0`) → 4.
10486                assert_eq!(metric.abc.conditions_sum(), 4);
10487                insta::assert_json_snapshot!(metric.abc);
10488            },
10489        );
10490    }
10491
10492    #[cfg(feature = "rust")]
10493    #[test]
10494    fn rust_let_chain_bare_identifier_operand_counts() {
10495        // Regression: pre-fix, `if a && let Some(_z) = y { }` reported
10496        // 1 condition (only the LetCondition). The bare-identifier
10497        // `a` operand was lost because Rust 2024 wraps let-chain
10498        // `&&` operands in a `LetChain` node (not `BinaryExpression`)
10499        // and the former `rust_count_unary_conditions` only counted
10500        // terminals under a `BinaryExpression` parent. Allowing `LetChain` /
10501        // `LetChain2` as known-bool list parents fixed the loss.
10502        // Expected: LetCondition (1) + walker on `a` (1) = 2.
10503        check_metrics::<RustParser>(
10504            "fn f(a: bool, y: Option<i32>) {\n\
10505             \x20   if a && let Some(_z) = y { }\n\
10506             }\n",
10507            "foo.rs",
10508            |metric| {
10509                assert_eq!(metric.abc.conditions_sum(), 2);
10510                insta::assert_json_snapshot!(metric.abc);
10511            },
10512        );
10513    }
10514
10515    #[cfg(feature = "rust")]
10516    #[test]
10517    fn rust_if_multiple_conditions() {
10518        // Fitzpatrick Rule 7 / Listing 2 (issue #403): every operand of
10519        // a `&&` / `||` chain is one condition. Mirrors
10520        // `java_if_multiple_conditions`. Rust's `if` head has no
10521        // parentheses, but the walker fires on each `&&` / `||` token
10522        // and walks the parent `binary_expression` regardless.
10523        check_metrics::<RustParser>(
10524            "fn f(a: bool, b: bool, c: bool, d: bool) -> i32 {\n\
10525             \x20   if a || b || c || d { return 1; }    // +4c\n\
10526             \x20   if a && b && c { return 2; }         // +3c\n\
10527             \x20   if !a && !b { return 3; }            // +2c\n\
10528             \x20   0\n\
10529             }\n",
10530            "foo.rs",
10531            |metric| {
10532                // 4 + 3 + 2 = 9
10533                assert_eq!(metric.abc.conditions_sum(), 9);
10534                insta::assert_json_snapshot!(metric.abc);
10535            },
10536        );
10537    }
10538
10539    #[cfg(feature = "rust")]
10540    #[test]
10541    fn rust_while_conditions() {
10542        // Rust has no `do { ... } while(cond);` construct, so this
10543        // mirrors only the `while` half of `java_while_and_do_while_conditions`.
10544        // Each operand of the `&&` / `||` chain in the loop condition
10545        // counts as one Fitzpatrick condition (Rule 7).
10546        check_metrics::<RustParser>(
10547            "fn f(a: bool, b: bool) {\n\
10548             \x20   while a || b { break; }       // +2c\n\
10549             \x20   while a && !b { break; }      // +2c\n\
10550             }\n",
10551            "foo.rs",
10552            |metric| {
10553                assert_eq!(metric.abc.conditions_sum(), 4);
10554                insta::assert_json_snapshot!(metric.abc);
10555            },
10556        );
10557    }
10558
10559    #[cfg(feature = "rust")]
10560    #[test]
10561    fn rust_if_boolean_literal_condition() {
10562        // Phase 2B (issue #403): a condition whose entire body is a
10563        // boolean literal counts as one Fitzpatrick condition.
10564        // `if true {}` → 1, `if !false {}` → 1 (unary unwrap), and
10565        // `while true { break }` → 1.
10566        check_metrics::<RustParser>(
10567            "fn f() {\n\
10568             \x20   if true { }                  // +1c\n\
10569             \x20   if !false { }                // +1c\n\
10570             \x20   while true { break; }        // +1c\n\
10571             }\n",
10572            "foo.rs",
10573            |metric| {
10574                assert_eq!(metric.abc.conditions_sum(), 3);
10575                insta::assert_json_snapshot!(metric.abc);
10576            },
10577        );
10578    }
10579
10580    #[cfg(feature = "rust")]
10581    #[test]
10582    fn rust_methods_arguments_with_conditions() {
10583        // Phase 2B (issue #403): unary-conditional arguments to a
10584        // call each count once. `m(!a, !b)` → 2 conditions + 1
10585        // branch (the call itself). Bare identifier arguments do
10586        // NOT count (they reach the count_unary_conditions list with
10587        // list_kind = Arguments, not BinaryExpression).
10588        check_metrics::<RustParser>(
10589            "fn f(a: bool, b: bool) {\n\
10590             \x20   m(a, b);                     // +1b\n\
10591             \x20   m(!a, !b);                   // +1b +2c\n\
10592             \x20   m(!a, b, !a);                // +1b +2c\n\
10593             }\n",
10594            "foo.rs",
10595            |metric| {
10596                assert_eq!(metric.abc.branches_sum(), 3);
10597                assert_eq!(metric.abc.conditions_sum(), 4);
10598                insta::assert_json_snapshot!(metric.abc);
10599            },
10600        );
10601    }
10602
10603    #[cfg(feature = "rust")]
10604    #[test]
10605    fn rust_return_with_conditions() {
10606        // Phase 2B (issue #403). Mirrors `java_return_with_conditions`
10607        // — `return !a` / `return x && y` count their unary
10608        // conditional operands. Per Fitzpatrick Rule 7, a `!`-wrapped
10609        // relational expression contributes ONE condition (the
10610        // relational op itself) — the `!` does not add a second
10611        // count when its operand is already a comparison.
10612        check_metrics::<RustParser>(
10613            "fn m1(z: i32) -> bool { return !(z >= 0); }\n\
10614             fn m2(x: bool) -> bool { return (((!x))); }\n\
10615             fn m3(x: bool, y: bool) -> bool { return x && y; }\n\
10616             fn m4(y: bool, z: i32) -> bool { return y || (z < 0); }\n",
10617            "foo.rs",
10618            |metric| {
10619                // m1: !(z >= 0) → the `>=` contributes 1; the unary
10620                //     `!` wraps a paren'd BinaryExpression, which
10621                //     inspect_container does not unwrap further →
10622                //     no walker count. Total: 1.
10623                // m2: (((!x))) → ReturnExpression arm walks (((!x))).
10624                //     inspect_container unwraps three parens and one
10625                //     unary, reaches Identifier `x`, has_boolean_content
10626                //     was seeded true by the unary-not flip. +1.
10627                // m3: x && y → `&&` walker counts both terminals → 2.
10628                // m4: y || (z < 0) → `||` walker counts `y` (terminal,
10629                //     +1); the `<` contributes 1 via its own arm; the
10630                //     paren'd BinaryExpression `(z < 0)` is not
10631                //     terminal under the walker → no extra count.
10632                //     Total: 2.
10633                // Sum: 1 + 1 + 2 + 2 = 6.
10634                assert_eq!(metric.abc.conditions_sum(), 6);
10635                insta::assert_json_snapshot!(metric.abc);
10636            },
10637        );
10638    }
10639
10640    #[cfg(feature = "rust")]
10641    #[test]
10642    fn rust_short_circuit_with_boolean_literal_operand() {
10643        // `if a && true` reports 2 conditions: one for the identifier
10644        // operand, one for the boolean-literal operand. Confirms the
10645        // walker terminal set includes `BooleanLiteral`.
10646        check_metrics::<RustParser>(
10647            "fn f(a: bool) -> bool { a && true }\n",
10648            "foo.rs",
10649            |metric| {
10650                assert_eq!(metric.abc.conditions_sum(), 2);
10651                insta::assert_json_snapshot!(metric.abc);
10652            },
10653        );
10654    }
10655
10656    // ----- Go -----
10657
10658    #[cfg(feature = "go")]
10659    #[test]
10660    fn go_empty_unit_zero() {
10661        // Package declaration only — no Fitzpatrick events. Confirms the
10662        // GoCode Abc trait is wired up and emits zero counts.
10663        check_metrics::<GoParser>("package main\n", "empty.go", |metric| {
10664            assert_eq!(metric.abc.assignments_sum(), 0);
10665            assert_eq!(metric.abc.branches_sum(), 0);
10666            assert_eq!(metric.abc.conditions_sum(), 0);
10667            insta::assert_json_snapshot!(metric.abc);
10668        });
10669    }
10670
10671    #[cfg(feature = "go")]
10672    #[test]
10673    fn go_assignments_count_plain_compound_short_var_and_incdec() {
10674        // `x := 0` (short var decl), `x = 5` and `x = 7` (plain `=`),
10675        // `x += 2` (compound), `x++` (inc), and the initialized
10676        // declaration `var y = 1` — which is counted, matching the Rust
10677        // and Java rules for `let y = 1` / `int y = 1` (both measured at
10678        // one assignment each). The comment here previously claimed the
10679        // opposite and pinned Go at 6 (#1278).
10680        check_metrics::<GoParser>(
10681            "package main\nfunc f() { var y = 1; _ = y; x := 0; x = 5; x += 2; x = 7; x++ }\n",
10682            "foo.go",
10683            |metric| {
10684                // `_ = y` is itself an assignment_statement → +1.
10685                // var y=1 + _=y + x:= + x=5 + x+=2 + x=7 + x++ → 7
10686                assert_eq!(metric.abc.assignments_sum(), 7);
10687                assert_eq!(metric.abc.branches_sum(), 0);
10688                assert_eq!(metric.abc.conditions_sum(), 0);
10689                insta::assert_json_snapshot!(metric.abc);
10690            },
10691        );
10692    }
10693
10694    #[cfg(feature = "go")]
10695    #[test]
10696    fn go_var_declarations_count_only_when_initialized() {
10697        // Regression for #1278: a `var` declaration with an initializer is
10698        // a `var_spec` carrying a `value` field, not an
10699        // `assignment_statement` or `short_var_declaration`, so it scored
10700        // zero — `var x = 5` and `x := 5` are the same binding spelled two
10701        // ways. Both typed and untyped initializers count; an
10702        // uninitialized `var z int` and a `const` do not.
10703        // expected: 3 assignments — `var x = 5`, `var y int = 6`, `z := 7`;
10704        // `var w int` and `const c = 1` contribute nothing.
10705        check_metrics::<GoParser>(
10706            "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",
10707            "foo.go",
10708            |metric| {
10709                assert_eq!(metric.abc.assignments_sum(), 3);
10710                assert_eq!(metric.abc.branches_sum(), 0);
10711            },
10712        );
10713    }
10714
10715    #[cfg(feature = "go")]
10716    #[test]
10717    fn go_grouped_var_block_counts_each_initialized_spec() {
10718        // A grouped `var ( … )` block is one `var_declaration` holding one
10719        // `var_spec` per line, so matching the spec counts each initialized
10720        // line on its own. A multi-name spec is still one binding
10721        // statement, matching `p, q := 1, 2` (#1278).
10722        // expected: 3 assignments — `a = 1`, `p, q = 1, 2`, and `r := 0`;
10723        // the uninitialized `b int` contributes nothing.
10724        check_metrics::<GoParser>(
10725            "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",
10726            "foo.go",
10727            |metric| {
10728                // The trailing `_ = …` is itself an assignment_statement.
10729                assert_eq!(metric.abc.assignments_sum(), 4);
10730            },
10731        );
10732    }
10733
10734    #[cfg(feature = "go")]
10735    #[test]
10736    fn go_calls_are_branches() {
10737        // Three calls: free function `g()`, method call `r.Inc()`, and
10738        // builtin call `len(s)`. All parse as `call_expression` → B = 3.
10739        // Composite literal `Foo{}` is NOT a call.
10740        check_metrics::<GoParser>(
10741            "package main\n\
10742             type R struct{}\n\
10743             func (r R) Inc() {}\n\
10744             func g() {}\n\
10745             func f(s string) { g(); var r R = R{}; r.Inc(); _ = len(s) }\n",
10746            "foo.go",
10747            |metric| {
10748                assert_eq!(metric.abc.branches_sum(), 3);
10749                insta::assert_json_snapshot!(metric.abc);
10750            },
10751        );
10752    }
10753
10754    #[cfg(feature = "go")]
10755    #[test]
10756    fn go_comparisons_count_conditions() {
10757        // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six
10758        // comparisons → C = 6.
10759        check_metrics::<GoParser>(
10760            "package main\nfunc f(a, b int) bool { return a < b || a > b || a <= b || a >= b || a == b || a != b }\n",
10761            "foo.go",
10762            |metric| {
10763                assert_eq!(metric.abc.conditions_sum(), 6);
10764                insta::assert_json_snapshot!(metric.abc);
10765            },
10766        );
10767    }
10768
10769    #[cfg(feature = "go")]
10770    #[test]
10771    fn go_generic_brackets_not_conditions() {
10772        // Generic instantiation `Min[int](a, b)` puts `int` inside
10773        // `TypeArguments`, not `BinaryExpression`. The parent guard on
10774        // `<` / `>` must not count these. Expected C = 0; B = 1 (one call).
10775        check_metrics::<GoParser>(
10776            "package main\nfunc Min[T int | float64](a, b T) T { return a }\nfunc f() { _ = Min[int](1, 2) }\n",
10777            "foo.go",
10778            |metric| {
10779                assert_eq!(metric.abc.conditions_sum(), 0);
10780                assert_eq!(metric.abc.branches_sum(), 1);
10781                insta::assert_json_snapshot!(metric.abc);
10782            },
10783        );
10784    }
10785
10786    #[cfg(feature = "go")]
10787    #[test]
10788    fn go_switch_arms_count_conditions_default_excluded() {
10789        // Four arms: `case 1:`, `case 2:`, `case 3:`, `default:`. The
10790        // bare `default` is the C/Java `default:` equivalent and is
10791        // excluded — 3 conditions from ExpressionCase. The switch
10792        // expression `x` is bare (no comparison), so no extra
10793        // condition from `==`-style operators.
10794        check_metrics::<GoParser>(
10795            "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",
10796            "foo.go",
10797            |metric| {
10798                assert_eq!(metric.abc.conditions_sum(), 3);
10799                insta::assert_json_snapshot!(metric.abc);
10800            },
10801        );
10802    }
10803
10804    #[cfg(feature = "go")]
10805    #[test]
10806    fn go_type_switch_arms_count_conditions() {
10807        // Type switch: `case int:`, `case string:`, `default:`. Two
10808        // non-default type-case arms → C = 2.
10809        check_metrics::<GoParser>(
10810            "package main\nfunc f(v interface{}) { switch v.(type) { case int: return; case string: return; default: return } }\n",
10811            "foo.go",
10812            |metric| {
10813                assert_eq!(metric.abc.conditions_sum(), 2);
10814                insta::assert_json_snapshot!(metric.abc);
10815            },
10816        );
10817    }
10818
10819    #[cfg(feature = "go")]
10820    #[test]
10821    fn go_select_arms_count_conditions() {
10822        // `select { case <-ch: ...; case ch <- 1: ...; default: ... }`.
10823        // Two non-default communication cases → C = 2.
10824        check_metrics::<GoParser>(
10825            "package main\nfunc f(ch chan int) { select { case <-ch: return; case ch <- 1: return; default: return } }\n",
10826            "foo.go",
10827            |metric| {
10828                assert_eq!(metric.abc.conditions_sum(), 2);
10829                insta::assert_json_snapshot!(metric.abc);
10830            },
10831        );
10832    }
10833
10834    // Anchors the tag shape of a Go fixture before scoring its members:
10835    // `tagged` is how many expression switches carry a `value` field, so
10836    // a fixture edit that turns a tagless switch tagged (or back) fails
10837    // here rather than as a moved score.
10838    #[cfg(feature = "go")]
10839    fn assert_go_switch_members(
10840        src: &str,
10841        tagged: usize,
10842        kinds: &[(u16, usize, &str)],
10843        expected: &[(&str, u64, u64)],
10844    ) {
10845        let parser = GoParser::new(
10846            src.as_bytes().to_vec(),
10847            std::path::Path::new("foo.go"),
10848            None,
10849        );
10850        let found = parser
10851            .root()
10852            .preorder()
10853            .filter(|n| {
10854                n.kind_id() == Go::ExpressionSwitchStatement
10855                    && n.child_by_field_name("value").is_some()
10856            })
10857            .count();
10858        assert_eq!(found, tagged, "tagged switches — the fixture moved");
10859        assert_fixture_spells::<GoParser>(src, "foo.go", kinds);
10860        check_func_space::<GoParser, _>(src, "foo.go", |space| {
10861            assert_members_score(&space, expected);
10862        });
10863    }
10864
10865    // #1523, transferring #1453's Ruby rule. A tagless switch is
10866    // `switch true`: each case expression is an ordinary boolean, so a
10867    // comparison in it is already paid by the token arm, and the case's
10868    // former blanket +1 counted `gt` twice against its `if` twin `iff`.
10869    // Every row now equals the `if` spelling of the same predicate.
10870    //
10871    // `bare`, `paren` and `neg` are the controls: no operator for any
10872    // other arm to count, so the case still pays through the `if` slot
10873    // (1 before the fix and after). `negcmp` peels to a comparison the
10874    // token arm owns. `init1` carries an `initializer` and no tag, and
10875    // `cmt` puts a comment where the tag would go: neither is the
10876    // `value` field, so both stay tagless. `nest2` is a tagless switch
10877    // inside a tagged one — each switch is judged by its own tag.
10878    #[cfg(feature = "go")]
10879    #[test]
10880    fn go_tagless_switch_case_counts_its_condition_once() {
10881        let src = "package main
10882func gt(x int) int { switch { case x > 5: return 1 }; return 0 }
10883func iff(x int) int { if x > 5 { return 1 }; return 0 }
10884func bare(b bool) int { switch { case b: return 1 }; return 0 }
10885func paren(b bool) int { switch { case (b): return 1 }; return 0 }
10886func neg(b bool) int { switch { case !b: return 1 }; return 0 }
10887func negcmp(x int) int { switch { case !(x > 5): return 1 }; return 0 }
10888func andd(a, b bool) int { switch { case a && b: return 1 }; return 0 }
10889func two(x int) int { switch { case x > 1: fallthrough; case x < 0: return 1; default: return 2 } }
10890func init1(s string) int { switch n := len(s); { case n > 1: return 1 }; return 0 }
10891func cmt(x int) int { switch /* no tag */ { case /* c */ x > 5: return 1 }; return 0 }
10892func nest(x, y int) int { switch { case x > 1: switch { case y > 1: return 1 } }; return 0 }
10893func nest2(x, y int) int { switch x { case 1: switch { case y > 1: return 1 } }; return 0 }
10894";
10895        assert_go_switch_members(
10896            src,
10897            1,
10898            &[
10899                (Go::ExpressionCase as u16, 14, "expression cases"),
10900                (Go::DefaultCase as u16, 1, "`two`'s `default`"),
10901                (Go::GT as u16, 9, "`>` comparisons"),
10902                (
10903                    Go::ParenthesizedExpression as u16,
10904                    2,
10905                    "`paren` / `negcmp` parens",
10906                ),
10907                (Go::Comment as u16, 2, "`cmt`'s comments"),
10908            ],
10909            &[
10910                // (member, abc.conditions, cyclomatic)
10911                // Was 2: the `>` and the case, for one decision.
10912                ("gt", 1, 2),
10913                ("iff", 1, 2),
10914                ("bare", 1, 2),
10915                ("paren", 1, 2),
10916                ("neg", 1, 2),
10917                // Was 2.
10918                ("negcmp", 1, 2),
10919                // Was 3: the Rule 9 walker owns both operands.
10920                ("andd", 2, 3),
10921                // Was 4. `default` stays +0 and `fallthrough` adds nothing.
10922                ("two", 2, 3),
10923                // Was 2.
10924                ("init1", 1, 2),
10925                // Was 2. A comparison, not a bare operand, so a misread
10926                // tag (+1 per case on top of the `>`) shows as 2.
10927                ("cmt", 1, 2),
10928                // Was 4.
10929                ("nest", 2, 3),
10930                // Was 3: the tagged outer case keeps its 1.
10931                ("nest2", 2, 3),
10932            ],
10933        );
10934    }
10935
10936    // A pointer dereference or channel receive is a boolean operand the
10937    // way an identifier is: `*p` of a `*bool`, `<-ch` of a `chan bool`.
10938    // No operator arm counts either, so each slot pays for it — the `if`
10939    // slot, a tagless case (which #1523 routed through that slot, so
10940    // `swp` / `swr` fell from 1 to 0 until this), the `!` / paren peel,
10941    // and an `&&` operand. `neg` and `asg` are the controls: `-x`, `*&y`
10942    // and `b := *p` sit in no boolean slot and stay 0 (#1526).
10943    #[cfg(feature = "go")]
10944    #[test]
10945    fn go_deref_and_receive_are_boolean_operands() {
10946        let src = "package main
10947func ifp(p *bool) int { if *p { return 1 }; return 0 }
10948func ifr(ch chan bool) int { if <-ch { return 1 }; return 0 }
10949func swp(p *bool) int { switch { case *p: return 1 }; return 0 }
10950func swr(ch chan bool) int { switch { case <-ch: return 1 }; return 0 }
10951func andp(a bool, p *bool) int { if a && *p { return 1 }; return 0 }
10952func notp(p *bool) int { if !*p { return 1 }; return 0 }
10953func parp(p *bool) int { if (*p) { return 1 }; return 0 }
10954func neg(x int) int { y := -x; z := *&y; return z }
10955func asg(p *bool) bool { b := *p; return b }
10956";
10957        assert_go_switch_members(
10958            src,
10959            0,
10960            &[
10961                (Go::ExpressionCase as u16, 2, "`swp` / `swr` cases"),
10962                (Go::LTDASH as u16, 2, "`ifr` / `swr` receives"),
10963                (Go::ParenthesizedExpression as u16, 1, "`parp`'s parens"),
10964            ],
10965            &[
10966                // (member, abc.conditions, cyclomatic)
10967                ("ifp", 1, 2),
10968                ("ifr", 1, 2),
10969                ("swp", 1, 2),
10970                ("swr", 1, 2),
10971                ("andp", 2, 3),
10972                ("notp", 1, 2),
10973                ("parp", 1, 2),
10974                ("neg", 0, 1),
10975                ("asg", 0, 1),
10976            ],
10977        );
10978    }
10979
10980    // A case listing several expressions is an implicit `||` — Go's spec
10981    // defines a tagless switch as `switch true`, so `case a, b:` holds
10982    // when `a == true || b == true` — and each expression is scored as an
10983    // operand of that chain: once, unless its own arm already counted it.
10984    // `ab` and `mix` therefore read 2, exactly their `if a || b` /
10985    // `if a || x > 1` twins, and `xim` shows the order cannot matter.
10986    // This is #1453's Ruby rule (`when a, b` reads 2) rather than
10987    // Kotlin's first-alternative-only rule, which scores the two orders
10988    // differently.
10989    //
10990    // Parity consequence: cyclomatic scores the case as one decision, so
10991    // these rows sit one above `conditions == cyclomatic - 1`, while the
10992    // `||` twins sit at it — the same trade Ruby's `when a, b` makes.
10993    #[cfg(feature = "go")]
10994    #[test]
10995    fn go_tagless_switch_case_lists_are_an_implicit_or() {
10996        let src = "package main
10997func ab(a, b bool) int { switch { case a, b: return 1 }; return 0 }
10998func ifab(a, b bool) int { if a || b { return 1 }; return 0 }
10999func mix(a bool, x int) int { switch { case a, x > 1: return 1 }; return 0 }
11000func xim(a bool, x int) int { switch { case x > 1, a: return 1 }; return 0 }
11001func ifmix(a bool, x int) int { if a || x > 1 { return 1 }; return 0 }
11002func cmts(a, b bool) int { switch { case a, /* c */ b: return 1 }; return 0 }
11003";
11004        assert_go_switch_members(
11005            src,
11006            0,
11007            &[
11008                (Go::ExpressionCase as u16, 4, "expression cases"),
11009                (Go::PIPEPIPE as u16, 2, "the `||` twins"),
11010                (Go::Comment as u16, 1, "`cmts`'s comment"),
11011            ],
11012            &[
11013                // Was 1: only the case, for two operands.
11014                ("ab", 2, 2),
11015                ("ifab", 2, 3),
11016                ("mix", 2, 2),
11017                ("xim", 2, 2),
11018                ("ifmix", 2, 3),
11019                // Was 1: the comment is no operand.
11020                ("cmts", 2, 2),
11021            ],
11022        );
11023    }
11024
11025    // The controls that must not move. A tagged case lists values
11026    // compared with the tag, a comparison the source never spells, so the
11027    // case itself is the condition — `init2` carries an `initializer`
11028    // beside its tag, and `tagcmp`'s `x > 1` is a value compared with `b`
11029    // (`if b == (x > 1)` also scores 2). Type-switch and `select` cases
11030    // are not expression cases at all.
11031    #[cfg(feature = "go")]
11032    #[test]
11033    fn go_tagged_switch_keeps_the_per_case_count() {
11034        let src = "package main
11035func tag(x int) int { switch x { case 1, 2: return 1 }; return 0 }
11036func init2(s string) int { switch n := len(s); n { case 1: return 1 }; return 0 }
11037func tagcmp(b bool, x int) int { switch b { case x > 1: return 1 }; return 0 }
11038func ts(v any) int { switch v.(type) { case int: return 1 }; return 0 }
11039func sel(c chan int) int { select { case <-c: return 1 }; return 0 }
11040";
11041        assert_go_switch_members(
11042            src,
11043            3,
11044            &[
11045                (Go::ExpressionCase as u16, 3, "expression cases"),
11046                (Go::TypeCase as u16, 1, "`ts`'s type case"),
11047                (
11048                    Go::CommunicationCase as u16,
11049                    1,
11050                    "`sel`'s communication case",
11051                ),
11052            ],
11053            &[
11054                ("tag", 1, 2),
11055                ("init2", 1, 2),
11056                ("tagcmp", 2, 2),
11057                ("ts", 1, 2),
11058                ("sel", 1, 2),
11059            ],
11060        );
11061    }
11062
11063    #[cfg(feature = "go")]
11064    #[test]
11065    fn go_else_counts_as_condition() {
11066        // `if a > b { ... } else { ... }` → `a > b` is one condition,
11067        // `else` is one condition → C = 2.
11068        check_metrics::<GoParser>(
11069            "package main\nfunc f(a, b int) int { if a > b { return a } else { return b } }\n",
11070            "foo.go",
11071            |metric| {
11072                assert_eq!(metric.abc.conditions_sum(), 2);
11073                insta::assert_json_snapshot!(metric.abc);
11074            },
11075        );
11076    }
11077
11078    #[cfg(feature = "go")]
11079    #[test]
11080    fn go_complex_function_abc() {
11081        // Mixed shape, verified by hand:
11082        // - Assignments: `var x = 10` (an initialized declaration, #1278),
11083        //   `_ = x`, `n := 0`, `n = n + 1`, `n += 2`, `n++`,
11084        //   `_ = len(s)` → A = 7. Every `_ = ...` IS counted as an
11085        //   assignment_statement.
11086        // - Branches: `len(s)` → B = 1.
11087        // - Conditions: `n < 10` → 1, `else` → 1, switch arms `case 0:`
11088        //   and `case 1:` (default excluded) → 2 → total C = 4.
11089        check_metrics::<GoParser>(
11090            "package main\nfunc f(s string) int {\n\
11091             \x20   var x = 10\n\
11092             \x20   _ = x\n\
11093             \x20   n := 0\n\
11094             \x20   if n < 10 { n = n + 1 } else { n += 2 }\n\
11095             \x20   n++\n\
11096             \x20   _ = len(s)\n\
11097             \x20   switch n {\n\
11098             \x20   case 0: return 0\n\
11099             \x20   case 1: return 1\n\
11100             \x20   default: return n\n\
11101             \x20   }\n\
11102             }\n",
11103            "foo.go",
11104            |metric| {
11105                assert_eq!(metric.abc.assignments_sum(), 7);
11106                assert_eq!(metric.abc.branches_sum(), 1);
11107                assert_eq!(metric.abc.conditions_sum(), 4);
11108                insta::assert_json_snapshot!(metric.abc);
11109            },
11110        );
11111    }
11112
11113    #[cfg(feature = "go")]
11114    #[test]
11115    fn go_if_multiple_conditions() {
11116        // Fitzpatrick Rule 7 walker fan-out (issue #403). Mirrors
11117        // `rust_if_multiple_conditions`.
11118        check_metrics::<GoParser>(
11119            "package p\n\
11120             func F(a, b, c, d bool) int {\n\
11121             \x20   if a || b || c || d { return 1 }    // +4c\n\
11122             \x20   if a && b && c { return 2 }         // +3c\n\
11123             \x20   if !a && !b { return 3 }            // +2c\n\
11124             \x20   return 0\n\
11125             }\n",
11126            "foo.go",
11127            |metric| {
11128                assert_eq!(metric.abc.conditions_sum(), 9);
11129                insta::assert_json_snapshot!(metric.abc);
11130            },
11131        );
11132    }
11133
11134    #[cfg(feature = "go")]
11135    #[test]
11136    fn go_for_with_conditions() {
11137        // Go has no `while` or `do { … } while(…);` — the `for` loop
11138        // header is the sole condition slot. Each operand of the
11139        // `&&` / `||` chain in the for-condition counts as one
11140        // Fitzpatrick condition.
11141        check_metrics::<GoParser>(
11142            "package p\n\
11143             func F(a, b bool) {\n\
11144             \x20   for a || b { break }       // +2c\n\
11145             \x20   for a && !b { break }      // +2c\n\
11146             }\n",
11147            "foo.go",
11148            |metric| {
11149                assert_eq!(metric.abc.conditions_sum(), 4);
11150                insta::assert_json_snapshot!(metric.abc);
11151            },
11152        );
11153    }
11154
11155    #[cfg(feature = "go")]
11156    #[test]
11157    fn go_for_bare_condition_counts() {
11158        // Regression for findings.md #1: `for true {}` / `for !ready {}`
11159        // are Go's only loop-condition slot. Pre-fix, the Phase-2B
11160        // dispatcher had no `G::ForStatement` arm, so bare-boolean
11161        // and `!`-wrapped `for` conditions silently reported zero.
11162        // `go_count_condition`'s terminal-bool / paren / unary filter
11163        // makes the walker safe across all three for-statement shapes:
11164        // bare condition, `for_clause` (init; cond; post) — whose own
11165        // `condition` field #1276 taught the walker to read — and
11166        // `range_clause`, which has no such field and contributes
11167        // nothing.
11168        check_metrics::<GoParser>(
11169            "package p\n\
11170             func F(ready bool) {\n\
11171             \x20   for true { break }      // +1c\n\
11172             \x20   for !ready { break }    // +1c\n\
11173             \x20   for i := 0; i < 3; i++ { _ = i }    // +1c (the `<`)\n\
11174             }\n",
11175            "foo.go",
11176            |metric| {
11177                // `for true`: walker counts True (+1).
11178                // `for !ready`: walker on unary unwraps to `ready`
11179                //   (+1).
11180                // `for_clause`'s condition is `i < 3`, a
11181                //   `binary_expression` that `go_count_condition`
11182                //   filters out; the `<` itself contributes 1 via the
11183                //   pre-existing LT/GT arm.
11184                // Total: 3.
11185                assert_eq!(metric.abc.conditions_sum(), 3);
11186                insta::assert_json_snapshot!(metric.abc);
11187            },
11188        );
11189    }
11190
11191    // Issue #1276, Go's share. `for_statement`'s child(1) is the
11192    // condition only in the `for cond {}` spelling; the three-clause
11193    // form puts it one level down, in the `for_clause`'s `condition`
11194    // field. Letting the `for_clause` fall through — which the arm's
11195    // own comment used to call harmless — scored a bare three-clause
11196    // condition zero while `for a {}` scored one.
11197    #[cfg(feature = "go")]
11198    #[test]
11199    fn go_three_clause_for_condition_counts() {
11200        // Bare identifier in the three-clause header: no comparison
11201        // token, so only the `for_clause` lookup can count it.
11202        check_metrics::<GoParser>(
11203            "package p\n\
11204             func F(a bool) {\n\
11205             \x20   for i := 0; a; i++ { _ = i }\n\
11206             }\n",
11207            "foo.go",
11208            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
11209        );
11210        // Negation, through `go_inspect_container`'s `!` unwrap.
11211        check_metrics::<GoParser>(
11212            "package p\n\
11213             func F(a bool) {\n\
11214             \x20   for i := 0; !a; i++ { _ = i }\n\
11215             }\n",
11216            "foo.go",
11217            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
11218        );
11219        // Empty condition: the `for_clause` exposes no `condition`
11220        // field, so zero — the same answer as Go's bare `for {}` and
11221        // as every other language since #1276.
11222        check_metrics::<GoParser>(
11223            "package p\n\
11224             func F() {\n\
11225             \x20   for i := 0; ; i++ { break }\n\
11226             }\n",
11227            "foo.go",
11228            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
11229        );
11230        // A `range_clause` carries no condition either, and the walker
11231        // must not mistake the clause itself for one.
11232        check_metrics::<GoParser>(
11233            "package p\n\
11234             func F(xs []int) {\n\
11235             \x20   for _, v := range xs { _ = v }\n\
11236             }\n",
11237            "foo.go",
11238            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
11239        );
11240    }
11241
11242    // Go's `for_statement` is the one grammar here that exposes no
11243    // `condition` field, so its header slot is located structurally and
11244    // has to skip what the field-addressed siblings get for free. Two
11245    // things it must skip, each of which `node.child(1)` got wrong:
11246    // a leading comment (tree-sitter counts comments among a node's
11247    // children — the #1181 failure), and the body of a bare `for {}`,
11248    // which IS child(1) and would otherwise be offered to
11249    // `go_count_condition` as though it were a condition.
11250    #[cfg(feature = "go")]
11251    #[test]
11252    fn go_for_header_slot_skips_comments_and_the_body() {
11253        // Each pair is (source, expected conditions). The commented
11254        // spelling must agree with its bare twin.
11255        let cases = [
11256            ("for a { break }", 1),
11257            ("for /* n */ a { break }", 1),
11258            ("for i := 0; a; i++ { _ = i }", 1),
11259            ("for /* n */ i := 0; a; i++ { _ = i }", 1),
11260            ("for i := 0; /* n */ a; i++ { _ = i }", 1),
11261            // Bare infinite loop: the body is child(1) and must not be
11262            // read as the condition.
11263            ("for { break }", 0),
11264            ("for /* n */ { break }", 0),
11265            ("for _, v := range xs { _ = v }", 0),
11266            ("for /* n */ _, v := range xs { _ = v }", 0),
11267        ];
11268        let mut ran = 0;
11269        for (body, expected) in cases {
11270            let src = format!("package p\nfunc F(a bool, xs []int) {{\n\t{body}\n}}\n");
11271            assert_eq!(abc_conditions(LANG::Go, &src), expected, "`{body}`");
11272            ran += 1;
11273        }
11274        // Non-vacuity, both halves: the loop must actually have run
11275        // every row, and the rows must carry both answers — a walker
11276        // stuck at 0 or at 1 would otherwise pass half the table
11277        // silently.
11278        assert_eq!(ran, cases.len());
11279        assert!(cases.iter().any(|&(_, n)| n == 1));
11280        assert!(cases.iter().any(|&(_, n)| n == 0));
11281    }
11282
11283    #[cfg(feature = "go")]
11284    #[test]
11285    fn go_if_init_statement_condition_counts() {
11286        // Regression for the code-review finding: Go's
11287        // `if x := f(); x { ... }` init-statement form puts the
11288        // short-var declaration at child(1) and the condition at
11289        // child(2). Pre-fix, the dispatcher used child(1) and
11290        // counted zero conditions for this idiomatic Go shape.
11291        // The fix uses `child_by_field_name("condition")` which
11292        // returns the condition regardless of init presence.
11293        check_metrics::<GoParser>(
11294            "package p\nfunc F() { if x := g(); x { } }\n",
11295            "foo.go",
11296            |metric| {
11297                // `x` bare-identifier condition contributes 1
11298                // (Rule 6 — bare boolean identifier in if-condition).
11299                // `g()` call contributes 1 branch but no condition.
11300                assert_eq!(metric.abc.branches_sum(), 1);
11301                assert_eq!(metric.abc.conditions_sum(), 1);
11302                insta::assert_json_snapshot!(metric.abc);
11303            },
11304        );
11305    }
11306
11307    #[cfg(feature = "go")]
11308    #[test]
11309    fn go_if_boolean_literal_condition() {
11310        check_metrics::<GoParser>(
11311            "package p\n\
11312             func F() {\n\
11313             \x20   if true {}                  // +1c\n\
11314             \x20   if !false {}                // +1c\n\
11315             }\n",
11316            "foo.go",
11317            |metric| {
11318                assert_eq!(metric.abc.conditions_sum(), 2);
11319                insta::assert_json_snapshot!(metric.abc);
11320            },
11321        );
11322    }
11323
11324    #[cfg(feature = "go")]
11325    #[test]
11326    fn go_methods_arguments_with_conditions() {
11327        check_metrics::<GoParser>(
11328            "package p\n\
11329             func F(a, b bool) {\n\
11330             \x20   m(a, b)                     // +1b\n\
11331             \x20   m(!a, !b)                   // +1b +2c\n\
11332             }\n",
11333            "foo.go",
11334            |metric| {
11335                assert_eq!(metric.abc.branches_sum(), 2);
11336                assert_eq!(metric.abc.conditions_sum(), 2);
11337                insta::assert_json_snapshot!(metric.abc);
11338            },
11339        );
11340    }
11341
11342    #[cfg(feature = "go")]
11343    #[test]
11344    fn go_return_with_conditions() {
11345        check_metrics::<GoParser>(
11346            "package p\n\
11347             func M1(z int) bool { return !(z >= 0) }\n\
11348             func M2(x bool) bool { return !x }\n\
11349             func M3(x, y bool) bool { return x && y }\n",
11350            "foo.go",
11351            |metric| {
11352                // M1: `>=` (1). `!(z >= 0)` walker on the unary
11353                //     doesn't reach a terminal — stops at the
11354                //     BinaryExpression z>=0 inside the parens. +1.
11355                // M2: walker on `!x` → 1.
11356                // M3: `&&` walker counts both → 2.
11357                // Sum: 1 + 1 + 2 = 4.
11358                assert_eq!(metric.abc.conditions_sum(), 4);
11359                insta::assert_json_snapshot!(metric.abc);
11360            },
11361        );
11362    }
11363
11364    #[cfg(feature = "go")]
11365    #[test]
11366    fn go_short_circuit_with_boolean_literal_operand() {
11367        // `a && true` reports 2 conditions: one identifier, one
11368        // boolean literal. Confirms the terminal set includes
11369        // `True` / `False`.
11370        check_metrics::<GoParser>(
11371            "package p\nfunc F(a bool) bool { return a && true }\n",
11372            "foo.go",
11373            |metric| {
11374                assert_eq!(metric.abc.conditions_sum(), 2);
11375                insta::assert_json_snapshot!(metric.abc);
11376            },
11377        );
11378    }
11379
11380    // ----- Elixir -----
11381
11382    // No top-level Calls and no operators → all three vectors are
11383    // zero. Uses a bare expression rather than a `defmodule` wrapper
11384    // (which would itself be a Call → 1 branch). Confirms the
11385    // ElixirCode Abc trait is wired up and the metric emits.
11386    #[cfg(feature = "elixir")]
11387    #[test]
11388    fn elixir_empty_unit_zero() {
11389        check_metrics::<ElixirParser>(":ok\n", "foo.ex", |metric| {
11390            assert_eq!(metric.abc.assignments_sum(), 0);
11391            assert_eq!(metric.abc.branches_sum(), 0);
11392            assert_eq!(metric.abc.conditions_sum(), 0);
11393            insta::assert_json_snapshot!(metric.abc);
11394        });
11395    }
11396
11397    // An empty `defmodule Foo do ... end` is itself ONE `Call` →
11398    // Documents that module-/function-defining macros (`defmodule`,
11399    // `def`, `defp`, `defmacro`, `defmacrop`) and declarative
11400    // directives (`alias`, `import`, `require`, `use`) are NOT
11401    // runtime dispatch and therefore do NOT inflate `branches`,
11402    // matching Cognitive's treatment.
11403    #[cfg(feature = "elixir")]
11404    #[test]
11405    fn elixir_defmodule_is_zero_branches() {
11406        check_metrics::<ElixirParser>("defmodule Foo do\nend\n", "foo.ex", |metric| {
11407            assert_eq!(metric.abc.branches_sum(), 0);
11408            assert_eq!(metric.abc.assignments_sum(), 0);
11409            assert_eq!(metric.abc.conditions_sum(), 0);
11410            insta::assert_json_snapshot!(metric.abc);
11411        });
11412    }
11413
11414    // Pattern-match `=` counts as an assignment. Two bindings → A = 2.
11415    // `defmodule` and `def` are declarative-Call wrappers and are
11416    // filtered out of branches; the assertion focuses on assignments
11417    // so we only pin that vector.
11418    #[cfg(feature = "elixir")]
11419    #[test]
11420    fn elixir_pattern_match_is_assignment() {
11421        check_metrics::<ElixirParser>(
11422            "defmodule Foo do\n  def f do\n    x = 1\n    y = x + 1\n    y\n  end\nend\n",
11423            "foo.ex",
11424            |metric| {
11425                assert_eq!(metric.abc.assignments_sum(), 2);
11426                insta::assert_json_snapshot!(metric.abc);
11427            },
11428        );
11429    }
11430
11431    // `|>` pipeline operator: each `|>` token contributes one branch.
11432    // Two `|>` ops → +2 from the pipe operator itself. Each pipeline
11433    // step also dispatches a Call (`String.upcase(...)`,
11434    // `String.trim(...)`) — these are wrapped inside the outer
11435    // pipeline Call tree, contributing additional Call branches.
11436    // The headline assertion confirms (a) `|>` is detected and (b)
11437    // pipeline steps are not silently dropped.
11438    #[cfg(feature = "elixir")]
11439    #[test]
11440    fn elixir_pipeline_each_step_is_branch() {
11441        check_metrics::<ElixirParser>(
11442            "defmodule Foo do\n  def normalize(s) do\n    s |> String.trim() |> String.upcase()\n  end\nend\n",
11443            "foo.ex",
11444            |metric| {
11445                // Pipeline yields 2 `|>` branches plus Calls for
11446                // String.trim, String.upcase, and the outer pipeline
11447                // (which surfaces as a Call wrapping the binary
11448                // operator). `def` and `defmodule` are declarative
11449                // and excluded. Empirical total: B = 5.
11450                assert_eq!(metric.abc.branches_sum(), 5);
11451                assert_eq!(metric.abc.assignments_sum(), 0);
11452                insta::assert_json_snapshot!(metric.abc);
11453            },
11454        );
11455    }
11456
11457    // Comparison operators all count as conditions. Six comparisons
11458    // (`==`, `!=`, `<`, `>`, `<=`, `>=`) → C = 6.
11459    #[cfg(feature = "elixir")]
11460    #[test]
11461    fn elixir_comparisons_are_conditions() {
11462        check_metrics::<ElixirParser>(
11463            "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",
11464            "foo.ex",
11465            |metric| {
11466                assert_eq!(metric.abc.conditions_sum(), 6);
11467                insta::assert_json_snapshot!(metric.abc);
11468            },
11469        );
11470    }
11471
11472    // Strict-equality operators `===` / `!==` count as conditions too.
11473    #[cfg(feature = "elixir")]
11474    #[test]
11475    fn elixir_strict_equality_is_condition() {
11476        check_metrics::<ElixirParser>(
11477            "defmodule Foo do\n  def f(a, b) do\n    a === b or a !== b\n  end\nend\n",
11478            "foo.ex",
11479            |metric| {
11480                assert_eq!(metric.abc.conditions_sum(), 2);
11481                insta::assert_json_snapshot!(metric.abc);
11482            },
11483        );
11484    }
11485
11486    // #1461 moved every relational construct a grammar spells as its own
11487    // production onto an unconditional arm — scored by use, Fitzpatrick
11488    // Rule 5 — in C#, Java, Groovy, Kotlin and Ruby. Elixir's membership
11489    // and type tests were the same class and were not in that sweep, so
11490    // `a in b` scored zero where `a == b` scored one. The gap stayed
11491    // invisible while the `when` arm paid a flat one for every guard,
11492    // and surfaced the moment the guard became a slot: an
11493    // operator-spelled guard is owned by its operator's arm, and `in`
11494    // had none.
11495    //
11496    // `bare` / `negbare` are the by-use claim; `slot` is the §5 check
11497    // that a boolean slot does not now score the same operator twice;
11498    // `cmp` is the control the two membership rows are levelled against.
11499    // `in` rides the `<` / `>` `binary_operator` gate rather than
11500    // standing alone — it has the same three grammar positions — and
11501    // `elixir_operator_identifier_is_not_a_condition` pins that gate.
11502    #[cfg(feature = "elixir")]
11503    #[test]
11504    fn elixir_membership_is_a_condition_by_use() {
11505        check_func_space::<ElixirParser, _>(
11506            "defmodule Foo do\n\
11507               def bare(a, b), do: a in b\n\
11508               def negbare(a, b), do: a not in b\n\
11509               def slot(a, b) do\n\
11510                 if a in b do\n\
11511                   IO.puts(\"x\")\n\
11512                 end\n\
11513               end\n\
11514               def cmp(a, b), do: a == b\n\
11515             end\n",
11516            "foo.ex",
11517            |space| {
11518                assert_members_score(
11519                    &space.spaces[0],
11520                    &[
11521                        // Each was 0 before the arm.
11522                        ("bare", 1, 1),
11523                        ("negbare", 1, 1),
11524                        // `in` (1) only — the `if` predicate slot is
11525                        // already paid by the operator (#1527), exactly
11526                        // as `if a == b` scores.
11527                        ("slot", 1, 2),
11528                        ("cmp", 1, 1),
11529                    ],
11530                );
11531            },
11532        );
11533    }
11534
11535    // Guard `when` clause counts as a condition — exactly one, and here
11536    // it is the `>` that supplies it. The guard is a condition *slot*
11537    // (#1454), so an operator-spelled guard scores through the operator
11538    // arm that owns it and the slot itself adds nothing; see
11539    // `elixir_guard_is_a_decision_however_spelled` for the four-spelling
11540    // agreement this is one row of.
11541    //
11542    // Was 2, on a flat `+1` for the `when` token laid on top of the `>`:
11543    // the §5 double count a whole-branch review of the #1454 batch
11544    // found.
11545    #[cfg(feature = "elixir")]
11546    #[test]
11547    fn elixir_guard_when_is_condition() {
11548        check_metrics::<ElixirParser>(
11549            "defmodule Foo do\n  def f(x) when x > 0 do\n    :pos\n  end\nend\n",
11550            "foo.ex",
11551            |metric| {
11552                // the guard slot (+0, the guard is an operator
11553                // application) + `>` (+1) = 1
11554                assert_eq!(metric.abc.conditions_sum(), 1);
11555                insta::assert_json_snapshot!(metric.abc);
11556            },
11557        );
11558    }
11559
11560    // A `case` pays one condition per clause (#1531) AND one branch for
11561    // the Call itself. The `_ ->` catch-all is the default arm and pays
11562    // nothing, so this two-clause `case` scores 1; the per-clause rule
11563    // is pinned by `elixir_clause_constructs_pay_per_clause`.
11564    #[cfg(feature = "elixir")]
11565    #[test]
11566    fn elixir_case_is_condition_and_branch() {
11567        check_metrics::<ElixirParser>(
11568            "defmodule Foo do\n  def f(x) do\n    case x do\n      1 -> :one\n      _ -> :other\n    end\n  end\nend\n",
11569            "foo.ex",
11570            |metric| {
11571                // conditions: `1 ->` (+1) + `_ ->` default (+0) = 1
11572                assert_eq!(metric.abc.conditions_sum(), 1);
11573                insta::assert_json_snapshot!(metric.abc);
11574            },
11575        );
11576    }
11577
11578    // `cond` scores per clause, as its nested-`if` twin does (#1527): the
11579    // `x > 0` clause is a predicate slot its `>` already pays, and the
11580    // `true ->` catch-all is the default arm and pays nothing.
11581    #[cfg(feature = "elixir")]
11582    #[test]
11583    fn elixir_cond_is_condition() {
11584        check_metrics::<ElixirParser>(
11585            "defmodule Foo do\n  def f(x) do\n    cond do\n      x > 0 -> :pos\n      true -> :other\n    end\n  end\nend\n",
11586            "foo.ex",
11587            |metric| {
11588                // conditions: `x > 0` clause slot (+0) + `>` (+1) +
11589                // `true ->` default (+0) = 1, against cyclomatic 2
11590                assert_eq!(metric.abc.conditions_sum(), 1);
11591                insta::assert_json_snapshot!(metric.abc);
11592            },
11593        );
11594    }
11595
11596    // An `if` / `unless` predicate and each `cond` clause are condition
11597    // slots (#1527): one condition, unless an operator in the slot
11598    // already paid. Before, the keyword Call paid a flat one on top of
11599    // its predicate, so `if x > 5` scored 2 and `cond` paid once per
11600    // construct whatever its clause count.
11601    //
11602    // Expected values come from the twins, not the rule: each `if` row
11603    // is what Ruby and Java score for the same `if` (`if x > 5` 1,
11604    // `if b` 1, `if a && b` 2 — measured), every non-chain row sits at
11605    // `cyclomatic - 1`, and `cond3` equals Go's tagless
11606    // `switch { case x > 5: … case b: … default: … }` (2), whose
11607    // `default:` is as free as `cond`'s `true ->`. Its nested-`if` twin
11608    // `nested` pays Fitzpatrick Rule 5 for each `else` (#1531), as
11609    // Ruby's `if … elsif … else` and Java's `if … else if … else` do (4
11610    // against cyclomatic 3 — measured). The chain rows sit one under
11611    // parity exactly as Ruby's and Java's `if a && b` (2 / 3) do.
11612    //
11613    // Paths only one row exercises (§11): `cmp_comment` the comment
11614    // skip in a parenthesised peel, `assign` the match-`=` peel,
11615    // `negcmp` the `not` peel, `arith` a non-comparison operator the
11616    // slot pays for, `capture` a chain operand that no terminal list
11617    // held (`&f/1` scored one short of `a && b` on main), and the
11618    // `true ->` rows the default-arm gate shared with cyclomatic.
11619    #[cfg(feature = "elixir")]
11620    #[test]
11621    fn elixir_if_and_cond_predicates_are_slots() {
11622        check_func_space::<ElixirParser, _>(
11623            "defmodule Foo do
11624  def cmp(x), do: (if x > 5 do 1 end)
11625  def bare(b), do: (if b do 1 end)
11626  def kw(x), do: if(x > 5, do: 1)
11627  def unless_cmp(x), do: (unless x > 5 do 1 end)
11628  def cmp_comment(x) do
11629    if (x > 5 # c
11630    ) do
11631      1
11632    end
11633  end
11634  def assign(x), do: (if (y = x > 5) do y end)
11635  def negcmp(x), do: (if not(x > 5) do 1 end)
11636  def arith(x), do: (if x + 1 do 1 end)
11637  def chain(a, b), do: (if a && b do 1 end)
11638  def capture(a), do: (if a && &f/1 do 1 end)
11639  def cond_cmp(x) do
11640    cond do
11641      x > 5 -> 1
11642      true -> 2
11643    end
11644  end
11645  def cond_bare(b) do
11646    cond do
11647      b -> 1
11648      true -> 2
11649    end
11650  end
11651  def cond3(x, b) do
11652    cond do
11653      x > 5 -> 1
11654      b -> 2
11655      true -> 3
11656    end
11657  end
11658  def nested(x, b) do
11659    if x > 5 do
11660      1
11661    else
11662      if b do 2 else 3 end
11663    end
11664  end
11665  def cond_no_default(a, b, c) do
11666    cond do
11667      a -> 1
11668      b -> 2
11669      c -> 3
11670    end
11671  end
11672end
11673",
11674            "foo.ex",
11675            |space| {
11676                assert_members_score(
11677                    &space.spaces[0],
11678                    &[
11679                        ("cmp", 1, 2),
11680                        ("bare", 1, 2),
11681                        ("kw", 1, 2),
11682                        ("unless_cmp", 1, 2),
11683                        ("cmp_comment", 1, 2),
11684                        ("assign", 1, 2),
11685                        ("negcmp", 1, 2),
11686                        ("arith", 1, 2),
11687                        ("chain", 2, 3),
11688                        ("capture", 2, 3),
11689                        ("cond_cmp", 1, 2),
11690                        ("cond_bare", 1, 2),
11691                        ("cond3", 2, 3),
11692                        ("nested", 4, 3),
11693                        ("cond_no_default", 3, 4),
11694                    ],
11695                );
11696            },
11697        );
11698    }
11699
11700    // Every clause construct pays one condition per clause (#1531), as
11701    // `Cyclomatic` counts one decision per clause through the same gate.
11702    // `case` and `with` paid once per construct and the others not at
11703    // all, so a two-clause `case` scored 1 against Ruby's
11704    // `case x; when 1; when 2; end`, Go's `switch` and Rust's `match`
11705    // at 2 (measured).
11706    //
11707    // Expected values come from cyclomatic, which this fix did not move:
11708    // every row sits at `cyclomatic - 1`. Paths only one row exercises
11709    // (§11): `case_default` the free `_ ->`, `case_guard` a guarded
11710    // clause's flat one beside its `>`, `rescue_in` a sole pattern that
11711    // scores itself (the `in` arm), `rescue_alias` a sole pattern the
11712    // clause slot pays for, `catch_two` a multi-pattern clause, `recv` a
11713    // `receive`'s `after` timeout clause, and `with_else` the `else` of
11714    // a `with`, whose clauses pay rather than a Rule 5 `else`. Its `<-`
11715    // clause pays one more since #1535, in both metrics.
11716    #[cfg(feature = "elixir")]
11717    #[test]
11718    fn elixir_clause_constructs_pay_per_clause() {
11719        check_func_space::<ElixirParser, _>(
11720            "defmodule Foo do
11721  def case2(x) do
11722    case x do
11723      1 -> :a
11724      2 -> :b
11725    end
11726  end
11727  def case_default(x) do
11728    case x do
11729      1 -> :a
11730      2 -> :b
11731      _ -> :c
11732    end
11733  end
11734  def case_guard(x) do
11735    case x do
11736      y when y > 0 -> :a
11737      _ -> :b
11738    end
11739  end
11740  def with_else(x) do
11741    with {:ok, a} <- g(x) do
11742      a
11743    else
11744      {:error, e} -> e
11745      _ -> nil
11746    end
11747  end
11748  def recv(x) do
11749    receive do
11750      {:a, y} -> y
11751      {:b, z} -> z
11752    after
11753      100 -> x
11754    end
11755  end
11756  def rescue_in() do
11757    try do
11758      g()
11759    rescue
11760      e in RuntimeError -> e
11761    end
11762  end
11763  def rescue_alias() do
11764    try do
11765      g()
11766    rescue
11767      RuntimeError -> 1
11768    end
11769  end
11770  def catch_two() do
11771    try do
11772      g()
11773    catch
11774      :exit, r -> r
11775    end
11776  end
11777end
11778",
11779            "foo.ex",
11780            |space| {
11781                assert_members_score(
11782                    &space.spaces[0],
11783                    &[
11784                        ("case2", 2, 3),
11785                        ("case_default", 2, 3),
11786                        ("case_guard", 2, 3),
11787                        ("with_else", 2, 3),
11788                        ("recv", 3, 4),
11789                        ("rescue_in", 1, 2),
11790                        ("rescue_alias", 1, 2),
11791                        ("catch_two", 1, 2),
11792                    ],
11793                );
11794            },
11795        );
11796    }
11797
11798    // A multi-clause anonymous fn dispatches like a `case`, less its head
11799    // clause, which is the closure's definition (#776): `fn x -> x end`
11800    // has no decision and `fn 0 -> :a; _ -> :b end` one — the trailing
11801    // `_ ->` included, since the head already took the free path.
11802    // Expected: each total equals the closure's cyclomatic decisions.
11803    #[cfg(feature = "elixir")]
11804    #[test]
11805    fn elixir_multi_clause_fn_pays_after_its_head() {
11806        for (source, conditions) in [
11807            ("f = fn x -> x end\n", 0),
11808            ("f = fn 0 -> :a; _ -> :b end\n", 1),
11809            ("f = fn 0 -> :a; 1 -> :b; _ -> :c end\n", 2),
11810        ] {
11811            let metrics =
11812                metrics_verbatim(LANG::Elixir, source.as_bytes(), MetricsOptions::default());
11813            assert_eq!(metrics.abc.conditions_sum(), conditions, "{source}");
11814        }
11815    }
11816
11817    // A typespec's function type (`(any -> any)`) is a `stab_clause` too,
11818    // under a parenthesised `block`, and is type syntax with no decision
11819    // behind it. Cyclomatic counted each one; the shared clause gate
11820    // would have carried that into ABC (#1531). Expected: nothing but the
11821    // unit's base path, as for the typespec `when` in
11822    // `elixir_typespec_when_is_not_a_guard`.
11823    #[cfg(feature = "elixir")]
11824    #[test]
11825    fn elixir_typespec_function_type_is_not_a_clause() {
11826        let metrics = metrics_verbatim(
11827            LANG::Elixir,
11828            b"@spec f(list, (any -> any)) :: list\n@type cb :: (integer, integer -> boolean)\n",
11829            MetricsOptions::default(),
11830        );
11831        assert_eq!(metrics.abc.conditions_sum(), 0);
11832        assert_eq!(metrics.cyclomatic.cyclomatic_sum(), 1);
11833    }
11834
11835    // The keyword form of each clause construct holds its clauses in a
11836    // parenthesised `block` under a `do:` / `else:` / `rescue:` pair, the
11837    // shape a typespec function type also has. Excluding `block` for the
11838    // typespec dropped every keyword-form clause (`ck` read 1/4, and `rk`
11839    // 0/3); each `*k` row must equal its `*b` block-form twin, `dk`'s
11840    // `true ->` included (it was a decision in the keyword form).
11841    #[cfg(feature = "elixir")]
11842    #[test]
11843    fn elixir_keyword_form_clauses_match_block_form() {
11844        let src = "defmodule M do
11845  def ck(x), do: case(x, do: (1 -> :a; 2 -> :b; 3 -> :c))
11846  def cb(x) do
11847    case x do
11848      1 -> :a
11849      2 -> :b
11850      3 -> :c
11851    end
11852  end
11853  def dk(x), do: cond(do: (x > 1 -> :a; x < 0 -> :b; true -> :c))
11854  def db(x) do
11855    cond do
11856      x > 1 -> :a
11857      x < 0 -> :b
11858      true -> :c
11859    end
11860  end
11861  def rk(), do: receive(do: ({:a, y} -> y; {:b, z} -> z))
11862  def rb() do
11863    receive do
11864      {:a, y} -> y
11865      {:b, z} -> z
11866    end
11867  end
11868  def tk(), do: try(do: g(), rescue: (e in RuntimeError -> e; _ -> 2))
11869  def tb() do
11870    try do
11871      g()
11872    rescue
11873      e in RuntimeError -> e
11874      _ -> 2
11875    end
11876  end
11877  def wk(x), do: with({:ok, a} <- x, do: a, else: ({:error, e} -> e; _ -> 0))
11878  def wb(x) do
11879    with {:ok, a} <- x do
11880      a
11881    else
11882      {:error, e} -> e
11883      _ -> 0
11884    end
11885  end
11886end
11887";
11888        assert_fixture_spells::<ElixirParser>(
11889            src,
11890            "foo.ex",
11891            &[
11892                (Elixir::Block as u16, 5, "the five keyword-form sections"),
11893                (Elixir::StabClause as u16, 24, "clauses, 12 per spelling"),
11894            ],
11895        );
11896        check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
11897            assert_members_score(
11898                &space.spaces[0],
11899                &[
11900                    ("ck", 3, 4),
11901                    ("cb", 3, 4),
11902                    ("dk", 2, 3),
11903                    ("db", 2, 3),
11904                    ("rk", 2, 3),
11905                    ("rb", 2, 3),
11906                    ("tk", 1, 2),
11907                    ("tb", 1, 2),
11908                    ("wk", 2, 3),
11909                    ("wb", 2, 3),
11910                ],
11911            );
11912        });
11913    }
11914
11915    // Fitzpatrick Rule 5 counts the `else` of an `if` / `unless` (#1531),
11916    // in both of Elixir's spellings. Expected values are the twins':
11917    // Ruby's `if b … else … end`, Java's `if (b) {} else {}`, Python's
11918    // and Go's each score 2 against cyclomatic 2 (measured), one over
11919    // parity by the documented `else` arm. `kw_other` is the control
11920    // that the `else:` key is read only on an `if` / `unless`: on any
11921    // other call it is an ordinary keyword argument.
11922    #[cfg(feature = "elixir")]
11923    #[test]
11924    fn elixir_if_else_pays_rule_5() {
11925        check_func_space::<ElixirParser, _>(
11926            "defmodule Foo do
11927  def block(b) do
11928    if b do
11929      1
11930    else
11931      2
11932    end
11933  end
11934  def kw(b), do: if(b, do: 1, else: 2)
11935  def unless_block(b) do
11936    unless b do
11937      1
11938    else
11939      2
11940    end
11941  end
11942  def cmp(x) do
11943    if x > 5 do
11944      1
11945    else
11946      2
11947    end
11948  end
11949  def no_else(b) do
11950    if b do
11951      1
11952    end
11953  end
11954  def kw_no_else(b), do: if(b, do: 1)
11955  def kw_other(b), do: g(b, else: 1)
11956end
11957",
11958            "foo.ex",
11959            |space| {
11960                assert_members_score(
11961                    &space.spaces[0],
11962                    &[
11963                        ("block", 2, 2),
11964                        ("kw", 2, 2),
11965                        ("unless_block", 2, 2),
11966                        ("cmp", 2, 2),
11967                        ("no_else", 1, 2),
11968                        ("kw_no_else", 1, 2),
11969                        ("kw_other", 0, 1),
11970                    ],
11971                );
11972            },
11973        );
11974    }
11975
11976    // `for` is a comprehension/loop, NOT in the issue's condition
11977    // list. It is still a Call so it contributes one branch, but no
11978    // condition.
11979    #[cfg(feature = "elixir")]
11980    #[test]
11981    fn elixir_for_is_branch_not_condition() {
11982        check_metrics::<ElixirParser>(
11983            "defmodule Foo do\n  def f(xs) do\n    for x <- xs, do: x * 2\n  end\nend\n",
11984            "foo.ex",
11985            |metric| {
11986                assert_eq!(metric.abc.conditions_sum(), 0);
11987                insta::assert_json_snapshot!(metric.abc);
11988            },
11989        );
11990    }
11991
11992    // Mixed shape, verified by hand: defmodule Call + def Call + if Call
11993    // + Call to side_effect/0 + assignment `x = 1` + comparison `x > 0`.
11994    // - Assignments: `x = 1` → A = 1.
11995    // - Branches: `defmodule` and `def` are declarative and excluded;
11996    //   `if` Call + `side_effect()` Call → 2 Calls, plus 0 `|>` → B = 2.
11997    // - Conditions: `x > 0` → 1; the `if` predicate slot it fills adds
11998    //   nothing more (#1527) → C = 1.
11999    #[cfg(feature = "elixir")]
12000    #[test]
12001    fn elixir_mixed_abc() {
12002        check_metrics::<ElixirParser>(
12003            "defmodule Foo do\n  def f do\n    x = 1\n    if x > 0 do\n      side_effect()\n    end\n  end\nend\n",
12004            "foo.ex",
12005            |metric| {
12006                assert_eq!(metric.abc.assignments_sum(), 1);
12007                assert_eq!(metric.abc.branches_sum(), 2);
12008                assert_eq!(metric.abc.conditions_sum(), 1);
12009                insta::assert_json_snapshot!(metric.abc);
12010            },
12011        );
12012    }
12013
12014    #[cfg(feature = "elixir")]
12015    #[test]
12016    fn elixir_unary_conditions_in_chain() {
12017        // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
12018        // `&&` / `||` chain is one condition. For `if a && b || c` the
12019        // walker adds a, b, c → 3, and the `if` predicate slot adds
12020        // nothing because the chain already scored it (#1527). expected:
12021        // 3, as Ruby and Java score the same `if`, against cyclomatic 4
12022        // (base 1 + if + && + ||).
12023        check_metrics::<ElixirParser>(
12024            "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",
12025            "foo.ex",
12026            |metric| {
12027                assert_eq!(metric.abc.conditions_sum(), 3);
12028            },
12029        );
12030    }
12031
12032    // Elixir spells negation two ways and the operand peel once
12033    // recognised only `!`, so `a && not b` scored 3 where `a && !b`
12034    // scored 4 — the `not` operand reached no terminal and vanished.
12035    // Both forms now read the same. `not` is the stricter of the two (it
12036    // raises on a non-boolean operand where `!` accepts any truthy
12037    // value), so it is at least as good a proof that what it wraps is
12038    // boolean, which is all the walker's flag claims.
12039    //
12040    // Both members in one fixture because the claim is that they agree:
12041    // asserting either alone says nothing about the pair (§11), and a
12042    // shared `check_metrics` total could not tell 3 + 4 from 4 + 3.
12043    //
12044    // `assert_members_score` rather than the parity-asserting sibling:
12045    // each member scores its two `&&` operands and nothing for the `if`
12046    // slot the chain fills (#1527) — 2 against cyclomatic
12047    // `base 1 + if + &&` = 3, as Ruby's `if a && !b` does.
12048    #[cfg(feature = "elixir")]
12049    #[test]
12050    fn elixir_keyword_not_negates_like_bang() {
12051        check_func_space::<ElixirParser, _>(
12052            "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",
12053            "foo.ex",
12054            |space| {
12055                assert_members_score(&space.spaces[0], &[("kw", 2, 3), ("bang", 2, 3)]);
12056            },
12057        );
12058    }
12059
12060    #[cfg(feature = "elixir")]
12061    #[test]
12062    fn elixir_comparison_operands_add_nothing() {
12063        // Isolation check: comparison operands of a `&&` chain are nested
12064        // `binary_operator` nodes, not bare boolean leaves, so the walker
12065        // adds nothing. expected: 2 = `>` (1) + `>` (1); the `&&` walker
12066        // and the `if` slot the chain fills contribute 0 (#1527).
12067        check_metrics::<ElixirParser>(
12068            "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",
12069            "foo.ex",
12070            |metric| {
12071                assert_eq!(metric.abc.conditions_sum(), 2);
12072            },
12073        );
12074    }
12075
12076    #[cfg(feature = "elixir")]
12077    #[test]
12078    fn elixir_keyword_and_or_chain_counts_operands() {
12079        // The keyword forms `and` / `or` get the same Rule 9 treatment as
12080        // `&&` / `||`. expected: 3 = operands a, b, c; the `if` slot the
12081        // chain fills adds nothing (#1527).
12082        check_metrics::<ElixirParser>(
12083            "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",
12084            "foo.ex",
12085            |metric| {
12086                assert_eq!(metric.abc.conditions_sum(), 3);
12087            },
12088        );
12089    }
12090
12091    // Sigil delimiter choice must not move ABC: `~s<hi>` and `~s(hi)`
12092    // are the same value spelled differently, but the `<` / `>`
12093    // delimiter tokens carry the comparison kind ids, so the unguarded
12094    // condition arm scored `~s<hi>` as 2 conditions and `~s(hi)` as 0.
12095    // The parent-is-`Sigil` guard (mirroring the Halstead getter's,
12096    // #1256) suppresses the delimiter case. expected, for each
12097    // spelling: A = 1 (the `x =` pattern match), B = 0 (a bare sigil
12098    // is not a `Call` node — verified by AST dump: `binary_operator`
12099    // wrapping `identifier`, `=`, `sigil`), C = 0 (no comparison, no
12100    // guard, no keyword Call).
12101    #[cfg(feature = "elixir")]
12102    #[test]
12103    fn elixir_sigil_delimiter_choice_is_abc_invariant() {
12104        for src in ["x = ~s<hi>\n", "x = ~s(hi)\n"] {
12105            check_metrics::<ElixirParser>(src, "foo.ex", |metric| {
12106                assert_eq!(metric.abc.assignments_sum(), 1);
12107                assert_eq!(metric.abc.branches_sum(), 0);
12108                assert_eq!(metric.abc.conditions_sum(), 0);
12109            });
12110        }
12111    }
12112
12113    // Control for the guard above: `<` *outside* a sigil is a genuine
12114    // comparison and must keep counting even with a `<`-delimited
12115    // sigil in the same unit. expected: A = 2 (`x =`, `y =`), C = 1
12116    // (only `a < b`; the sigil's `<` / `>` delimiters are guarded).
12117    #[cfg(feature = "elixir")]
12118    #[test]
12119    fn elixir_lt_comparison_still_counts_beside_sigil() {
12120        check_metrics::<ElixirParser>("x = ~s<hi>\ny = a < b\n", "foo.ex", |metric| {
12121            assert_eq!(metric.abc.assignments_sum(), 2);
12122            assert_eq!(metric.abc.conditions_sum(), 1);
12123        });
12124    }
12125
12126    // #1297's sweep converted six grammars from a denylist of
12127    // non-comparison `<` / `>` parents to an allowlist and recorded
12128    // Elixir as already covered by its sigil guard. It was not: an
12129    // operator *named* rather than applied — the capture `&</2` and the
12130    // qualified call `Kernel.<(a, b)` — puts the bare token under an
12131    // `operator_identifier`, which no denylist entry mentioned, so each
12132    // scored a condition against zero decisions. Same shape as the C#
12133    // `operator <` declaration the sweep did fix.
12134    //
12135    // Each function is its own space and its own row, so a fix that
12136    // reached one spelling and not the other fails here. `max/2` is the
12137    // control: a qualified call whose name is not an operator was
12138    // always 0, so a guard that merely stopped counting `<` everywhere
12139    // would pass this row and fail the two below it.
12140    //
12141    // The six equality and ordering tokens share the gate (#1531): they
12142    // sat on an ungated arm, so `&==/2` and `Kernel.==(a, b)` scored a
12143    // condition each and `caps` five. `eq` is their applied control.
12144    #[cfg(feature = "elixir")]
12145    #[test]
12146    fn elixir_operator_identifier_is_not_a_condition() {
12147        check_func_space::<ElixirParser, _>(
12148            "defmodule M do\n\
12149               def asc(l), do: Enum.sort(l, &</2)\n\
12150               def lt(a, b), do: Kernel.<(a, b)\n\
12151               def big(a, b), do: Kernel.max(a, b)\n\
12152               def cmp(a, b), do: a < b\n\
12153               def eq_cap(l), do: Enum.sort(l, &==/2)\n\
12154               def eq_remote(a, b), do: Kernel.==(a, b)\n\
12155               def caps(), do: {&===/2, &!=/2, &!==/2, &<=/2, &>=/2}\n\
12156               def eq(a, b), do: a == b\n\
12157             end\n",
12158            "foo.ex",
12159            |space| {
12160                let module = &space.spaces[0];
12161                let scored: Vec<(String, u64)> = module
12162                    .spaces
12163                    .iter()
12164                    .map(|f| {
12165                        (
12166                            f.name.clone().unwrap_or_default(),
12167                            f.metrics.abc.conditions(),
12168                        )
12169                    })
12170                    .collect();
12171                assert_eq!(
12172                    scored,
12173                    vec![
12174                        ("asc".to_owned(), 0),
12175                        ("lt".to_owned(), 0),
12176                        ("big".to_owned(), 0),
12177                        ("cmp".to_owned(), 1),
12178                        ("eq_cap".to_owned(), 0),
12179                        ("eq_remote".to_owned(), 0),
12180                        ("caps".to_owned(), 0),
12181                        ("eq".to_owned(), 1),
12182                    ],
12183                );
12184            },
12185        );
12186    }
12187
12188    // ----- C++ -----
12189
12190    #[cfg(feature = "cpp")]
12191    #[test]
12192    fn cpp_empty_unit_zero() {
12193        // No code → A=B=C=0. Wires up the trait and exercises the
12194        // per-language compute reachability.
12195        check_metrics::<CppParser>("", "empty.cpp", |metric| {
12196            assert_eq!(metric.abc.assignments_sum(), 0);
12197            assert_eq!(metric.abc.branches_sum(), 0);
12198            assert_eq!(metric.abc.conditions_sum(), 0);
12199            insta::assert_json_snapshot!(metric.abc);
12200        });
12201    }
12202
12203    #[cfg(feature = "cpp")]
12204    #[test]
12205    fn cpp_plain_and_compound_assignments_count() {
12206        // `int x = 0` is an `init_declarator` carrying an `=` token
12207        // and counts as 1 (post-#393: the literal Fitzpatrick rule
12208        // counts every `=` operator, matching the JS impl's
12209        // `let x = 5` treatment). `x = 5`, `x += 2`, `x = 7` all
12210        // parse as `assignment_expression` → 3. Total A = 4.
12211        check_metrics::<CppParser>(
12212            "void f() { int x = 0; x = 5; x += 2; x = 7; }",
12213            "foo.cpp",
12214            |metric| {
12215                assert_eq!(metric.abc.assignments_sum(), 4);
12216                assert_eq!(metric.abc.branches_sum(), 0);
12217                assert_eq!(metric.abc.conditions_sum(), 0);
12218                insta::assert_json_snapshot!(metric.abc);
12219            },
12220        );
12221    }
12222
12223    #[cfg(feature = "cpp")]
12224    #[test]
12225    fn cpp_increment_and_decrement_count_as_assignment() {
12226        // `x++` / `--x` / prefix and postfix forms each parse as
12227        // `update_expression` and count as 1 assignment per
12228        // Fitzpatrick — 4. `int x = 0` (init_declarator with `=`)
12229        // adds 1 (post-#393). Total A = 5.
12230        check_metrics::<CppParser>(
12231            "void f() { int x = 0; x++; --x; ++x; x--; }",
12232            "foo.cpp",
12233            |metric| {
12234                assert_eq!(metric.abc.assignments_sum(), 5);
12235                insta::assert_json_snapshot!(metric.abc);
12236            },
12237        );
12238    }
12239
12240    #[cfg(feature = "cpp")]
12241    #[test]
12242    fn cpp_init_declarators_count_as_assignments() {
12243        // Issue #393 regression: `int a=1;`, `int b=2;`, `int c=a+b;`,
12244        // `int d=0;` are all `init_declarator` nodes with `=` → 4
12245        // assignments. `d=5;` is one plain `assignment_expression`,
12246        // `d+=1;` is one compound. Total A = 4 + 1 + 1 = 6.
12247        check_metrics::<CppParser>(
12248            "void f() { int a=1; int b=2; int c=a+b; int d=0; d=5; d+=1; }",
12249            "foo.cpp",
12250            |metric| {
12251                assert_eq!(metric.abc.assignments_sum(), 6);
12252                insta::assert_json_snapshot!(metric.abc);
12253            },
12254        );
12255    }
12256
12257    #[cfg(feature = "cpp")]
12258    #[test]
12259    fn cpp_declaration_without_initializer_does_not_count() {
12260        // `int a;` parses as a plain declarator inside `declaration`,
12261        // NOT an `init_declarator` (the latter only appears when an
12262        // initializer is present). Regression test for issue #393:
12263        // un-initialised declarations contribute zero to A.
12264        check_metrics::<CppParser>("void f() { int a; a = 5; }", "foo.cpp", |metric| {
12265            // Only `a = 5` (assignment_expression) → A = 1.
12266            assert_eq!(metric.abc.assignments_sum(), 1);
12267            insta::assert_json_snapshot!(metric.abc);
12268        });
12269    }
12270
12271    #[cfg(feature = "cpp")]
12272    #[test]
12273    fn cpp_init_declarator_brace_paren_init_does_not_count() {
12274        // `init_declarator` has two grammar forms: `declarator = value`
12275        // (the `=` form) and `declarator argument_list_or_initializer_list`
12276        // (the `int x(5);` / `int x{5};` direct-init forms). Only the
12277        // first form contains an `=` token, so only it should count.
12278        // Regression test pinning that distinction so that
12279        // refactorings of the init_declarator arm don't accidentally
12280        // start counting direct-init too.
12281        check_metrics::<CppParser>(
12282            "void f() { int x(5); int y{7}; x = 1; }",
12283            "foo.cpp",
12284            |metric| {
12285                // Only `x = 1` (assignment_expression) → A = 1.
12286                assert_eq!(metric.abc.assignments_sum(), 1);
12287                insta::assert_json_snapshot!(metric.abc);
12288            },
12289        );
12290    }
12291
12292    #[cfg(feature = "cpp")]
12293    #[test]
12294    fn cpp_calls_are_branches() {
12295        // Free call + member-fn call (parses as `call_expression` with
12296        // a `field_expression` callee) + `new` allocation. All three
12297        // are branches → B = 3. `auto* p = new int(5)` is also an
12298        // `init_declarator` with `=` so it contributes one assignment
12299        // (post-#393); the snapshot pins that magnitude.
12300        check_metrics::<CppParser>(
12301            "struct S { void m(); }; void g(); void f() { g(); S s; s.m(); auto* p = new int(5); }",
12302            "foo.cpp",
12303            |metric| {
12304                assert_eq!(metric.abc.branches_sum(), 3);
12305                assert_eq!(metric.abc.assignments_sum(), 1);
12306                insta::assert_json_snapshot!(metric.abc);
12307            },
12308        );
12309    }
12310
12311    #[cfg(feature = "cpp")]
12312    #[test]
12313    fn cpp_comparisons_count_conditions() {
12314        // `<`, `>`, `<=`, `>=`, `==`, `!=`, and the C++20 spaceship
12315        // `<=>` each contribute one condition. The `||` short-
12316        // circuits add 0 (Fitzpatrick Rule 5, issue #395). Six
12317        // comparisons in the `||` chain plus `<=>` (1) plus the
12318        // outer `== 0` (1) → C = 8.
12319        check_metrics::<CppParser>(
12320            "#include <compare>\n\
12321             bool f(int a, int b) {\n\
12322                 return a < b || a > b || a <= b || a >= b || a == b || a != b || (a <=> b) == 0;\n\
12323             }\n",
12324            "foo.cpp",
12325            |metric| {
12326                // `<`, `>`, `<=`, `>=`, `==`, `!=` → 6 comparisons
12327                // from the chained `||` expression. `(a <=> b) == 0`
12328                // adds the spaceship `<=>` (1) + the outer `== 0`
12329                // (1) → 8 total. The six `||` short-circuits add 0
12330                // (Fitzpatrick Rule 5; issue #395).
12331                assert_eq!(metric.abc.conditions_sum(), 8);
12332                insta::assert_json_snapshot!(metric.abc);
12333            },
12334        );
12335    }
12336
12337    #[cfg(feature = "cpp")]
12338    #[test]
12339    fn cpp_short_circuit_ops_not_counted_directly() {
12340        // `&&` and `||` do NOT count on their own (see the
12341        // module-level `Stats` doc-comment; #395). Phase-2 walker
12342        // counts each operand of a logical chain once (#403), but
12343        // when every operand is itself a relational expression
12344        // (`a == b`, `a > 0`, `b < 0`) the walker doesn't add
12345        // anything on top of the existing comparison-token tally
12346        // — a relational sub-expression scores itself
12347        // (`cpp_condition_scores_itself`), so its chain slot pays
12348        // nothing on top.
12349        check_metrics::<CppParser>(
12350            "bool f(int a, int b) { return a == b && a > 0 || b < 0; }",
12351            "foo.cpp",
12352            |metric| {
12353                // == 1, > 1, < 1; the walker on && and || finds
12354                // BinaryExpression operands (not terminal-bool) and
12355                // adds nothing. Total: 3.
12356                assert_eq!(metric.abc.conditions_sum(), 3);
12357                insta::assert_json_snapshot!(metric.abc);
12358            },
12359        );
12360    }
12361
12362    #[cfg(feature = "cpp")]
12363    #[test]
12364    fn cpp_generic_brackets_not_conditions() {
12365        // `<` / `>` in `std::vector<int>` are `template_argument_list`
12366        // delimiters, NOT comparison operators. The `binary_expression`
12367        // parent check must filter them out → C = 0.
12368        check_metrics::<CppParser>(
12369            "#include <vector>\nstd::vector<int> f() { return std::vector<int>{}; }",
12370            "foo.cpp",
12371            |metric| {
12372                assert_eq!(metric.abc.conditions_sum(), 0);
12373                insta::assert_json_snapshot!(metric.abc);
12374            },
12375        );
12376    }
12377
12378    #[cfg(feature = "cpp")]
12379    #[test]
12380    fn cpp_else_and_ternary_count_conditions() {
12381        // `if (cond) ... else ...` + ternary `cond ? a : b`. The
12382        // `if`-keyword is NOT a condition (its condition is the
12383        // comparison inside, which counts separately). `else` adds 1,
12384        // `?` adds 1. Two comparisons (`a > b`, `b < 0`) → 2. Total = 4.
12385        check_metrics::<CppParser>(
12386            "int f(int a, int b) {\n\
12387                 if (a > b) { return a; } else { return b; }\n\
12388                 return (b < 0) ? -b : b;\n\
12389             }\n",
12390            "foo.cpp",
12391            |metric| {
12392                assert_eq!(metric.abc.conditions_sum(), 4);
12393                insta::assert_json_snapshot!(metric.abc);
12394            },
12395        );
12396    }
12397
12398    // Issue #1102. A ternary's condition and both branch operands are
12399    // Fitzpatrick Rule 9 unary conditions, exactly as `java_walk_ternary`
12400    // has always counted them. Before the fix the C family scored
12401    // `a ? !b : !c` as 1 — the `?` token alone — against Java's 4.
12402    #[cfg(feature = "cpp")]
12403    #[test]
12404    fn cpp_ternary_operand_slots_count_as_unary_conditions() {
12405        // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4.
12406        check_metrics::<CppParser>("void f() { x = a ? !b : !c; }", "foo.cpp", |metric| {
12407            assert_eq!(metric.abc.conditions_sum(), 4);
12408        });
12409        // No-double-count pin: `?` (1) + `>` (1) = 2, unchanged by the
12410        // fix. The parenthesised condition peels to a
12411        // `binary_expression`, which scores itself, and
12412        // neither branch is negated — the `!` is the type-free proxy for
12413        // "this operand is boolean", so an unnegated branch contributes
12414        // nothing.
12415        check_metrics::<CppParser>("void f() { x = (a > 0) ? b : -b; }", "foo.cpp", |metric| {
12416            assert_eq!(metric.abc.conditions_sum(), 2);
12417        });
12418        // Nested: outer `?` (1) + outer condition `a` (1) + inner `?`
12419        // (1) + inner condition `b` (1) = 4. The outer consequence is
12420        // the inner ternary — which scores itself, and is no
12421        // paren / `!` wrapper — so it adds nothing on its own and the
12422        // inner one is reached by the walk, not by descent.
12423        check_metrics::<CppParser>("void f() { x = a ? b ? c : d : e; }", "foo.cpp", |metric| {
12424            assert_eq!(metric.abc.conditions_sum(), 4);
12425        });
12426        // A negated *condition* is the input where the slot must peel a
12427        // `!`: `cpp_wrapper_operand` takes `!a` down to `a`, which does
12428        // not score itself, so the slot pays once. Every other condition
12429        // fixture in this file wraps a comparison, which scores itself
12430        // and so pays 0 from the slot. `?` (1) + `!a` (1) = 2.
12431        check_metrics::<CppParser>("void f() { x = !a ? b : c; }", "foo.cpp", |metric| {
12432            assert_eq!(metric.abc.conditions_sum(), 2);
12433        });
12434    }
12435
12436    // The GNU short-ternary `a ?: b` elides the consequence, so the
12437    // C-family grammar marks that field optional and the alternative
12438    // lands at child(3) rather than child(4). Addressing the operand
12439    // slots by grammar field name — never by index — is what keeps `!b`
12440    // counted here; a fixed `child(4)` reads `None` and scores 2.
12441    #[cfg(feature = "cpp")]
12442    #[test]
12443    fn cpp_elided_ternary_consequence_still_walks_the_alternative() {
12444        // `?` (1) + condition `a` (1) + `!b` (1) = 3.
12445        check_metrics::<CppParser>("void f() { x = a ?: !b; }", "foo.cpp", |metric| {
12446            assert_eq!(metric.abc.conditions_sum(), 3);
12447        });
12448    }
12449
12450    // `cpp_walk_ternary` is shared by the C, ObjC, and Mozcpp ABC impls
12451    // exactly as `cpp_count_condition` is, so each needs its own
12452    // dispatcher arm. Mozcpp owns no file extension and so gets no
12453    // integration-snapshot coverage at all — this parity assertion is
12454    // its only guard.
12455    //
12456    // The expected value is *derived from the C++ run*, not hardcoded,
12457    // so the four languages cannot silently drift apart if the C++
12458    // expectation ever legitimately moves.
12459    #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12460    #[test]
12461    fn c_family_ternary_operand_slots_agree_with_cpp() {
12462        #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12463        const SRC: &str = "void f() { x = a ? !b : !c; }\n";
12464        let conditions = abc_conditions;
12465
12466        let cpp = conditions(LANG::Cpp, SRC);
12467        // Non-degenerate: a zeroed reference would make every
12468        // comparison below vacuous.
12469        assert_eq!(cpp, 4, "C++ reference value for `a ? !b : !c`");
12470
12471        assert_eq!(conditions(LANG::C, SRC), cpp, "C must match C++");
12472        assert_eq!(conditions(LANG::Mozcpp, SRC), cpp, "Mozcpp must match C++");
12473        assert_eq!(
12474            conditions(
12475                LANG::Objc,
12476                "@implementation Foo\n\
12477                 - (void)bar {\n\
12478                     x = a ? !b : !c;\n\
12479                 }\n\
12480                 @end\n",
12481            ),
12482            cpp,
12483            "ObjC must match C++"
12484        );
12485    }
12486
12487    // Issue #1276, C-family half. `cpp_walk_for_statement` is the
12488    // `for` header's counterpart to the `if` / `while` arms: the slot
12489    // is a bare expression rather than a `condition_clause`, read by
12490    // the same slot rule `cpp_walk_ternary` uses. Every fixture is a
12491    // shape only that walker can classify —
12492    // a comparison-shaped condition proves nothing, the `<` token arm
12493    // counts it either way (grammar-dispatch §11).
12494    #[cfg(feature = "cpp")]
12495    #[test]
12496    fn cpp_for_condition_slot_counts_unary_conditions() {
12497        // Bare identifier: the whole condition, no operator token.
12498        check_metrics::<CppParser>("void f(int a) { for (; a; ) {} }", "foo.cpp", |metric| {
12499            assert_eq!(metric.abc.conditions_sum(), 1);
12500        });
12501        // Negation: the slot peels the `!` (`cpp_wrapper_operand`) and
12502        // pays for the operand beneath it.
12503        check_metrics::<CppParser>("void f(int a) { for (; !a; ) {} }", "foo.cpp", |metric| {
12504            assert_eq!(metric.abc.conditions_sum(), 1);
12505        });
12506        // Parentheses: counted because the slot peels them
12507        // (`cpp_wrapper_operand`) — the path #1276 found dead.
12508        check_metrics::<CppParser>("void f(int a) { for (; (a); ) {} }", "foo.cpp", |metric| {
12509            assert_eq!(metric.abc.conditions_sum(), 1);
12510        });
12511        // No-double-count pin: the `<` token arm already counted this
12512        // shape before the fix and the walker must not add a second.
12513        // The two assignments confirm the header parsed as the
12514        // three-clause form rather than degenerating.
12515        check_metrics::<CppParser>(
12516            "void f(int n) { for (int i = 0; i < n; i++) {} }",
12517            "foo.cpp",
12518            |metric| {
12519                assert_eq!(metric.abc.conditions_sum(), 1);
12520                assert_eq!(metric.abc.assignments_sum(), 2);
12521            },
12522        );
12523        // Empty condition: no `condition` field, no decision, zero.
12524        check_metrics::<CppParser>("void f() { for (;;) { break; } }", "foo.cpp", |metric| {
12525            assert_eq!(metric.abc.conditions_sum(), 0);
12526        });
12527    }
12528
12529    // `cpp_walk_for_statement` is shared by the C, ObjC and Mozcpp ABC
12530    // impls the way `cpp_walk_ternary` is, so each needs its own
12531    // dispatcher arm — and Mozcpp, which owns no file extension, has no
12532    // integration-snapshot coverage at all, making this its only guard.
12533    // The expected value is derived from the C++ run rather than
12534    // hardcoded, so the four cannot silently drift apart.
12535    #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12536    #[test]
12537    fn c_family_for_condition_slot_agrees_with_cpp() {
12538        #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12539        const SRC: &str = "void f(int a) { for (; !a; ) {} }\n";
12540        let conditions = abc_conditions;
12541
12542        let cpp = conditions(LANG::Cpp, SRC);
12543        // Non-degenerate: a zeroed reference makes every comparison
12544        // below vacuous.
12545        assert_eq!(cpp, 1, "C++ reference value for `for (; !a; )`");
12546
12547        assert_eq!(conditions(LANG::C, SRC), cpp, "C must match C++");
12548        assert_eq!(conditions(LANG::Mozcpp, SRC), cpp, "Mozcpp must match C++");
12549        assert_eq!(
12550            conditions(
12551                LANG::Objc,
12552                "@implementation Foo\n\
12553                 - (void)bar {\n\
12554                     for (; !a; ) {}\n\
12555                 }\n\
12556                 @end\n",
12557            ),
12558            cpp,
12559            "ObjC must match C++"
12560        );
12561    }
12562
12563    #[cfg(feature = "cpp")]
12564    #[test]
12565    fn cpp_switch_cases_count_default_excluded() {
12566        // `case 1`, `case 2` → 2 conditions. `default` is intentionally
12567        // excluded (the unconditional fallthrough, mirroring cyclomatic's
12568        // `Case`-only count). Since #469 every C-family language —
12569        // Java, C#, Groovy, JS, TS — agrees on this; C++ already did.
12570        // C = 2.
12571        check_metrics::<CppParser>(
12572            "void f(int x) {\n\
12573                 switch (x) {\n\
12574                     case 1: break;\n\
12575                     case 2: break;\n\
12576                     default: break;\n\
12577                 }\n\
12578             }\n",
12579            "foo.cpp",
12580            |metric| {
12581                assert_eq!(metric.abc.conditions_sum(), 2);
12582                insta::assert_json_snapshot!(metric.abc);
12583            },
12584        );
12585    }
12586
12587    #[cfg(feature = "cpp")]
12588    #[test]
12589    fn cpp_try_catch_count_conditions() {
12590        // `try` and `catch` each add one condition (Fitzpatrick's rule;
12591        // Java's impl above counts them too).
12592        check_metrics::<CppParser>(
12593            "void f() { try { } catch (int) { } catch (...) { } }",
12594            "foo.cpp",
12595            |metric| {
12596                // 1 `try` + 2 `catch` arms = 3.
12597                assert_eq!(metric.abc.conditions_sum(), 3);
12598                insta::assert_json_snapshot!(metric.abc);
12599            },
12600        );
12601    }
12602
12603    #[cfg(feature = "cpp")]
12604    #[test]
12605    fn cpp_complex_function_abc() {
12606        // Mixed-shape regression: assignments, calls, conditions,
12607        // ternary, switch, new. Verified by hand:
12608        // - assignments: `int x = 0` (init_declarator with `=`),
12609        //   `x = 5`, `x += 2`, `x++`, `x = (a > b) ? a : b`, `x = b`,
12610        //   `auto* p = new int(5)` (init_declarator with `=`) → A = 7
12611        //   (post-#393: every `=` in an init_declarator counts).
12612        // - branches: `f(a, b)` self-call + `new int(5)` → B = 2.
12613        // - conditions: `a == b` (1) + `a > 0` (1) inside the if;
12614        //   `&&` itself is NOT a condition (Fitzpatrick Rule 5,
12615        //   issue #395). `a > b` (1) + `?` (1) in the ternary.
12616        //   `else` (1, from the `else if` keyword) + `a < b` (1)
12617        //   in the else-if. `!x` contributes 1 via the unary-
12618        //   conditional walker (Fitzpatrick Rule 9, issue #403):
12619        //   the `||` walker treats `!x` as a unary boolean operand
12620        //   and counts the wrapped Identifier once. `case 1`,
12621        //   `case 2` → 2. `default` excluded. Total C = 9.
12622        check_metrics::<CppParser>(
12623            "int f(int a, int b) {\n\
12624                 int x = 0;\n\
12625                 x = 5;\n\
12626                 x += 2;\n\
12627                 x++;\n\
12628                 if (a == b && a > 0) {\n\
12629                     x = (a > b) ? a : b;\n\
12630                 } else if (a < b || !x) {\n\
12631                     x = b;\n\
12632                 }\n\
12633                 switch (x) {\n\
12634                     case 1: break;\n\
12635                     case 2: break;\n\
12636                     default: break;\n\
12637                 }\n\
12638                 auto* p = new int(5);\n\
12639                 return f(a, b);\n\
12640             }\n",
12641            "foo.cpp",
12642            |metric| {
12643                assert_eq!(metric.abc.assignments_sum(), 7);
12644                assert_eq!(metric.abc.branches_sum(), 2);
12645                assert_eq!(metric.abc.conditions_sum(), 9);
12646                insta::assert_json_snapshot!(metric.abc);
12647            },
12648        );
12649    }
12650
12651    #[cfg(feature = "cpp")]
12652    #[test]
12653    fn cpp_if_multiple_conditions() {
12654        // Fitzpatrick Rule 9 walker (issue #403): each operand of a
12655        // `&&` / `||` chain is one condition.
12656        check_metrics::<CppParser>(
12657            "void f(bool a, bool b, bool c, bool d) {\n\
12658             \x20   if (a || b || c || d) {}        // +4c\n\
12659             \x20   if (a && b && c) {}             // +3c\n\
12660             \x20   if (!a && !b) {}                // +2c\n\
12661             }\n",
12662            "foo.cpp",
12663            |metric| {
12664                assert_eq!(metric.abc.conditions_sum(), 9);
12665                insta::assert_json_snapshot!(metric.abc);
12666            },
12667        );
12668    }
12669
12670    #[cfg(feature = "cpp")]
12671    #[test]
12672    fn cpp_while_and_do_while_conditions() {
12673        // Exercise both the WhileStatement and DoStatement arms via
12674        // the walker on the `&&` / `||` tokens inside their parens.
12675        check_metrics::<CppParser>(
12676            "void f(bool a, bool b) {\n\
12677             \x20   while (a || b) {}              // +2c\n\
12678             \x20   do {} while (a && !b);         // +2c\n\
12679             }\n",
12680            "foo.cpp",
12681            |metric| {
12682                assert_eq!(metric.abc.conditions_sum(), 4);
12683                insta::assert_json_snapshot!(metric.abc);
12684            },
12685        );
12686    }
12687
12688    #[cfg(feature = "cpp")]
12689    #[test]
12690    fn cpp_if_constexpr_condition_counts() {
12691        // Regression for the code-review finding: C++ `if constexpr
12692        // (cond)` puts the `constexpr` keyword at child(1) and the
12693        // condition_clause at child(2). Pre-fix, the dispatcher used
12694        // child(1) and counted zero conditions for the `constexpr`
12695        // form. The fix uses `child_by_field_name("condition")`
12696        // which returns the condition_clause regardless of the
12697        // optional `constexpr` keyword.
12698        check_metrics::<CppParser>(
12699            "template <int N> void f() {\n\
12700             \x20   if constexpr (true) { }      // +1c\n\
12701             \x20   if (false) { }               // +1c\n\
12702             }\n",
12703            "foo.cpp",
12704            |metric| {
12705                assert_eq!(metric.abc.conditions_sum(), 2);
12706                insta::assert_json_snapshot!(metric.abc);
12707            },
12708        );
12709    }
12710
12711    #[cfg(feature = "cpp")]
12712    #[test]
12713    fn cpp_cast_expression_in_logical_chain_counts() {
12714        // Regression for findings.md round-2 #1 (C++):
12715        // `if ((bool)ptr && ready) {}` had the `||` walker missing
12716        // the `(bool)ptr` operand because `CastExpression` was not
12717        // in the former `cpp_bool_terminal_kinds!()`. Mirrors C#'s
12718        // former `csharp_bool_terminal_kinds!()`, which listed
12719        // `CastExpression` (lesson 19, #372).
12720        check_metrics::<CppParser>(
12721            "void f(void* ptr, bool ready) { if ((bool)ptr && ready) { } }\n",
12722            "foo.cpp",
12723            |metric| {
12724                // `&&` walker counts both operands: `(bool)ptr` (1)
12725                // and `ready` (1). Total: 2.
12726                assert_eq!(metric.abc.conditions_sum(), 2);
12727                insta::assert_json_snapshot!(metric.abc);
12728            },
12729        );
12730    }
12731
12732    #[cfg(feature = "cpp")]
12733    #[test]
12734    fn cpp_qualified_identifier_condition_counts() {
12735        // Regression for findings.md #3 (C++): tree-sitter-cpp emits
12736        // `qualified_identifier` under four kind_ids (573..576) per
12737        // the production-rule path; runtime kind for `ns::flag` is
12738        // 574 (`QualifiedIdentifier2`). Pre-fix the
12739        // former `cpp_bool_terminal_kinds!()` macro listed neither the
12740        // primary nor any alias, so `if (n::flag) {}` reported zero
12741        // conditions. The slot now pays for every variant, since none
12742        // scores itself (lesson #2).
12743        check_metrics::<CppParser>(
12744            "namespace n { extern bool flag; }\n\
12745             void f() { if (n::flag) { } }\n",
12746            "foo.cpp",
12747            |metric| {
12748                assert_eq!(metric.abc.conditions_sum(), 1);
12749                insta::assert_json_snapshot!(metric.abc);
12750            },
12751        );
12752    }
12753
12754    #[cfg(feature = "cpp")]
12755    #[test]
12756    fn cpp_if_boolean_literal_condition() {
12757        check_metrics::<CppParser>(
12758            "void f() {\n\
12759             \x20   if (true) {}                 // +1c\n\
12760             \x20   if (!false) {}               // +1c\n\
12761             \x20   while (true) {}              // +1c\n\
12762             \x20   do {} while (false);         // +1c\n\
12763             }\n",
12764            "foo.cpp",
12765            |metric| {
12766                assert_eq!(metric.abc.conditions_sum(), 4);
12767                insta::assert_json_snapshot!(metric.abc);
12768            },
12769        );
12770    }
12771
12772    #[cfg(feature = "cpp")]
12773    #[test]
12774    fn cpp_methods_arguments_with_conditions() {
12775        check_metrics::<CppParser>(
12776            "void f(bool a, bool b) {\n\
12777             \x20   m(a, b);                     // +1b\n\
12778             \x20   m(!a, !b);                   // +1b +2c\n\
12779             }\n",
12780            "foo.cpp",
12781            |metric| {
12782                assert_eq!(metric.abc.branches_sum(), 2);
12783                assert_eq!(metric.abc.conditions_sum(), 2);
12784                insta::assert_json_snapshot!(metric.abc);
12785            },
12786        );
12787    }
12788
12789    #[cfg(feature = "cpp")]
12790    #[test]
12791    fn cpp_return_with_conditions() {
12792        check_metrics::<CppParser>(
12793            "bool m1(int z) { return !(z >= 0); }\n\
12794             bool m2(bool x) { return (((!x))); }\n\
12795             bool m3(bool x, bool y) { return x && y; }\n",
12796            "foo.cpp",
12797            |metric| {
12798                // m1: !(z >= 0) → `>=` (1). `!` wraps a paren'd
12799                //     BinaryExpression — inspect_container reaches
12800                //     the inner BinaryExpression and stops, no
12801                //     walker count. +1.
12802                // m2: (((!x))) → ReturnStatement → inspect_container
12803                //     unwraps three parens + one unary → reaches `x`
12804                //     in has_boolean_content=true (seeded by the
12805                //     unary `!`). +1.
12806                // m3: x && y → `&&` walker counts both → +2.
12807                // Sum: 1 + 1 + 2 = 4.
12808                assert_eq!(metric.abc.conditions_sum(), 4);
12809                insta::assert_json_snapshot!(metric.abc);
12810            },
12811        );
12812    }
12813
12814    // #1547: `co_return` is a coroutine's `return`, so ABC reads its value
12815    // the same way. Each row's expected value is its `return` twin's. A
12816    // `co_yield` hands a value back without returning, as the JS family's
12817    // `yield` does, and scores no value slot either.
12818    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
12819    fn assert_co_return_scores_like_return(lang: LANG) {
12820        let source = |body: &str| format!("task f(bool a) {{ {body} }}\n");
12821        let rows = [
12822            ("co_return !a;", "return !a;", 1),
12823            ("co_return /*c*/ !a;", "return /*c*/ !a;", 1),
12824            ("co_return a;", "return a;", 0),
12825            ("co_return;", "return;", 0),
12826        ];
12827        for (coroutine, twin, expected) in rows {
12828            assert_eq!(
12829                abc_conditions(lang, &source(twin)),
12830                expected,
12831                "{lang:?} `{twin}`"
12832            );
12833            assert_eq!(
12834                abc_conditions(lang, &source(coroutine)),
12835                expected,
12836                "{lang:?} `{coroutine}`"
12837            );
12838        }
12839        assert_eq!(
12840            abc_conditions(lang, &source("co_yield !a;")),
12841            0,
12842            "{lang:?} co_yield"
12843        );
12844    }
12845
12846    #[cfg(feature = "cpp")]
12847    #[test]
12848    fn cpp_co_return_value_scores_like_return_1547() {
12849        assert_co_return_scores_like_return(LANG::Cpp);
12850    }
12851
12852    // Mozcpp owns no file extension, so this is its only coverage.
12853    #[cfg(feature = "mozcpp")]
12854    #[test]
12855    fn mozcpp_co_return_value_scores_like_return_1547() {
12856        assert_co_return_scores_like_return(LANG::Mozcpp);
12857    }
12858
12859    #[cfg(feature = "cpp")]
12860    #[test]
12861    fn cpp_short_circuit_with_boolean_literal_operand() {
12862        // `a && true` reports 2 conditions: one for the identifier
12863        // operand, one for the `True` literal operand.
12864        check_metrics::<CppParser>(
12865            "bool f(bool a) { return a && true; }\n",
12866            "foo.cpp",
12867            |metric| {
12868                assert_eq!(metric.abc.conditions_sum(), 2);
12869                insta::assert_json_snapshot!(metric.abc);
12870            },
12871        );
12872    }
12873
12874    #[cfg(feature = "javascript")]
12875    #[test]
12876    fn javascript_empty_unit_zero() {
12877        // No code → A=B=C=0. Wires up the trait and exercises the
12878        // per-language compute reachability.
12879        check_metrics::<JavascriptParser>("", "empty.js", |metric| {
12880            assert_eq!(metric.abc.assignments_sum(), 0);
12881            assert_eq!(metric.abc.branches_sum(), 0);
12882            assert_eq!(metric.abc.conditions_sum(), 0);
12883            insta::assert_json_snapshot!(metric.abc);
12884        });
12885    }
12886
12887    #[cfg(feature = "javascript")]
12888    #[test]
12889    fn javascript_plain_and_compound_assignments_count() {
12890        // `let` / `var` declarations behave like TypeScript: only a
12891        // `const` initializer is suppressed. So `let x = 0` does count as
12892        // A=+1; only `const PI = 3.14` would be elided. Plain `x = 5`,
12893        // `x += 2`, `x = 7` all count → A = 4 total here.
12894        check_metrics::<JavascriptParser>(
12895            "function f() { let x = 0; x = 5; x += 2; x = 7; }",
12896            "foo.js",
12897            |metric| {
12898                assert_eq!(metric.abc.assignments_sum(), 4);
12899                assert_eq!(metric.abc.branches_sum(), 0);
12900                assert_eq!(metric.abc.conditions_sum(), 0);
12901                insta::assert_json_snapshot!(metric.abc);
12902            },
12903        );
12904    }
12905
12906    #[cfg(feature = "javascript")]
12907    #[test]
12908    fn javascript_const_initializer_not_assignment() {
12909        // `const PI = 3.14` must NOT count as an assignment — its `=`
12910        // initialises a `const` binding. `let x = 1` and `var y = 2`
12911        // still count (matches the TS impl: only `const` suppresses).
12912        check_metrics::<JavascriptParser>(
12913            "function f() { const PI = 3.14; let x = 1; var y = 2; x = 9; }",
12914            "foo.js",
12915            |metric| {
12916                // `const PI` suppressed; `let x = 1`, `var y = 2`,
12917                // `x = 9` all count → A = 3.
12918                assert_eq!(metric.abc.assignments_sum(), 3);
12919                insta::assert_json_snapshot!(metric.abc);
12920            },
12921        );
12922    }
12923
12924    #[cfg(feature = "javascript")]
12925    #[test]
12926    fn javascript_asi_const_does_not_suppress_later_assignments() {
12927        // The issue #1277 reproducer verbatim. JavaScript half of the
12928        // cluster documented at
12929        // `typescript_asi_const_does_not_suppress_later_assignments`.
12930        check_metrics::<JavascriptParser>(
12931            "function f() {
12932                const a = 1
12933                x = 2
12934                return x
12935            }",
12936            "foo.js",
12937            |metric| {
12938                // Pre-#1277 this reported 0; the semicolon-terminated
12939                // spelling reported 1.
12940                assert_eq!(metric.abc.assignments_sum(), 1);
12941            },
12942        );
12943    }
12944
12945    #[cfg(feature = "javascript")]
12946    #[test]
12947    fn javascript_nested_arrow_const_does_not_leak() {
12948        // The ASI leak beside a nested space: the arrow body opens its
12949        // own space, so `y = 1` was always counted there, but
12950        // `const f = () => …` has no `;`, so `h`'s sentinel stayed live
12951        // and suppressed `z = 2`. The stack was per-space state that
12952        // `Stats::merge` never carried, so this is the terminator defect
12953        // and not a second route.
12954        check_metrics::<JavascriptParser>(
12955            "function h() {
12956                const f = () => { y = 1 }
12957                z = 2
12958            }",
12959            "foo.js",
12960            |metric| {
12961                // `y = 1` and `z = 2`; the `const` initializer is
12962                // suppressed. Pre-#1277: 1.
12963                assert_eq!(metric.abc.assignments_sum(), 2);
12964            },
12965        );
12966    }
12967
12968    #[cfg(feature = "javascript")]
12969    #[test]
12970    fn javascript_non_declarator_equals_still_count() {
12971        // An `=` that does not belong to a `const` declarator is always
12972        // an assignment: a class `field_definition` initializer, a
12973        // default-parameter `assignment_pattern`, and a destructured
12974        // parameter's defaults, whose climb crosses the same pattern
12975        // layers as a `const` pattern's but ends at `formal_parameters`.
12976        // A predicate that accepted any pattern ancestor as a declarator
12977        // would zero the last three.
12978        check_metrics::<JavascriptParser>(
12979            "class K { f = 1; g(p = 2, {q = 3} = {}) { let d = 4 } }",
12980            "foo.js",
12981            |metric| {
12982                // `f = 1`, `p = 2`, `q = 3`, `= {}`, `let d = 4`.
12983                assert_eq!(metric.abc.assignments_sum(), 5);
12984            },
12985        );
12986    }
12987
12988    #[cfg(feature = "javascript")]
12989    #[test]
12990    fn javascript_const_initializer_value_assignments_still_count() {
12991        // An `=` inside a `const` initializer's *value* is an
12992        // `assignment_expression`: its climb reaches no pattern layer and
12993        // no declarator, so it counts. The pre-#1277 sentinel
12994        // blanket-suppressed every `=` between `const` and `;`, which is
12995        // the shape that moved the pdf.js corpus snapshots
12996        // (`const bbox = (this.data.rect = …)`).
12997        check_metrics::<JavascriptParser>(
12998            "function m(o, a, b) { const x = (o.p = 1); const y = a || (b = 2); }",
12999            "foo.js",
13000            |metric| {
13001                // `o.p = 1` and `b = 2`; the two `const` initializers are
13002                // suppressed. Pre-#1277: 0.
13003                assert_eq!(metric.abc.assignments_sum(), 2);
13004            },
13005        );
13006    }
13007
13008    #[cfg(feature = "javascript")]
13009    #[test]
13010    fn javascript_const_declarator_shapes_stay_suppressed() {
13011        // JavaScript half of
13012        // `typescript_const_declarator_shapes_stay_suppressed`.
13013        check_metrics::<JavascriptParser>(
13014            "function f(o, xs) {
13015                const a = 1, b = 2
13016                const {c = 5, d: {e = 6} = {}} = o
13017                const [g = 7, ...[h = 8]] = xs
13018                let i = 3
13019                var j = 4
13020                for (const x of xs) { k(x) }
13021            }",
13022            "foo.js",
13023            |metric| {
13024                assert_eq!(metric.abc.assignments_sum(), 2);
13025            },
13026        );
13027    }
13028
13029    #[cfg(feature = "javascript")]
13030    #[test]
13031    fn javascript_increment_and_decrement_count_as_assignment() {
13032        // `x++` (post) and `--x` (pre) both update an lvalue and so
13033        // count as assignments. Combined with the `let x = 0`
13034        // initializer (which counts under the JS/TS rule — only `const`
13035        // suppresses), A = 3.
13036        check_metrics::<JavascriptParser>(
13037            "function f() { let x = 0; x++; --x; }",
13038            "foo.js",
13039            |metric| {
13040                assert_eq!(metric.abc.assignments_sum(), 3);
13041                insta::assert_json_snapshot!(metric.abc);
13042            },
13043        );
13044    }
13045
13046    #[cfg(feature = "javascript")]
13047    #[test]
13048    fn javascript_calls_are_branches() {
13049        // `g(1)` is a `call_expression` → B = 1. `new Foo(2)` is a
13050        // `new_expression` → B = 1. Total B = 2.
13051        check_metrics::<JavascriptParser>(
13052            "function f() { g(1); new Foo(2); }",
13053            "foo.js",
13054            |metric| {
13055                assert_eq!(metric.abc.branches_sum(), 2);
13056                assert_eq!(metric.abc.conditions_sum(), 0);
13057                insta::assert_json_snapshot!(metric.abc);
13058            },
13059        );
13060    }
13061
13062    #[cfg(feature = "javascript")]
13063    #[test]
13064    fn javascript_comparisons_count_conditions() {
13065        // `==`, `===`, `!=`, `!==`, `<`, `>`, `<=`, `>=` each count
13066        // once. The `&&` / `||` short-circuit operators are NOT
13067        // counted as conditions in this impl (matches the TS
13068        // precedent — short-circuit ops are folded into the
13069        // surrounding `if` / control-flow arm, not separately).
13070        // Total C = 8.
13071        check_metrics::<JavascriptParser>(
13072            "function f(a, b) { return a == b && a === b && a != b && a !== b && a < b && a > b && a <= b && a >= b; }",
13073            "foo.js",
13074            |metric| {
13075                assert_eq!(metric.abc.conditions_sum(), 8);
13076                insta::assert_json_snapshot!(metric.abc);
13077            },
13078        );
13079    }
13080
13081    // #1297 in the `js_abc_compute!` expansion, where `LT` / `GT` had
13082    // no gate at all: plain JavaScript has no generics, but
13083    // tree-sitter-javascript parses JSX unconditionally, so the same
13084    // seven tag brackets that
13085    // `tsx_jsx_elements_are_not_conditions` covers scored seven
13086    // conditions in a `.js` file too. Same fixture minus the type
13087    // annotations, same discriminating numbers: 8 pre-fix, 1 after.
13088    #[cfg(feature = "javascript")]
13089    #[test]
13090    fn javascript_jsx_elements_are_not_conditions() {
13091        check_metrics::<JavascriptParser>(
13092            "function f(a, b) {
13093                if (a < b) { return <div className=\"x\"><span>hi</span></div>; }
13094                return <br />;
13095            }",
13096            "foo.js",
13097            |metric| {
13098                assert_eq!(metric.abc.conditions_sum(), 1);
13099                // Fixture-presence anchor: the `className="x"` attribute
13100                // `=` is this fixture's only assignment, so the test
13101                // fails if a later edit drops the JSX. Without it the
13102                // surviving `a < b` alone also scores 1, and the
13103                // conditions assertion would pass while proving nothing.
13104                assert_eq!(metric.abc.assignments_sum(), 1);
13105            },
13106        );
13107    }
13108
13109    #[cfg(feature = "javascript")]
13110    #[test]
13111    fn javascript_number_truthy_condition_counts() {
13112        // Regression for #772: JS treats every non-zero number as
13113        // truthy, so `while (5)` and `x && 5` should each count their
13114        // numeric literal as a Fitzpatrick unary condition. Pre-fix
13115        // the former `javascript_bool_terminal_kinds!()` listed `True` /
13116        // `False` but omitted `Number`, so the walker dropped every numeric-
13117        // truthy operand (mirrors the Lua `Number` fix).
13118        check_metrics::<JavascriptParser>(
13119            "function f(x) { while (5) {} return x && 5; }",
13120            "foo.js",
13121            |metric| {
13122                // `while (5)` → Number literal (+1). `x && 5` → both
13123                // operands count: identifier `x` (+1), Number `5` (+1).
13124                // Total: 3.
13125                assert_eq!(metric.abc.conditions_sum(), 3);
13126                insta::assert_json_snapshot!(metric.abc);
13127            },
13128        );
13129    }
13130
13131    #[cfg(feature = "typescript")]
13132    #[test]
13133    fn typescript_number_truthy_condition_counts() {
13134        // Regression for #772: TS shares the JS truthy semantics. The
13135        // numeric *literal* `5` (kind `Number`) counts; the type-keyword
13136        // `number` (kind `Number2`, the `predefined_type`) must not —
13137        // see the former `typescript_bool_terminal_kinds!`.
13138        check_metrics::<TypescriptParser>(
13139            "function f(x: number) { while (5) {} return x && 5; }",
13140            "foo.ts",
13141            |metric| {
13142                // `while (5)` → +1; `x && 5` → `x` (+1) + `5` (+1).
13143                // Total: 3. The `: number` annotation contributes 0.
13144                assert_eq!(metric.abc.conditions_sum(), 3);
13145                insta::assert_json_snapshot!(metric.abc);
13146            },
13147        );
13148    }
13149
13150    #[cfg(feature = "javascript")]
13151    #[test]
13152    fn javascript_nullish_coalescing_counts_condition() {
13153        // `a ?? b` is one nullish-coalescing operator → C = 1.
13154        check_metrics::<JavascriptParser>(
13155            "function f(a, b) { return a ?? b; }",
13156            "foo.js",
13157            |metric| {
13158                assert_eq!(metric.abc.conditions_sum(), 1);
13159                insta::assert_json_snapshot!(metric.abc);
13160            },
13161        );
13162    }
13163
13164    #[cfg(feature = "javascript")]
13165    #[test]
13166    fn javascript_else_ternary_case_default_try_catch() {
13167        // `else`, `?` (ternary), `case`, `try`, `catch` all count.
13168        // `default` is the unconditional fallthrough → +0 (#469).
13169        // With the comparisons:
13170        //   - `a > 0` → 1
13171        //   - `else` opens an else_clause → 1
13172        //   - `?` ternary → 1
13173        //   - the ternary's bare-identifier condition `a` → 1 (#1102)
13174        //   - `case 1` → 1
13175        //   - `default` → 0 (fallthrough, #469)
13176        //   - `try` + `catch` → 2
13177        // Total C = 7.
13178        check_metrics::<JavascriptParser>(
13179            "function f(a) { if (a > 0) {} else {} let x = a ? 1 : 2; switch (x) { case 1: break; default: break; } try { } catch (e) { } }",
13180            "foo.js",
13181            |metric| {
13182                assert_eq!(metric.abc.conditions_sum(), 7);
13183                insta::assert_json_snapshot!(metric.abc);
13184            },
13185        );
13186    }
13187
13188    // Issue #1102, JS-family half. See
13189    // `cpp_ternary_operand_slots_count_as_unary_conditions` for the
13190    // rule; the two families were behind Java by the same three units.
13191    #[cfg(feature = "javascript")]
13192    #[test]
13193    fn javascript_ternary_operand_slots_count_as_unary_conditions() {
13194        // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4.
13195        check_metrics::<JavascriptParser>(
13196            "function f() { x = a ? !b : !c; }",
13197            "foo.js",
13198            |metric| assert_eq!(metric.abc.conditions_sum(), 4),
13199        );
13200        // No-double-count pin: `?` (1) + `>` (1) = 2, unchanged by the
13201        // fix — the parenthesised condition unwraps to a
13202        // `binary_expression` (not a boolean terminal) and neither
13203        // branch is negated.
13204        check_metrics::<JavascriptParser>(
13205            "function f() { x = (a > 0) ? b : -b; }",
13206            "foo.js",
13207            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
13208        );
13209        // Nested: two `?` tokens plus the two bare-identifier
13210        // conditions = 4.
13211        check_metrics::<JavascriptParser>(
13212            "function f() { x = a ? b ? c : d : e; }",
13213            "foo.js",
13214            |metric| assert_eq!(metric.abc.conditions_sum(), 4),
13215        );
13216        // A negated condition is the only input reaching the walker's
13217        // `else` fallback — see the C++ sibling for why. `?` (1) +
13218        // `!a` (1) = 2.
13219        check_metrics::<JavascriptParser>("function f() { x = !a ? b : c; }", "foo.js", |metric| {
13220            assert_eq!(metric.abc.conditions_sum(), 2);
13221        });
13222    }
13223
13224    // TypeScript expands the same `ts_abc_compute!` arm from a separate
13225    // macro body than JavaScript's `js_abc_compute!`, so wiring one and
13226    // not the other is a live failure mode; TSX and Mozjs are clones of
13227    // these two.
13228    #[cfg(feature = "typescript")]
13229    #[test]
13230    fn typescript_ternary_operand_slots_count_as_unary_conditions() {
13231        check_metrics::<TypescriptParser>(
13232            "function f() { x = a ? !b : !c; }",
13233            "foo.ts",
13234            |metric| assert_eq!(metric.abc.conditions_sum(), 4),
13235        );
13236        check_metrics::<TypescriptParser>(
13237            "function f() { x = (a > 0) ? b : -b; }",
13238            "foo.ts",
13239            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
13240        );
13241    }
13242
13243    // Issue #1276, JS-family half. See
13244    // `cpp_for_condition_slot_counts_unary_conditions` for the rule.
13245    // The JS grammar marks the `condition` field on both the expression
13246    // and the `;` closing it, so `child_by_field_name` is the only
13247    // addressing that lands on the expression for every header shape.
13248    #[cfg(feature = "javascript")]
13249    #[test]
13250    fn javascript_for_condition_slot_counts_unary_conditions() {
13251        // Bare identifier: no operator token anywhere in the header.
13252        check_metrics::<JavascriptParser>("function f(a) { for (; a; ) {} }", "foo.js", |metric| {
13253            assert_eq!(metric.abc.conditions_sum(), 1);
13254        });
13255        // Negation, through the `!` unwrap.
13256        check_metrics::<JavascriptParser>(
13257            "function f(a) { for (; !a; ) {} }",
13258            "foo.js",
13259            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
13260        );
13261        // Parentheses: counts only via the `ForStatement`
13262        // boolean-context seed #1276 found dead.
13263        check_metrics::<JavascriptParser>(
13264            "function f(a) { for (; (a); ) {} }",
13265            "foo.js",
13266            |metric| assert_eq!(metric.abc.conditions_sum(), 1),
13267        );
13268        // No-double-count pin: the `<` arm already counted this shape.
13269        // `let i = 0` and `i++` are the two assignments, which also
13270        // confirms the header parsed as the three-clause form.
13271        check_metrics::<JavascriptParser>(
13272            "function f(n) { for (let i = 0; i < n; i++) {} }",
13273            "foo.js",
13274            |metric| {
13275                assert_eq!(metric.abc.conditions_sum(), 1);
13276                assert_eq!(metric.abc.assignments_sum(), 2);
13277            },
13278        );
13279        // Empty condition: the slot holds an `empty_statement`, which
13280        // is neither a terminal nor a wrapper, so it counts nothing.
13281        check_metrics::<JavascriptParser>(
13282            "function f() { for (;;) { break; } }",
13283            "foo.js",
13284            |metric| assert_eq!(metric.abc.conditions_sum(), 0),
13285        );
13286    }
13287
13288    // TypeScript expands the `ForStatement` arm from `ts_abc_compute!`,
13289    // a separate macro body from JavaScript's `js_abc_compute!`, so
13290    // wiring one and not the other is a live failure mode; TSX and
13291    // Mozjs are the clones of those two. The expected values are
13292    // derived from the JavaScript run rather than hardcoded.
13293    #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13294    #[test]
13295    fn js_family_for_condition_slot_agrees_with_javascript() {
13296        #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13297        const BARE: &str = "function f(a) { for (; a; ) {} }\n";
13298        #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13299        const EMPTY: &str = "function f() { for (;;) { break; } }\n";
13300        let conditions = abc_conditions;
13301
13302        let bare = conditions(LANG::Javascript, BARE);
13303        // Non-degenerate: a zeroed reference makes the comparisons
13304        // below vacuous.
13305        assert_eq!(bare, 1, "JavaScript reference value for `for (; a; )`");
13306        assert_eq!(
13307            conditions(LANG::Javascript, EMPTY),
13308            0,
13309            "JavaScript `for (;;)`"
13310        );
13311
13312        for lang in [LANG::Mozjs, LANG::Typescript, LANG::Tsx] {
13313            assert_eq!(conditions(lang, BARE), bare, "{lang:?} bare for-condition");
13314            assert_eq!(conditions(lang, EMPTY), 0, "{lang:?} empty for-condition");
13315        }
13316    }
13317
13318    #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13319    #[test]
13320    fn js_family_ternary_operand_slots_agree_with_javascript() {
13321        // `a ? !b : !c` is the one shape that tells the ternary walker
13322        // from the `for`-header walker: both read the `condition` field
13323        // and share the `(&Node, &mut f64)` signature, so a transposed
13324        // pair in a `ts_abc_compute!` / `js_abc_compute!` invocation
13325        // compiles, passes every condition-slot test, and drops only the
13326        // two branch operands. TypeScript alone pinned those before; the
13327        // other three expansions now do too.
13328        #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13329        const SRC: &str = "function f(a, b, c) { x = a ? !b : !c; }\n";
13330        let javascript = abc_conditions(LANG::Javascript, SRC);
13331        // expected: 4 — the `?`, the `a` condition slot and both negated
13332        // branch operands; non-degenerate by construction.
13333        assert_eq!(
13334            javascript, 4,
13335            "JavaScript reference value for `a ? !b : !c`"
13336        );
13337        for lang in [LANG::Mozjs, LANG::Typescript, LANG::Tsx] {
13338            assert_eq!(
13339                abc_conditions(lang, SRC),
13340                javascript,
13341                "{lang:?} ternary operand slots"
13342            );
13343        }
13344    }
13345
13346    #[cfg(feature = "javascript")]
13347    #[test]
13348    fn javascript_instanceof_counts_condition() {
13349        // `x instanceof Foo` is a binary expression whose operator is
13350        // the `instanceof` keyword token → C = 1.
13351        check_metrics::<JavascriptParser>(
13352            "function f(x) { return x instanceof Foo; }",
13353            "foo.js",
13354            |metric| {
13355                assert_eq!(metric.abc.conditions_sum(), 1);
13356                insta::assert_json_snapshot!(metric.abc);
13357            },
13358        );
13359    }
13360
13361    #[cfg(feature = "javascript")]
13362    #[test]
13363    fn javascript_complex_function_abc() {
13364        // Mixed-shape regression. Verified by hand:
13365        // - assignments: `let x = 0` (a `let` initializer counts)
13366        //   + `x = 5`, `x += 2`, `x++`, `x = (a>b)?a:b`, `x = b`,
13367        //   `let p = ...` (likewise) → A = 7.
13368        // - branches: `f(a, b)` self-call + `new Bar()` → B = 2.
13369        // - conditions: `a == b`, `a > 0` → 2 inside the if header
13370        //   (`&&` is not counted directly). `else` (1) + `a > b`,
13371        //   `?` → 2 in the ternary. `a < b` → 1 in the else-if.
13372        //   `!x` → 1 from the Fitzpatrick Rule 9 walker on `||`
13373        //   (issue #403): the wrapped Identifier counts once.
13374        //   `case 1` → 1 in the switch; `default` → 0 (fallthrough,
13375        //   #469). Total C = 8.
13376        check_metrics::<JavascriptParser>(
13377            "function f(a, b) {\n\
13378                 let x = 0;\n\
13379                 x = 5;\n\
13380                 x += 2;\n\
13381                 x++;\n\
13382                 if (a == b && a > 0) {\n\
13383                     x = (a > b) ? a : b;\n\
13384                 } else if (a < b || !x) {\n\
13385                     x = b;\n\
13386                 }\n\
13387                 switch (x) {\n\
13388                     case 1: break;\n\
13389                     default: break;\n\
13390                 }\n\
13391                 let p = new Bar();\n\
13392                 return f(a, b);\n\
13393             }\n",
13394            "foo.js",
13395            |metric| {
13396                assert_eq!(metric.abc.assignments_sum(), 7);
13397                assert_eq!(metric.abc.branches_sum(), 2);
13398                assert_eq!(metric.abc.conditions_sum(), 8);
13399                insta::assert_json_snapshot!(metric.abc);
13400            },
13401        );
13402    }
13403
13404    #[cfg(feature = "mozjs")]
13405    #[test]
13406    fn mozjs_asi_const_does_not_suppress_later_assignments() {
13407        // Mozjs half of the #1277 cluster; the fork carries its own
13408        // kind-id numbering, so it needs its own fixture. See
13409        // `typescript_asi_const_does_not_suppress_later_assignments`.
13410        check_metrics::<MozjsParser>(
13411            "function f() {
13412                const a = 1
13413                x = 2
13414                y = 3
13415            }",
13416            "foo.jsm",
13417            |metric| {
13418                assert_eq!(metric.abc.assignments_sum(), 2);
13419            },
13420        );
13421    }
13422
13423    #[cfg(feature = "mozjs")]
13424    #[test]
13425    fn mozjs_const_declarator_shapes_stay_suppressed() {
13426        // Mozjs half of
13427        // `javascript_const_declarator_shapes_stay_suppressed`.
13428        check_metrics::<MozjsParser>(
13429            "function f(o, xs) {
13430                const a = 1, b = 2
13431                const {c = 5, d: {e = 6} = {}} = o
13432                const [g = 7, ...[h = 8]] = xs
13433                let i = 3
13434                var j = 4
13435                for (const x of xs) { k(x) }
13436            }",
13437            "foo.jsm",
13438            |metric| {
13439                assert_eq!(metric.abc.assignments_sum(), 2);
13440            },
13441        );
13442    }
13443
13444    #[cfg(feature = "mozjs")]
13445    #[test]
13446    fn mozjs_complex_function_abc() {
13447        // Mozjs shares JavaScript's expression / statement vocabulary;
13448        // the `js_abc_compute!` macro expands identical token-level
13449        // rules for both. This test pins parity against the JS impl.
13450        check_metrics::<MozjsParser>(
13451            "function f(a, b) {\n\
13452                 let x = 0;\n\
13453                 x = 5;\n\
13454                 x += 2;\n\
13455                 x++;\n\
13456                 if (a == b && a > 0) {\n\
13457                     x = (a > b) ? a : b;\n\
13458                 } else if (a < b || !x) {\n\
13459                     x = b;\n\
13460                 }\n\
13461                 switch (x) {\n\
13462                     case 1: break;\n\
13463                     default: break;\n\
13464                 }\n\
13465                 let p = new Bar();\n\
13466                 return f(a, b);\n\
13467             }\n",
13468            "foo.js",
13469            |metric| {
13470                assert_eq!(metric.abc.assignments_sum(), 7);
13471                assert_eq!(metric.abc.branches_sum(), 2);
13472                assert_eq!(metric.abc.conditions_sum(), 8);
13473                insta::assert_json_snapshot!(metric.abc);
13474            },
13475        );
13476    }
13477
13478    // ----- JS / TS / Tsx / Mozjs Phase-2B condition slots -----
13479
13480    #[cfg(feature = "javascript")]
13481    #[test]
13482    fn javascript_await_expression_condition_counts() {
13483        // Regression for findings.md round-2 #2 (JS):
13484        // `if (await ready()) {}` parses with `await_expression` as
13485        // the condition node inside the `parenthesized_expression`.
13486        // The former `javascript_inspect_container` unwrapped the paren
13487        // but the await child was not in the terminal-bool set, so the
13488        // walker broke without counting. Mirrors C# (lesson 19).
13489        check_metrics::<JavascriptParser>(
13490            "async function ready() { return true; }\n\
13491             async function f() { if (await ready()) { } }\n",
13492            "foo.js",
13493            |metric| {
13494                assert_eq!(metric.abc.branches_sum(), 1);
13495                assert_eq!(metric.abc.conditions_sum(), 1);
13496                insta::assert_json_snapshot!(metric.abc);
13497            },
13498        );
13499    }
13500
13501    #[cfg(feature = "javascript")]
13502    #[test]
13503    fn javascript_member_expression_condition_counts() {
13504        // Regression for findings.md #3 (JS-family): tree-sitter-
13505        // javascript emits `member_expression` under three kind_ids
13506        // (191 primary, 208, 228 — `MemberExpression2/3`) depending
13507        // on the production rule path. The verifier in this audit
13508        // confirmed runtime kind for `o.x` is 208. Pre-fix the
13509        // shared former `js_family_bool_terminal_kinds!()` macro listed
13510        // only the primary, so every `if (o.x) {}` / `o.x && o.y`
13511        // condition silently reported zero. The slot now pays for all
13512        // three aliases, none of which scores itself (lesson #2).
13513        check_metrics::<JavascriptParser>(
13514            "function f(o) {\n\
13515             \x20   if (o.x) {}                  // +1c\n\
13516             \x20   return o.x && o.y;           // +2c (walker on &&)\n\
13517             }\n",
13518            "foo.js",
13519            |metric| {
13520                assert_eq!(metric.abc.conditions_sum(), 3);
13521                insta::assert_json_snapshot!(metric.abc);
13522            },
13523        );
13524    }
13525
13526    #[cfg(feature = "javascript")]
13527    #[test]
13528    fn javascript_if_boolean_literal_condition() {
13529        check_metrics::<JavascriptParser>(
13530            "function f() {\n\
13531             \x20   if (true) {}                 // +1c\n\
13532             \x20   if (!false) {}               // +1c\n\
13533             \x20   while (true) {}              // +1c\n\
13534             \x20   do {} while (false);         // +1c\n\
13535             }\n",
13536            "foo.js",
13537            |metric| {
13538                assert_eq!(metric.abc.conditions_sum(), 4);
13539                insta::assert_json_snapshot!(metric.abc);
13540            },
13541        );
13542    }
13543
13544    #[cfg(feature = "javascript")]
13545    #[test]
13546    fn javascript_methods_arguments_with_conditions() {
13547        check_metrics::<JavascriptParser>(
13548            "function f(a, b) {\n\
13549             \x20   m(a, b);                     // +1b\n\
13550             \x20   m(!a, !b);                   // +1b +2c\n\
13551             }\n",
13552            "foo.js",
13553            |metric| {
13554                assert_eq!(metric.abc.branches_sum(), 2);
13555                assert_eq!(metric.abc.conditions_sum(), 2);
13556                insta::assert_json_snapshot!(metric.abc);
13557            },
13558        );
13559    }
13560
13561    #[cfg(feature = "javascript")]
13562    #[test]
13563    fn javascript_return_with_conditions() {
13564        check_metrics::<JavascriptParser>(
13565            "function m1(z) { return !(z >= 0); }\n\
13566             function m2(x) { return (((!x))); }\n\
13567             function m3(x, y) { return x && y; }\n",
13568            "foo.js",
13569            |metric| {
13570                // m1: 1 (`>=`). m2: 1 (walker unwraps to `x`).
13571                // m3: 2 (`&&` walker counts both terminals).
13572                assert_eq!(metric.abc.conditions_sum(), 4);
13573                insta::assert_json_snapshot!(metric.abc);
13574            },
13575        );
13576    }
13577
13578    #[cfg(feature = "typescript")]
13579    #[test]
13580    fn typescript_if_boolean_literal_condition() {
13581        check_metrics::<TypescriptParser>(
13582            "function f() {\n\
13583             \x20   if (true) {}\n\
13584             \x20   if (!false) {}\n\
13585             \x20   while (true) {}\n\
13586             \x20   do {} while (false);\n\
13587             }\n",
13588            "foo.ts",
13589            |metric| {
13590                assert_eq!(metric.abc.conditions_sum(), 4);
13591                insta::assert_json_snapshot!(metric.abc);
13592            },
13593        );
13594    }
13595
13596    #[cfg(feature = "typescript")]
13597    #[test]
13598    fn typescript_methods_arguments_with_conditions() {
13599        check_metrics::<TypescriptParser>(
13600            "function f(a: boolean, b: boolean) {\n\
13601             \x20   m(a, b);\n\
13602             \x20   m(!a, !b);\n\
13603             }\n",
13604            "foo.ts",
13605            |metric| {
13606                assert_eq!(metric.abc.branches_sum(), 2);
13607                assert_eq!(metric.abc.conditions_sum(), 2);
13608                insta::assert_json_snapshot!(metric.abc);
13609            },
13610        );
13611    }
13612
13613    #[cfg(feature = "typescript")]
13614    #[test]
13615    fn typescript_return_with_conditions() {
13616        check_metrics::<TypescriptParser>(
13617            "function m1(z: number): boolean { return !(z >= 0); }\n\
13618             function m2(x: boolean): boolean { return (((!x))); }\n\
13619             function m3(x: boolean, y: boolean): boolean { return x && y; }\n",
13620            "foo.ts",
13621            |metric| {
13622                assert_eq!(metric.abc.conditions_sum(), 4);
13623                insta::assert_json_snapshot!(metric.abc);
13624            },
13625        );
13626    }
13627
13628    #[cfg(feature = "typescript")]
13629    #[test]
13630    fn tsx_if_boolean_literal_condition() {
13631        check_metrics::<TsxParser>(
13632            "function f() {\n\
13633             \x20   if (true) {}\n\
13634             \x20   if (!false) {}\n\
13635             \x20   while (true) {}\n\
13636             \x20   do {} while (false);\n\
13637             }\n",
13638            "foo.tsx",
13639            |metric| {
13640                assert_eq!(metric.abc.conditions_sum(), 4);
13641                insta::assert_json_snapshot!(metric.abc);
13642            },
13643        );
13644    }
13645
13646    #[cfg(feature = "typescript")]
13647    #[test]
13648    fn tsx_methods_arguments_with_conditions() {
13649        check_metrics::<TsxParser>(
13650            "function f(a: boolean, b: boolean) {\n\
13651             \x20   m(a, b);\n\
13652             \x20   m(!a, !b);\n\
13653             }\n",
13654            "foo.tsx",
13655            |metric| {
13656                assert_eq!(metric.abc.branches_sum(), 2);
13657                assert_eq!(metric.abc.conditions_sum(), 2);
13658                insta::assert_json_snapshot!(metric.abc);
13659            },
13660        );
13661    }
13662
13663    #[cfg(feature = "typescript")]
13664    #[test]
13665    fn tsx_return_with_conditions() {
13666        check_metrics::<TsxParser>(
13667            "function m1(z: number): boolean { return !(z >= 0); }\n\
13668             function m2(x: boolean): boolean { return (((!x))); }\n\
13669             function m3(x: boolean, y: boolean): boolean { return x && y; }\n",
13670            "foo.tsx",
13671            |metric| {
13672                assert_eq!(metric.abc.conditions_sum(), 4);
13673                insta::assert_json_snapshot!(metric.abc);
13674            },
13675        );
13676    }
13677
13678    #[cfg(feature = "mozjs")]
13679    #[test]
13680    fn mozjs_jsx_elements_are_not_conditions() {
13681        // #1297 in the second expansion of `js_abc_compute!`. The
13682        // vendored mozjs fork carries the same three JSX productions as
13683        // upstream tree-sitter-javascript with its own `kind_id`s, so
13684        // the gate is a distinct instantiation and needs its own
13685        // fixture — a passing JavaScript test says nothing about the
13686        // macro's other expansion. Same shape and same discriminating
13687        // numbers as `javascript_jsx_elements_are_not_conditions`: 8
13688        // pre-fix, 1 after.
13689        check_metrics::<MozjsParser>(
13690            "function f(a, b) {
13691                if (a < b) { return <div className=\"x\"><span>hi</span></div>; }
13692                return <br />;
13693            }",
13694            "foo.js",
13695            |metric| {
13696                assert_eq!(metric.abc.conditions_sum(), 1);
13697                // Fixture-presence anchor: the `className="x"` attribute
13698                // `=` is this fixture's only assignment, so the test
13699                // fails if a later edit drops the JSX. Without it the
13700                // surviving `a < b` alone also scores 1, and the
13701                // conditions assertion would pass while proving nothing.
13702                assert_eq!(metric.abc.assignments_sum(), 1);
13703            },
13704        );
13705    }
13706
13707    #[cfg(feature = "mozjs")]
13708    #[test]
13709    fn mozjs_if_boolean_literal_condition() {
13710        check_metrics::<MozjsParser>(
13711            "function f() {\n\
13712             \x20   if (true) {}\n\
13713             \x20   if (!false) {}\n\
13714             \x20   while (true) {}\n\
13715             \x20   do {} while (false);\n\
13716             }\n",
13717            "foo.js",
13718            |metric| {
13719                assert_eq!(metric.abc.conditions_sum(), 4);
13720                insta::assert_json_snapshot!(metric.abc);
13721            },
13722        );
13723    }
13724
13725    #[cfg(feature = "mozjs")]
13726    #[test]
13727    fn mozjs_methods_arguments_with_conditions() {
13728        check_metrics::<MozjsParser>(
13729            "function f(a, b) {\n\
13730             \x20   m(a, b);\n\
13731             \x20   m(!a, !b);\n\
13732             }\n",
13733            "foo.js",
13734            |metric| {
13735                assert_eq!(metric.abc.branches_sum(), 2);
13736                assert_eq!(metric.abc.conditions_sum(), 2);
13737                insta::assert_json_snapshot!(metric.abc);
13738            },
13739        );
13740    }
13741
13742    #[cfg(feature = "mozjs")]
13743    #[test]
13744    fn mozjs_return_with_conditions() {
13745        check_metrics::<MozjsParser>(
13746            "function m1(z) { return !(z >= 0); }\n\
13747             function m2(x) { return (((!x))); }\n\
13748             function m3(x, y) { return x && y; }\n",
13749            "foo.js",
13750            |metric| {
13751                assert_eq!(metric.abc.conditions_sum(), 4);
13752                insta::assert_json_snapshot!(metric.abc);
13753            },
13754        );
13755    }
13756
13757    // ----- JS / TS / Tsx / Mozjs unary-conditional walker -----
13758
13759    #[cfg(feature = "javascript")]
13760    #[test]
13761    fn javascript_if_multiple_conditions() {
13762        check_metrics::<JavascriptParser>(
13763            "function f(a, b, c, d) {\n\
13764             \x20   if (a || b || c || d) {}        // +4c\n\
13765             \x20   if (a && b && c) {}             // +3c\n\
13766             \x20   if (!a && !b) {}                // +2c\n\
13767             }\n",
13768            "foo.js",
13769            |metric| {
13770                assert_eq!(metric.abc.conditions_sum(), 9);
13771                insta::assert_json_snapshot!(metric.abc);
13772            },
13773        );
13774    }
13775
13776    #[cfg(feature = "javascript")]
13777    #[test]
13778    fn javascript_while_and_do_while_conditions() {
13779        check_metrics::<JavascriptParser>(
13780            "function f(a, b) {\n\
13781             \x20   while (a || b) {}              // +2c\n\
13782             \x20   do {} while (a && !b);         // +2c\n\
13783             }\n",
13784            "foo.js",
13785            |metric| {
13786                assert_eq!(metric.abc.conditions_sum(), 4);
13787                insta::assert_json_snapshot!(metric.abc);
13788            },
13789        );
13790    }
13791
13792    #[cfg(feature = "javascript")]
13793    #[test]
13794    fn javascript_short_circuit_with_boolean_literal_operand() {
13795        check_metrics::<JavascriptParser>(
13796            "function f(a) { return a && true; }\n",
13797            "foo.js",
13798            |metric| {
13799                assert_eq!(metric.abc.conditions_sum(), 2);
13800                insta::assert_json_snapshot!(metric.abc);
13801            },
13802        );
13803    }
13804
13805    #[cfg(feature = "typescript")]
13806    #[test]
13807    fn typescript_if_multiple_conditions() {
13808        check_metrics::<TypescriptParser>(
13809            "function f(a: boolean, b: boolean, c: boolean, d: boolean) {\n\
13810             \x20   if (a || b || c || d) {}        // +4c\n\
13811             \x20   if (a && b && c) {}             // +3c\n\
13812             \x20   if (!a && !b) {}                // +2c\n\
13813             }\n",
13814            "foo.ts",
13815            |metric| {
13816                assert_eq!(metric.abc.conditions_sum(), 9);
13817                insta::assert_json_snapshot!(metric.abc);
13818            },
13819        );
13820    }
13821
13822    #[cfg(feature = "typescript")]
13823    #[test]
13824    fn typescript_while_and_do_while_conditions() {
13825        check_metrics::<TypescriptParser>(
13826            "function f(a: boolean, b: boolean) {\n\
13827             \x20   while (a || b) {}              // +2c\n\
13828             \x20   do {} while (a && !b);         // +2c\n\
13829             }\n",
13830            "foo.ts",
13831            |metric| {
13832                assert_eq!(metric.abc.conditions_sum(), 4);
13833                insta::assert_json_snapshot!(metric.abc);
13834            },
13835        );
13836    }
13837
13838    #[cfg(feature = "typescript")]
13839    #[test]
13840    fn typescript_short_circuit_with_boolean_literal_operand() {
13841        check_metrics::<TypescriptParser>(
13842            "function f(a: boolean): boolean { return a && true; }\n",
13843            "foo.ts",
13844            |metric| {
13845                assert_eq!(metric.abc.conditions_sum(), 2);
13846                insta::assert_json_snapshot!(metric.abc);
13847            },
13848        );
13849    }
13850
13851    #[cfg(feature = "typescript")]
13852    #[test]
13853    fn tsx_if_multiple_conditions() {
13854        check_metrics::<TsxParser>(
13855            "function f(a: boolean, b: boolean, c: boolean, d: boolean) {\n\
13856             \x20   if (a || b || c || d) {}        // +4c\n\
13857             \x20   if (a && b && c) {}             // +3c\n\
13858             \x20   if (!a && !b) {}                // +2c\n\
13859             }\n",
13860            "foo.tsx",
13861            |metric| {
13862                assert_eq!(metric.abc.conditions_sum(), 9);
13863                insta::assert_json_snapshot!(metric.abc);
13864            },
13865        );
13866    }
13867
13868    #[cfg(feature = "typescript")]
13869    #[test]
13870    fn tsx_while_and_do_while_conditions() {
13871        check_metrics::<TsxParser>(
13872            "function f(a: boolean, b: boolean) {\n\
13873             \x20   while (a || b) {}              // +2c\n\
13874             \x20   do {} while (a && !b);         // +2c\n\
13875             }\n",
13876            "foo.tsx",
13877            |metric| {
13878                assert_eq!(metric.abc.conditions_sum(), 4);
13879                insta::assert_json_snapshot!(metric.abc);
13880            },
13881        );
13882    }
13883
13884    #[cfg(feature = "typescript")]
13885    #[test]
13886    fn tsx_short_circuit_with_boolean_literal_operand() {
13887        check_metrics::<TsxParser>(
13888            "function f(a: boolean): boolean { return a && true; }\n",
13889            "foo.tsx",
13890            |metric| {
13891                assert_eq!(metric.abc.conditions_sum(), 2);
13892                insta::assert_json_snapshot!(metric.abc);
13893            },
13894        );
13895    }
13896
13897    #[cfg(feature = "mozjs")]
13898    #[test]
13899    fn mozjs_if_multiple_conditions() {
13900        check_metrics::<MozjsParser>(
13901            "function f(a, b, c, d) {\n\
13902             \x20   if (a || b || c || d) {}        // +4c\n\
13903             \x20   if (a && b && c) {}             // +3c\n\
13904             \x20   if (!a && !b) {}                // +2c\n\
13905             }\n",
13906            "foo.js",
13907            |metric| {
13908                assert_eq!(metric.abc.conditions_sum(), 9);
13909                insta::assert_json_snapshot!(metric.abc);
13910            },
13911        );
13912    }
13913
13914    #[cfg(feature = "mozjs")]
13915    #[test]
13916    fn mozjs_while_and_do_while_conditions() {
13917        check_metrics::<MozjsParser>(
13918            "function f(a, b) {\n\
13919             \x20   while (a || b) {}              // +2c\n\
13920             \x20   do {} while (a && !b);         // +2c\n\
13921             }\n",
13922            "foo.js",
13923            |metric| {
13924                assert_eq!(metric.abc.conditions_sum(), 4);
13925                insta::assert_json_snapshot!(metric.abc);
13926            },
13927        );
13928    }
13929
13930    #[cfg(feature = "mozjs")]
13931    #[test]
13932    fn mozjs_short_circuit_with_boolean_literal_operand() {
13933        check_metrics::<MozjsParser>(
13934            "function f(a) { return a && true; }\n",
13935            "foo.js",
13936            |metric| {
13937                assert_eq!(metric.abc.conditions_sum(), 2);
13938                insta::assert_json_snapshot!(metric.abc);
13939            },
13940        );
13941    }
13942
13943    // ---------- Perl ABC tests ----------
13944
13945    #[cfg(feature = "perl")]
13946    #[test]
13947    fn perl_empty_unit_zero() {
13948        // Empty source produces zero ABC magnitude — pins the trait
13949        // wiring without exercising any compute branch.
13950        check_metrics::<PerlParser>("", "empty.pl", |metric| {
13951            assert_eq!(metric.abc.assignments_sum(), 0);
13952            assert_eq!(metric.abc.branches_sum(), 0);
13953            assert_eq!(metric.abc.conditions_sum(), 0);
13954            insta::assert_json_snapshot!(metric.abc);
13955        });
13956    }
13957
13958    #[cfg(feature = "perl")]
13959    #[test]
13960    fn perl_plain_and_compound_assignments_count() {
13961        // `my $x = 0` parses as a `binary_expression` with an `=`
13962        // token, so the initialiser counts (Perl has no equivalent of
13963        // the JS `const` initialiser-suppression rule). Each
13964        // assignment operator token contributes one assignment:
13965        // `=`, `=`, `+=`, `.=`, `**=` → A = 5. Two of those `=` come
13966        // from the `my $x = 0` initialiser and the later `$x = 5`
13967        // reassignment.
13968        check_metrics::<PerlParser>(
13969            "sub f { my $x = 0; $x = 5; $x += 2; $x .= \"a\"; $x **= 3; }",
13970            "foo.pl",
13971            |metric| {
13972                assert_eq!(metric.abc.assignments_sum(), 5);
13973                assert_eq!(metric.abc.branches_sum(), 0);
13974                assert_eq!(metric.abc.conditions_sum(), 0);
13975                insta::assert_json_snapshot!(metric.abc);
13976            },
13977        );
13978    }
13979
13980    #[cfg(feature = "perl")]
13981    #[test]
13982    fn perl_calls_are_branches() {
13983        // `foo()` parses as `call_expression_with_args_with_brackets`
13984        // wrapping an inner `call_expression_with_bareword(foo)`;
13985        // `bar 1, 2` wraps `bar` likewise under spaced-args; `shift`
13986        // appears as a standalone bareword. The bareword-inside-
13987        // wrapper case must NOT double-count — only the outer wrapper
13988        // contributes a branch. So B = 3 (foo, bar, shift), not 5.
13989        check_metrics::<PerlParser>(
13990            "sub f { foo(); bar 1, 2; my $a = shift; }",
13991            "foo.pl",
13992            |metric| {
13993                // shift's `my $a = shift` initialiser contributes one
13994                // assignment via the `=` token.
13995                assert_eq!(metric.abc.assignments_sum(), 1);
13996                assert_eq!(metric.abc.branches_sum(), 3);
13997                assert_eq!(metric.abc.conditions_sum(), 0);
13998                insta::assert_json_snapshot!(metric.abc);
13999            },
14000        );
14001    }
14002
14003    #[cfg(feature = "perl")]
14004    #[test]
14005    fn perl_method_invocation_counts_as_branch() {
14006        // `$obj->method(...)` parses as `method_invocation`. Any
14007        // arrow-dispatch counts as one branch regardless of how the
14008        // arguments are passed.
14009        check_metrics::<PerlParser>(
14010            "sub f { my $obj = shift; $obj->run($x); $obj->ping; }",
14011            "foo.pl",
14012            |metric| {
14013                // `my $obj = shift` → A=1, B=1 (shift bareword).
14014                // `$obj->run($x)` and `$obj->ping` → 2 more branches.
14015                assert_eq!(metric.abc.assignments_sum(), 1);
14016                assert_eq!(metric.abc.branches_sum(), 3);
14017                assert_eq!(metric.abc.conditions_sum(), 0);
14018                insta::assert_json_snapshot!(metric.abc);
14019            },
14020        );
14021    }
14022
14023    // #1297's own sweep cleared Perl, and it was wrong. `<STDIN>` does
14024    // lex as a single `standard_input` token and scores 0, which is
14025    // what the issue measured — but the filehandle and lexical-handle
14026    // readlines do not: `<FH>` is `standard_input_to_identifier` and
14027    // `<$fh>` is `standard_input_to_variable`, each a plain three-token
14028    // sequence whose brackets are the same bare `<` / `>` a comparison
14029    // uses, and the arm was ungated. Each scored two phantom
14030    // conditions.
14031    //
14032    // All three readline spellings against two genuine comparisons, so
14033    // every mis-aim lands on its own number: 6 pre-fix, 2 once the arm
14034    // allows `BinaryExpression`, 0 if it allows the wrong parent or the
14035    // fixture stops parsing. The assertion is also the grammar-dispatch
14036    // §8 pin — `cyclomatic()` is 3 on this space, so decisions is 2 and
14037    // the two counts agree exactly.
14038    #[cfg(feature = "perl")]
14039    #[test]
14040    fn perl_readline_angle_brackets_are_not_conditions() {
14041        check_func_space::<PerlParser, _>(
14042            "sub g {\n\
14043                 my ($a, $b, $fh) = @_;\n\
14044                 my $l = <FH>;\n\
14045                 my $m = <$fh>;\n\
14046                 my $n = <STDIN>;\n\
14047                 if ($a < $b) { return 1; }\n\
14048                 if ($a > $b) { return 2; }\n\
14049                 return 0;\n\
14050             }",
14051            "foo.pl",
14052            |space| {
14053                assert_deepest_conditions_match_cyclomatic(&space, 2);
14054                // Fixture-presence anchor: the three readlines are
14055                // three of this sub's four assignments (the fourth is
14056                // the `my (...) = @_` unpack), so dropping them from
14057                // the fixture fails here rather than leaving a test
14058                // that still reports 2 conditions and proves nothing.
14059                assert_eq!(space.metrics.abc.assignments_sum(), 4);
14060            },
14061        );
14062    }
14063
14064    #[cfg(feature = "perl")]
14065    #[test]
14066    fn perl_numeric_and_string_comparisons_count_conditions() {
14067        // Numeric ops `==`, `!=`, `<`, `>`, `<=`, `>=`, `<=>` and
14068        // string ops `eq`, `ne`, `lt`, `gt`, `le`, `ge`, `cmp` each
14069        // fire once per token. The sample below uses one of each →
14070        // C = 14. No assignments, no branches.
14071        check_metrics::<PerlParser>(
14072            "sub f {\n\
14073                 my $r;\n\
14074                 $r = $a == $b;\n\
14075                 $r = $a != $b;\n\
14076                 $r = $a <  $b;\n\
14077                 $r = $a >  $b;\n\
14078                 $r = $a <= $b;\n\
14079                 $r = $a >= $b;\n\
14080                 $r = $a <=> $b;\n\
14081                 $r = $a eq $b;\n\
14082                 $r = $a ne $b;\n\
14083                 $r = $a lt $b;\n\
14084                 $r = $a gt $b;\n\
14085                 $r = $a le $b;\n\
14086                 $r = $a ge $b;\n\
14087                 $r = $a cmp $b;\n\
14088             }",
14089            "foo.pl",
14090            |metric| {
14091                // 15 `=` tokens: one declaration `my $r` (no `=`),
14092                // then 14 `$r = …` plus there's no `=` in `my $r;`.
14093                // Actually: `my $r;` has no `=`; the 14 `$r = …` are
14094                // 14 `=` tokens. So A=14, C=14.
14095                assert_eq!(metric.abc.assignments_sum(), 14);
14096                assert_eq!(metric.abc.branches_sum(), 0);
14097                assert_eq!(metric.abc.conditions_sum(), 14);
14098                insta::assert_json_snapshot!(metric.abc);
14099            },
14100        );
14101    }
14102
14103    #[cfg(feature = "perl")]
14104    #[test]
14105    fn perl_short_circuit_not_counted_directly_ternary_counts() {
14106        // `&&`, `||`, `//`, low-precedence `and`, `or` are NOT
14107        // counted as conditions on their own (Fitzpatrick Rule 5;
14108        // #395) — instead each operand is counted as a unary
14109        // conditional by the walker (Rule 9; #403). `xor` evaluates
14110        // both operands, so it is a value like `^` and, outside a
14111        // slot, scores nothing (#1536). The pinned
14112        // tree-sitter-perl parses the low-precedence `and` as a
14113        // two-operand `unary_expression` rather than a
14114        // `binary_expression`; the walker reads the operands of either
14115        // since #1526, which is what moved this count from 10 to 14.
14116        // The low-precedence forms bind looser than `=`, so
14117        // `$r = $a and $b` is `($r = $a) and $b`, and its left operand
14118        // pays through the assignment's value.
14119        // Net: 5 chain lines × 2 operands + 0 for the `xor` line + 1
14120        // ternary node + 1 for the ternary's bare `$a` condition operand
14121        // (#1102) = 12.
14122        check_metrics::<PerlParser>(
14123            "sub f {\n\
14124                 my $r;\n\
14125                 $r = $a && $b;\n\
14126                 $r = $a || $b;\n\
14127                 $r = $a // $b;\n\
14128                 $r = $a and $b;\n\
14129                 $r = $a or  $b;\n\
14130                 $r = $a xor $b;\n\
14131                 $r = $a ? 1 : 2;\n\
14132             }",
14133            "foo.pl",
14134            |metric| {
14135                // 7 `=` tokens (one per reassignment line).
14136                assert_eq!(metric.abc.assignments_sum(), 7);
14137                assert_eq!(metric.abc.branches_sum(), 0);
14138                // 5 chain lines × 2 operands + 1 ternary node + 1 for
14139                // its bare `$a` condition operand = 12.
14140                assert_eq!(metric.abc.conditions_sum(), 12);
14141                insta::assert_json_snapshot!(metric.abc);
14142            },
14143        );
14144    }
14145
14146    // Issue #1102, Perl half. See
14147    // `cpp_ternary_operand_slots_count_as_unary_conditions` for the
14148    // rule. Like PHP, Perl's ABC dispatcher has no `?`-token arm — the
14149    // grammar does emit the token, but the `ternary_expression` node is
14150    // what carries the tally's +1. tree-sitter-perl names the branch
14151    // fields `true` / `false` rather than the C-family `consequence` /
14152    // `alternative`, so a copied C-family gate would match nothing.
14153    #[cfg(feature = "perl")]
14154    #[test]
14155    fn perl_ternary_operand_slots_count_as_unary_conditions() {
14156        // ternary (1) + condition `$a` (1) + `!$b` (1) + `!$c` (1) = 4.
14157        check_metrics::<PerlParser>("sub f { my $x = $a ? !$b : !$c; }", "foo.pl", |metric| {
14158            assert_eq!(metric.abc.conditions_sum(), 4);
14159        });
14160        // No-double-count pin: ternary (1) + `>` (1) = 2, unchanged by
14161        // the fix.
14162        check_metrics::<PerlParser>(
14163            "sub f { my $x = ($a > 0) ? $b : -$b; }",
14164            "foo.pl",
14165            |metric| assert_eq!(metric.abc.conditions_sum(), 2),
14166        );
14167        // A negated *condition*: the slot peels the `!`
14168        // (`perl_wrapper_operand`) and pays for `$a`, which does not
14169        // score itself. ternary (1) + `!$a` (1) = 2.
14170        check_metrics::<PerlParser>("sub f { my $x = !$a ? $b : $c; }", "foo.pl", |metric| {
14171            assert_eq!(metric.abc.conditions_sum(), 2);
14172        });
14173        // Nested: two ternary nodes plus the two bare-variable
14174        // conditions = 4.
14175        check_metrics::<PerlParser>(
14176            "sub f { my $x = $a ? ($b ? $c : $d) : $e; }",
14177            "foo.pl",
14178            |metric| assert_eq!(metric.abc.conditions_sum(), 4),
14179        );
14180    }
14181
14182    // Issue #1273. tree-sitter-perl 1.1.2 parses `$a > $b ? 1 : 2` as
14183    // `$a > ($b ? 1 : 2)`, and the ternary's condition slot then paid
14184    // for `$b`: 3 against JavaScript's 2 for `a > b ? 1 : 2`. Every
14185    // chaining comparison does it; each row must also equal its
14186    // parenthesised spelling, which parses correctly and so takes its
14187    // value from outside the fix.
14188    #[cfg(feature = "perl")]
14189    #[test]
14190    fn perl_comparison_conditioned_ternary_counts_the_comparison_once() {
14191        let operators = [
14192            "<", ">", "<=", ">=", "lt", "gt", "le", "ge", "==", "!=", "eq", "ne",
14193        ];
14194        for op in operators {
14195            let bare = format!("sub f {{ my $m = $a {op} $b ? 1 : 2; }}\n");
14196            let grouped = format!("sub f {{ my $m = ($a {op} $b) ? 1 : 2; }}\n");
14197            // The ternary (1) and the comparison token (1).
14198            assert_eq!(abc_conditions(LANG::Perl, &bare), 2, "`{op}`");
14199            assert_eq!(abc_conditions(LANG::Perl, &grouped), 2, "`({op})`");
14200        }
14201    }
14202
14203    // Issue #1273, the general shape: a chaining comparison swallows
14204    // every lower-precedence operator to its right, so a logical chain
14205    // misparses too — `$a > $b && $c` is `$a > ($b && $c)`, and the
14206    // chain walker paid for `$b`. Expected values are the JavaScript
14207    // twins (`a > b && c ? 1 : 2` = 3, …) or, where JavaScript has no
14208    // spelling, the parenthesised Perl one.
14209    #[cfg(feature = "perl")]
14210    #[test]
14211    fn perl_misparsed_comparison_operand_is_not_a_condition() {
14212        let cases = [
14213            // `<` `<` `?`; `($a < $b < $c) ? 1 : 2` is 3.
14214            ("my $m = $a < $b < $c ? 1 : 2;", 3),
14215            // `>` `?` `>` `?`, as `a > b ? (c > d ? 1 : 2) : 3`.
14216            ("my $m = $a > $b ? $c > $d ? 1 : 2 : 3;", 4),
14217            // Two ternaries and two comparisons, as the JS twin.
14218            ("if ($a > $b ? $c : $d) { } return $a == $b ? 1 : 0;", 4),
14219            // The chain is the ternary's condition: `>` `?` `$c`.
14220            ("my $m = $a > $b && $c ? 1 : 2;", 3),
14221            // The ternary hangs off the chain's right comparison.
14222            ("my $m = $x > 0 && $y > 0 ? 1 : 2;", 3),
14223            ("my $m = $x && $a > $b ? 1 : 2;", 3),
14224            ("if ($a > $b && $c) { }", 2),
14225            ("if ($a eq $b && $c eq $d) { }", 2),
14226            // `&&` is the first operand of `||`: the climb crosses it.
14227            ("if ($a > $b && $c || $d) { }", 3),
14228            ("if ($a > $b && $c && $d) { }", 3),
14229            // `and` is a two-operand `unary_expression`, which the
14230            // climb crosses too: `(($a > $b) && $c) and $d` is 3.
14231            ("if ($a > $b and $c) { }", 2),
14232            ("if ($a > $b && $c and $d) { }", 3),
14233            // A comment between the operator and the ternary is an
14234            // `extra` sibling, not the operator.
14235            ("my $m = $a > # c\n $b ? 1 : 2;", 2),
14236        ];
14237        for (body, expected) in cases {
14238            let src = format!("sub f {{ {body} }}\n");
14239            assert_eq!(abc_conditions(LANG::Perl, &src), expected, "`{body}`");
14240        }
14241    }
14242
14243    // Issue #1273: shapes the workaround must leave alone. The explicit
14244    // `$a > ($b ? 1 : 2)` puts an `array` between the comparison and
14245    // the ternary, so `$b` really is a condition: 3, as
14246    // `a > (b ? 1 : 2)` in JavaScript. `<=>`, `cmp` and `=~` do not
14247    // chain and parse correctly; those rows pin that grammar fact, not
14248    // the workaround's operator list — adding `<=>` to the list moves
14249    // nothing, since no tree ever puts a ternary straight after it.
14250    #[cfg(feature = "perl")]
14251    #[test]
14252    fn perl_correctly_parsed_ternaries_keep_their_condition_slot() {
14253        let cases = [
14254            ("my $m = ($a > $b) ? 1 : 2;", 2),
14255            ("my $m = $a <=> $b ? 1 : 2;", 2),
14256            ("my $m = $a cmp $b ? 1 : 2;", 2),
14257            ("my $m = $a =~ /x/ ? 1 : 2;", 2),
14258            ("my $m = $a > ($b ? 1 : 2);", 3),
14259            ("my $m = $a ? !$b : !$c;", 4),
14260            ("my $m = $a && $b ? 1 : 2;", 3),
14261        ];
14262        for (body, expected) in cases {
14263            let src = format!("sub f {{ {body} }}\n");
14264            assert_eq!(abc_conditions(LANG::Perl, &src), expected, "`{body}`");
14265        }
14266    }
14267
14268    #[cfg(feature = "perl")]
14269    #[test]
14270    fn perl_elsif_and_else_count_conditions() {
14271        // `if (… == …) { … } elsif (… < …) { … } else { … }` →
14272        // 2 comparison tokens (`==`, `<`), plus `elsif_clause` and
14273        // `else_clause` each + 1 → C = 4. Branches: 0 (only
14274        // assignments). Assignments: just the `=` initialisers /
14275        // reassignments — there are 4 here (`$x` init plus three
14276        // `$x = …` reassigns).
14277        check_metrics::<PerlParser>(
14278            "sub f {\n\
14279                 my $x = 0;\n\
14280                 if ($a == $b) {\n\
14281                     $x = 1;\n\
14282                 } elsif ($a < $b) {\n\
14283                     $x = 2;\n\
14284                 } else {\n\
14285                     $x = 3;\n\
14286                 }\n\
14287             }",
14288            "foo.pl",
14289            |metric| {
14290                assert_eq!(metric.abc.assignments_sum(), 4);
14291                assert_eq!(metric.abc.branches_sum(), 0);
14292                assert_eq!(metric.abc.conditions_sum(), 4);
14293                insta::assert_json_snapshot!(metric.abc);
14294            },
14295        );
14296    }
14297
14298    #[cfg(feature = "perl")]
14299    #[test]
14300    fn perl_regex_match_operators_count_conditions() {
14301        // `=~` and `!~` are pattern-match operators; we count both
14302        // as conditions because they evaluate the regex match in a
14303        // boolean context.
14304        check_metrics::<PerlParser>(
14305            "sub f { my $s = shift; my $m = $s =~ /foo/; my $n = $s !~ /bar/; }",
14306            "foo.pl",
14307            |metric| {
14308                // 3 `=` tokens, 0 branches except `shift` bareword.
14309                assert_eq!(metric.abc.assignments_sum(), 3);
14310                assert_eq!(metric.abc.branches_sum(), 1);
14311                assert_eq!(metric.abc.conditions_sum(), 2);
14312                insta::assert_json_snapshot!(metric.abc);
14313            },
14314        );
14315    }
14316
14317    #[cfg(feature = "perl")]
14318    #[test]
14319    fn perl_complex_function_abc() {
14320        // Mixed program exercising every category. Computed
14321        // expected:
14322        //   Assignments: `my $i = 0` (1), `$i++` is a unary
14323        //     increment — Perl's grammar emits `PLUSPLUS` not an `=`
14324        //     operator, so it does NOT count under the operator-
14325        //     token rule. The for-loop's `$i++` is similarly
14326        //     uncounted.
14327        //     Total A: 1 from `my $i = 0`, 1 from `$total += $i`
14328        //     (the `+=` token) → A = 2.
14329        //   Branches: `do_work($i)` → 1; `print "done\n"` is a
14330        //     call_expression_with_spaced_args → 1; `return $total`
14331        //     uses the `return` keyword not a call → 0. B = 2.
14332        //   Conditions: `$i < 10` (`<`) → 1; `$i % 2 == 0` (`==`) →
14333        //     1; `else_clause` → 1. C = 3.
14334        check_metrics::<PerlParser>(
14335            "sub run {\n\
14336                 my $total = 0;\n\
14337                 for (my $i = 0; $i < 10; $i++) {\n\
14338                     if ($i % 2 == 0) {\n\
14339                         do_work($i);\n\
14340                     } else {\n\
14341                         $total += $i;\n\
14342                     }\n\
14343                 }\n\
14344                 print \"done\\n\";\n\
14345                 return $total;\n\
14346             }",
14347            "foo.pl",
14348            |metric| {
14349                // `my $total = 0` is one `=`; `my $i = 0` is another
14350                // `=`; `$total += $i` is one `+=`. Total = 3.
14351                assert_eq!(metric.abc.assignments_sum(), 3);
14352                assert_eq!(metric.abc.branches_sum(), 2);
14353                assert_eq!(metric.abc.conditions_sum(), 3);
14354                insta::assert_json_snapshot!(metric.abc);
14355            },
14356        );
14357    }
14358
14359    #[cfg(feature = "perl")]
14360    #[test]
14361    fn perl_if_multiple_conditions() {
14362        // Fitzpatrick Rule 9 walker (issue #403): each operand of a
14363        // `&&` / `||` / `//` / `and` / `or` chain is one
14364        // condition. ScalarVariable operands ($a, $b, …) qualify as
14365        // terminal-bool kinds for the walker.
14366        check_metrics::<PerlParser>(
14367            "sub f {\n\
14368                 my ($a, $b, $c, $d) = @_;\n\
14369                 if ($a || $b || $c || $d) { return 1; }    # +4c\n\
14370                 if ($a && $b && $c) { return 2; }          # +3c\n\
14371                 if (!$a && !$b) { return 3; }              # +2c\n\
14372                 return 0;\n\
14373             }",
14374            "foo.pl",
14375            |metric| {
14376                assert_eq!(metric.abc.conditions_sum(), 9);
14377                insta::assert_json_snapshot!(metric.abc);
14378            },
14379        );
14380    }
14381
14382    #[cfg(feature = "perl")]
14383    #[test]
14384    fn perl_while_and_until_conditions() {
14385        // Perl has no `do { ... } while(cond);` shape in this grammar
14386        // — `while` and `until` are the loop forms with a condition
14387        // slot. The walker fires on each `&&` / `||` token inside
14388        // those headers.
14389        check_metrics::<PerlParser>(
14390            "sub f {\n\
14391                 my ($a, $b) = @_;\n\
14392                 while ($a || $b) { last; }            # +2c\n\
14393                 until ($a && !$b) { last; }           # +2c\n\
14394             }",
14395            "foo.pl",
14396            |metric| {
14397                assert_eq!(metric.abc.conditions_sum(), 4);
14398                insta::assert_json_snapshot!(metric.abc);
14399            },
14400        );
14401    }
14402
14403    #[cfg(feature = "perl")]
14404    #[test]
14405    fn perl_for_header_condition_slot_counts_unary_conditions() {
14406        // The Perl half of #1276. The C-style `for` header's condition
14407        // slot is a Rule 9 unary condition like the `if` slot: a bare
14408        // `$ok`, a negated `!$ok` and a parenthesised `($ok)` each count
14409        // one, an empty header and a `foreach` count zero, and a
14410        // comparison-shaped `$i < $n` stays at the one the `<` arm
14411        // already counts. Every C-style row is the three-clause spelling
14412        // because tree-sitter-perl does not parse an empty initializer.
14413        let cases = [
14414            ("for (my $i = 0; $ok; $i++) { }", 1),
14415            ("for (my $i = 0; !$ok; $i++) { }", 1),
14416            ("for (my $i = 0; ($ok); $i++) { }", 1),
14417            ("for (my $i = 0; $i < $n; $i++) { }", 1),
14418            ("for (my $i = 0; $ok && $j; $i++) { }", 2),
14419            ("for (;;) { last; }", 0),
14420            ("for my $x (@l) { }", 0),
14421        ];
14422        let mut ran = 0;
14423        for (body, expected) in cases {
14424            let src = format!("sub f {{ {body} }}\n");
14425            assert_eq!(abc_conditions(LANG::Perl, &src), expected, "`{body}`");
14426            ran += 1;
14427        }
14428        assert_eq!(ran, cases.len());
14429        assert!(cases.iter().any(|&(_, n)| n == 0));
14430        assert!(cases.iter().any(|&(_, n)| n == 2));
14431
14432        // The slot agrees with the `if` slot for every shape, which is
14433        // the property the fix restores; the reference is non-degenerate
14434        // by the table above.
14435        for condition in ["$ok", "!$ok", "($ok)", "$ok && $j"] {
14436            let in_for = format!("sub f {{ for (my $i = 0; {condition}; $i++) {{ }} }}\n");
14437            let in_if = format!("sub f {{ if ({condition}) {{ }} }}\n");
14438            assert_eq!(
14439                abc_conditions(LANG::Perl, &in_for),
14440                abc_conditions(LANG::Perl, &in_if),
14441                "`{condition}` must score alike in a `for` header and an `if`",
14442            );
14443        }
14444    }
14445
14446    #[cfg(feature = "perl")]
14447    #[test]
14448    fn perl_short_circuit_counts_scalar_variable_operands() {
14449        // `$a && $b` reports 2 conditions — one walker count per
14450        // `ScalarVariable` operand. Renamed from the cross-language
14451        // `_with_boolean_literal_operand` convention because Perl has
14452        // no readily-grammar-exposed boolean literal in an `&&`
14453        // operand slot at the pinned grammar version (the `Boolean`
14454        // kind only fires on the `boolean` pragma's named constants,
14455        // not bareword `1` / `0`). Two scalar variables are the
14456        // grammar-stable terminal-set witness for Perl.
14457        check_metrics::<PerlParser>(
14458            "sub f { my ($a) = @_; return $a && $b; }\n",
14459            "foo.pl",
14460            |metric| {
14461                assert_eq!(metric.abc.conditions_sum(), 2);
14462                insta::assert_json_snapshot!(metric.abc);
14463            },
14464        );
14465    }
14466
14467    #[cfg(feature = "perl")]
14468    #[test]
14469    fn perl_array_in_binary_operand_descends_to_scalar_context_value() {
14470        // Regression test for the code-review findings on the
14471        // Phase-2B Perl walker:
14472        //   - Pre-fix-A: the former `perl_inspect_container` descended `Array`
14473        //     via `node.child(1)` — the FIRST element — wrongly
14474        //     attributing `$x` for `($x, $y)` (semantically `$y`
14475        //     is the scalar-context value).
14476        //   - Fix-A (the `array_is_paren` guard, 5db8078): dropped
14477        //     Array-as-paren entirely in `BinaryExpression` operand
14478        //     contexts to avoid the wrong attribution — but
14479        //     regressed `$a || ($x)` (single paren-grouped operand)
14480        //     to C=1 instead of 2.
14481        //   - Fix-B (this change): keeps Array-as-paren unconditional
14482        //     but descends via the LAST named child. `$a || ($x)`
14483        //     reaches `$x` (count both operands → 2);
14484        //     `$a || ($x, $y)` reaches `$y` (count `$a` + `$y` →
14485        //     still 2, matching Fitzpatrick Rule 7 "one per
14486        //     operand"); `if ($a)` still reaches `$a` (single-
14487        //     element grouping → 1).
14488        check_metrics::<PerlParser>(
14489            "sub f { my ($a, $x, $y) = @_;\n\
14490             \x20   my $r = $a || ($x, $y);    # +2c: $a + last-named $y\n\
14491             \x20   my $s = $a || ($x);        # +2c: $a + only-named $x\n\
14492             \x20   $r + $s;\n\
14493             }\n",
14494            "foo.pl",
14495            |metric| {
14496                // 2 + 2 = 4 unary conditions from the two `||`s.
14497                assert_eq!(metric.abc.conditions_sum(), 4);
14498                insta::assert_json_snapshot!(metric.abc);
14499            },
14500        );
14501    }
14502
14503    #[cfg(feature = "perl")]
14504    #[test]
14505    fn perl_if_scalar_variable_condition() {
14506        // Renamed from the cross-language
14507        // `_if_boolean_literal_condition` convention because
14508        // Perl has no readily-grammar-exposed boolean literal in
14509        // an `if (cond)` slot at the pinned grammar version:
14510        // tree-sitter-perl's `Boolean` kind only fires for the
14511        // `boolean` pragma's named constants (not bareword `1` /
14512        // `0`, which surface as `Integer` / not in the
14513        // terminal-bool set). A scalar-variable condition is the
14514        // grammar-stable witness — `if ($a)` reaches
14515        // `scalar_variable` via the `Array` paren unwrap.
14516        check_metrics::<PerlParser>(
14517            "sub f { my ($a) = @_; if ($a) { return 1; } }\n",
14518            "foo.pl",
14519            |metric| {
14520                assert_eq!(metric.abc.conditions_sum(), 1);
14521                insta::assert_json_snapshot!(metric.abc);
14522            },
14523        );
14524    }
14525
14526    #[cfg(feature = "perl")]
14527    #[test]
14528    fn perl_methods_arguments_with_conditions() {
14529        // `call(!$a, !$b)` — argument list walker counts each
14530        // unary-conditional argument once. Cannot use `m(...)` as
14531        // the function name — tree-sitter-perl parses `m(...)` as
14532        // the regex-match operator, not a function call.
14533        check_metrics::<PerlParser>(
14534            "sub f { my ($a, $b) = @_; call($a, $b); call(!$a, !$b); }\n",
14535            "foo.pl",
14536            |metric| {
14537                // Two calls × 1 branch each = 2 branches.
14538                // `call(!$a, !$b)` contributes 2 walker conditions
14539                // (one per `!`-wrapped scalar-variable argument);
14540                // `call($a, $b)` contributes 0 (bare-args don't
14541                // count via the Arguments walker — list_kind !=
14542                // BinaryExpression).
14543                assert_eq!(metric.abc.branches_sum(), 2);
14544                assert_eq!(metric.abc.conditions_sum(), 2);
14545                insta::assert_json_snapshot!(metric.abc);
14546            },
14547        );
14548    }
14549
14550    #[cfg(feature = "perl")]
14551    #[test]
14552    fn perl_return_with_conditions() {
14553        // `return !$a` reports 1 condition via the walker (unary
14554        // unwrap to scalar-variable terminal). `return $a` reports
14555        // 0 (no paren / unary wrap, has_boolean_content stays
14556        // false from ReturnExpression parent).
14557        check_metrics::<PerlParser>(
14558            "sub m1 { my ($z) = @_; return !($z); }\n\
14559             sub m2 { my ($x) = @_; return (((!$x))); }\n\
14560             sub m3 { my ($x, $y) = @_; return $x && $y; }\n",
14561            "foo.pl",
14562            |metric| {
14563                // m1: !($z) → walker on `!` unwraps paren to $z (1).
14564                // m2: (((!$x))) → walker unwraps three parens + one
14565                //     unary to $x (1).
14566                // m3: $x && $y → walker on `&&` counts both (2).
14567                // Sum: 4.
14568                assert_eq!(metric.abc.conditions_sum(), 4);
14569                insta::assert_json_snapshot!(metric.abc);
14570            },
14571        );
14572    }
14573
14574    // A bare `/re/` or `m{re}` is a match against `$_`, a relational
14575    // operator spelled with no operator token, so it scores by use
14576    // (#1467, the rule #1461 applied to five other languages). Each
14577    // `*_out` member and `ret` / `notm` / `grp` / `arg` / `smart` was 0
14578    // before the arm: only a boolean slot reached the pattern, through
14579    // the former `perl_bool_terminal_kinds!()`. `mbare_out` is the `m{}` spelling, a
14580    // sibling kind rather than an alias. `smart` levels a smartmatch
14581    // with its `=~` sibling: no arm counts `~~`, so the pattern does.
14582    //
14583    // The in-slot members are the §5 check that the slot stopped
14584    // counting the pattern in the change that gave it an arm: each
14585    // reads what it read before. `bound_out` / `nbound` pin the gate on
14586    // `=~` / `!~` — the operator token scores, the pattern under it does
14587    // not — and `ctrl_out` is the `==` control the rest level with.
14588    // `s///`, `tr///` and `qr//` are not matches and stay at 0; the
14589    // bound `s///` / `tr///` agree (#1540, the test below).
14590    #[cfg(feature = "perl")]
14591    #[test]
14592    fn perl_bare_match_is_a_condition_by_use() {
14593        check_func_space::<PerlParser, _>(
14594            "sub bare_out { my $r = /^#/; return $r; }\n\
14595             sub mbare_out { my $r = m{^#}; return $r; }\n\
14596             sub ret { return /x/; }\n\
14597             sub notm { my $r = !/x/; return $r; }\n\
14598             sub grp { my @a = @_; return grep { /x/ } @a; }\n\
14599             sub arg { foo(/x/); }\n\
14600             sub smart { my ($x) = @_; my $r = $x ~~ /re/; return $r; }\n\
14601             sub bare_in { if (/^#/) { return 1; } return 0; }\n\
14602             sub mbare_in { unless (m{^#}) { return 1; } return 0; }\n\
14603             sub modif { return 0 unless /x/; return 1; }\n\
14604             sub andy { my ($y) = @_; return /x/ && $y; }\n\
14605             sub oror { return /a/ || /b/; }\n\
14606             sub tern { return /x/ ? 1 : 0; }\n\
14607             sub forc { for (my $i = 0; /x/; $i++) { } }\n\
14608             sub bound_out { my ($x) = @_; my $r = ($x =~ /^#/); return $r; }\n\
14609             sub nbound { my ($x) = @_; my $r = ($x !~ /^#/); return $r; }\n\
14610             sub ctrl_out { my ($x) = @_; my $r = ($x == 1); return $r; }\n\
14611             sub subst { my $n = s/x/y/; return $n; }\n\
14612             sub tr1 { my $n = tr/a/b/; return $n; }\n\
14613             sub qrx { my $q = qr/x/; return $q; }\n",
14614            "foo.pl",
14615            |space| {
14616                assert_members_score(
14617                    &space,
14618                    &[
14619                        ("bare_out", 1, 1),
14620                        ("mbare_out", 1, 1),
14621                        ("ret", 1, 1),
14622                        // `!` adds nothing outside a slot; the match is
14623                        // the one condition.
14624                        ("notm", 1, 1),
14625                        ("grp", 1, 1),
14626                        ("arg", 1, 1),
14627                        ("smart", 1, 1),
14628                        ("bare_in", 1, 2),
14629                        ("mbare_in", 1, 2),
14630                        ("modif", 1, 2),
14631                        // One per `&&` / `||` operand, the match included.
14632                        ("andy", 2, 2),
14633                        ("oror", 2, 2),
14634                        // The ternary node plus its condition.
14635                        ("tern", 2, 2),
14636                        ("forc", 1, 2),
14637                        ("bound_out", 1, 1),
14638                        ("nbound", 1, 1),
14639                        ("ctrl_out", 1, 1),
14640                        ("subst", 0, 1),
14641                        ("tr1", 0, 1),
14642                        ("qrx", 0, 1),
14643                    ],
14644                );
14645            },
14646        );
14647    }
14648
14649    // A substitution or transliteration bound with `=~` / `!~` is the
14650    // same edit as its bare form on `$_`, so it scores by use exactly as
14651    // that twin does: 0 (#1540). Each `*_s` / `*_tr` / `*_y` row below
14652    // was 1 through the operator token before the gate; `count` (an
14653    // assigned count) and `sr` (a `/r` copy, a value) are uses too. A
14654    // match keeps its 1 whatever spells it — a pattern, `qr//`, or a
14655    // variable holding one. The `if_*` rows are the slot side: each pays
14656    // once, as its bare twin `if_s` does, because
14657    // `perl_condition_scores_itself` stops claiming a rewrite binding in
14658    // the same change; `tern_s` and `neg_s` reach that predicate through
14659    // a ternary condition and a `!`-proved `return` operand.
14660    #[cfg(feature = "perl")]
14661    #[test]
14662    fn perl_bound_rewrite_scores_like_its_bare_twin() {
14663        check_func_space::<PerlParser, _>(
14664            "sub ctrl { my ($x) = @_; $x; return $x; }\n\
14665             sub m_bare { my ($x) = @_; /re/; return $x; }\n\
14666             sub m_bound { my ($x) = @_; $x =~ /re/; return $x; }\n\
14667             sub m_nbound { my ($x) = @_; $x !~ /re/; return $x; }\n\
14668             sub m_qr { my ($x) = @_; $x =~ qr/re/; return $x; }\n\
14669             sub m_var { my ($x, $q) = @_; $x =~ $q; return $x; }\n\
14670             sub s_bare { my ($x) = @_; s/a/b/; return $x; }\n\
14671             sub s_bound { my ($x) = @_; $x =~ s/a/b/; return $x; }\n\
14672             sub s_nbound { my ($x) = @_; $x !~ s/a/b/; return $x; }\n\
14673             sub tr_bare { my ($x) = @_; tr/a/b/; return $x; }\n\
14674             sub tr_bound { my ($x) = @_; $x =~ tr/a/b/; return $x; }\n\
14675             sub tr_nbound { my ($x) = @_; $x !~ tr/a/b/; return $x; }\n\
14676             sub y_bound { my ($x) = @_; $x =~ y/a/b/; return $x; }\n\
14677             sub count { my ($x) = @_; my $n = ($x =~ s/a/b/g); return $x; }\n\
14678             sub sr { my ($x) = @_; my $r = $x =~ s/a/b/r; return $r; }\n\
14679             sub if_s { my ($x) = @_; if (s/a/b/) { g(); } return $x; }\n\
14680             sub if_m { my ($x) = @_; if ($x =~ /re/) { g(); } return $x; }\n\
14681             sub if_bs { my ($x) = @_; if ($x =~ s/a/b/) { g(); } return $x; }\n\
14682             sub if_nbs { my ($x) = @_; if ($x !~ s/a/b/) { g(); } return $x; }\n\
14683             sub if_btr { my ($x) = @_; if ($x =~ tr/a/b/) { g(); } return $x; }\n\
14684             sub if_by { my ($x) = @_; if ($x =~ y/a/b/) { g(); } return $x; }\n\
14685             sub if_bsr { my ($x) = @_; if ($x =~ s/a/b/r) { g(); } return $x; }\n\
14686             sub tern_s { my ($x) = @_; return ($x =~ s/a/b/) ? 1 : 0; }\n\
14687             sub neg_s { my ($x) = @_; return !($x =~ s/a/b/); }\n",
14688            "foo.pl",
14689            |space| {
14690                assert_members_score(
14691                    &space,
14692                    &[
14693                        ("ctrl", 0, 1),
14694                        ("m_bare", 1, 1),
14695                        ("m_bound", 1, 1),
14696                        ("m_nbound", 1, 1),
14697                        ("m_qr", 1, 1),
14698                        ("m_var", 1, 1),
14699                        ("s_bare", 0, 1),
14700                        ("s_bound", 0, 1),
14701                        ("s_nbound", 0, 1),
14702                        ("tr_bare", 0, 1),
14703                        ("tr_bound", 0, 1),
14704                        ("tr_nbound", 0, 1),
14705                        ("y_bound", 0, 1),
14706                        ("count", 0, 1),
14707                        ("sr", 0, 1),
14708                        ("if_s", 1, 2),
14709                        ("if_m", 1, 2),
14710                        ("if_bs", 1, 2),
14711                        ("if_nbs", 1, 2),
14712                        ("if_btr", 1, 2),
14713                        ("if_by", 1, 2),
14714                        ("if_bsr", 1, 2),
14715                        // The ternary node plus its condition slot.
14716                        ("tern_s", 2, 2),
14717                        ("neg_s", 1, 1),
14718                    ],
14719                );
14720            },
14721        );
14722    }
14723
14724    // The pattern `split` takes first is its delimiter, not a match
14725    // against `$_`, so it is no condition (#1467) in any spelling: the
14726    // spaced and parenthesised call forms, the `m{}` pattern, and the
14727    // `CORE::` qualifier. Three discriminating members keep the exclusion
14728    // narrow: `spl2` puts the pattern in `split`'s *second* slot, where
14729    // it is an ordinary match whose result `split` receives, `grpe`
14730    // puts one first in a call that is not `split`, and `tsplit` puts
14731    // one first in a list whose grandparent holds a bare `split` without
14732    // being that call — the ternary node plus `$c` plus the match.
14733    #[cfg(feature = "perl")]
14734    #[test]
14735    fn perl_split_delimiter_is_not_a_condition() {
14736        use crate::{ParserTrait, Perl};
14737
14738        let src = "sub spl { my ($s) = @_; my @p = split /,/, $s; return @p; }\n\
14739                   sub splp { my ($s) = @_; my @p = split(/,/, $s); return @p; }\n\
14740                   sub splm { my ($s) = @_; my @p = split m{,}, $s; return @p; }\n\
14741                   sub splc { my ($s) = @_; my @p = CORE::split(/,/, $s); return @p; }\n\
14742                   sub spl2 { my @p = split ' ', /x/; return @p; }\n\
14743                   sub grpe { my @a = @_; return grep /x/, @a; }\n\
14744                   sub tsplit { my ($c) = @_; my @r = $c ? split : (/x/); return @r; }\n";
14745
14746        // Fixture census: each member but `tsplit` holds exactly one
14747        // pattern, as a direct argument of its call — so a zero below
14748        // is the exclusion at work and not a pattern the fixture lost.
14749        let parser = PerlParser::new(
14750            src.as_bytes().to_vec(),
14751            &std::path::PathBuf::from("foo.pl"),
14752            None,
14753        );
14754        let call_arguments = parser
14755            .root()
14756            .preorder()
14757            .filter(|n| {
14758                matches!(
14759                    n.kind_id().into(),
14760                    Perl::PatternMatcher | Perl::PatternMatcherM
14761                )
14762            })
14763            .filter(|n| {
14764                n.parent()
14765                    .and_then(|list| list.parent())
14766                    .is_some_and(|call| {
14767                        matches!(
14768                            call.kind_id().into(),
14769                            Perl::CallExpressionWithSpacedArgs
14770                                | Perl::CallExpressionWithArgsWithBrackets
14771                        )
14772                    })
14773            })
14774            .count();
14775        assert_eq!(call_arguments, 6, "one call-argument pattern per member");
14776
14777        check_func_space::<PerlParser, _>(src, "foo.pl", |space| {
14778            assert_members_score(
14779                &space,
14780                &[
14781                    ("spl", 0, 1),
14782                    ("splp", 0, 1),
14783                    ("splm", 0, 1),
14784                    ("splc", 0, 1),
14785                    ("spl2", 1, 1),
14786                    ("grpe", 1, 1),
14787                    ("tsplit", 3, 2),
14788                ],
14789            );
14790        });
14791    }
14792
14793    // tree-sitter-perl names its separators, so a trailing `,` or `=>`
14794    // was read as the last element of a condition list and paid a slot
14795    // of its own: `if ($a > 1,)` scored 2. `perl -MO=Deparse` reduces
14796    // every `*_sep` row to its `*_twin`, which is where the expected
14797    // values come from; the chain rows put the list inside a `&&`
14798    // operand, which reaches the same peel through
14799    // `perl_count_chain_operands`.
14800    #[cfg(feature = "perl")]
14801    #[test]
14802    fn perl_trailing_separator_scores_like_its_bare_twin() {
14803        check_func_space::<PerlParser, _>(
14804            "sub cmp_twin { my ($a) = @_; if ($a > 1) { g(); } }\n\
14805             sub cmp_comma { my ($a) = @_; if ($a > 1,) { g(); } }\n\
14806             sub cmp_fat { my ($a) = @_; if ($a > 1 =>) { g(); } }\n\
14807             sub var_twin { my ($a) = @_; if ($a) { g(); } }\n\
14808             sub var_comma { my ($a) = @_; if ($a,) { g(); } }\n\
14809             sub chain_twin { my ($a, $b) = @_; if (($a > 1) && $b) { g(); } }\n\
14810             sub chain_comma { my ($a, $b) = @_; if (($a > 1,) && $b) { g(); } }\n",
14811            "foo.pl",
14812            |space| {
14813                assert_members_score(
14814                    &space,
14815                    &[
14816                        ("cmp_twin", 1, 2),
14817                        ("cmp_comma", 1, 2),
14818                        ("cmp_fat", 1, 2),
14819                        ("var_twin", 1, 2),
14820                        ("var_comma", 1, 2),
14821                        ("chain_twin", 2, 3),
14822                        ("chain_comma", 2, 3),
14823                    ],
14824                );
14825            },
14826        );
14827    }
14828
14829    // A pattern's role is read on the outermost `( … )` around it, which
14830    // is the value inside it, so each `*_paren` row scores as its bare
14831    // `*_twin` (#1467, #1540); `perl -MO=Deparse` prints each pair
14832    // identically. The reversed binding is the other direction: in
14833    // `/foo/ =~ $x` the pattern is the *left* operand, so it first
14834    // matches `$_` and that result is bound (Deparse: `/foo/ =~ /$x/`) —
14835    // the match and the `=~` both pay, as in the explicit `rev_twin`.
14836    #[cfg(feature = "perl")]
14837    #[test]
14838    fn perl_pattern_role_reads_through_parentheses() {
14839        check_func_space::<PerlParser, _>(
14840            "sub bind_twin { my ($x) = @_; return $x =~ /foo/; }\n\
14841             sub bind_paren { my ($x) = @_; return $x =~ (/foo/); }\n\
14842             sub bind_parens_m { my ($x) = @_; return $x =~ (((m{foo}))); }\n\
14843             sub bind_list { my ($x, $y) = @_; return $x =~ (/foo/, $y); }\n\
14844             sub bind_list_twin { my ($x, $y) = @_; /foo/; return $x =~ $y; }\n\
14845             sub split_twin { my ($x) = @_; my @p = split(/foo/, $x); return @p; }\n\
14846             sub split_paren { my ($x) = @_; my @p = split((/foo/), $x); return @p; }\n\
14847             sub split_sole { my @p = split(/foo/); return @p; }\n\
14848             sub subst_twin { my ($x) = @_; $x =~ s/a/b/; return $x; }\n\
14849             sub subst_paren { my ($x) = @_; $x =~ (s/a/b/); return $x; }\n\
14850             sub rev { my ($x) = @_; return /foo/ =~ $x; }\n\
14851             sub rev_twin { my ($x) = @_; my $m = /foo/; return $m =~ $x; }\n",
14852            "foo.pl",
14853            |space| {
14854                assert_members_score(
14855                    &space,
14856                    &[
14857                        ("bind_twin", 1, 1),
14858                        ("bind_paren", 1, 1),
14859                        ("bind_parens_m", 1, 1),
14860                        // A comma operator: `/foo/` matches `$_` and `$y`
14861                        // is what binds, so the match pays as in the twin.
14862                        ("bind_list", 2, 1),
14863                        ("bind_list_twin", 2, 1),
14864                        ("split_twin", 0, 1),
14865                        ("split_paren", 0, 1),
14866                        // The pattern is the whole argument list, which is
14867                        // the call's own and no wrapper to climb through.
14868                        ("split_sole", 0, 1),
14869                        ("subst_twin", 0, 1),
14870                        ("subst_paren", 0, 1),
14871                        ("rev", 2, 1),
14872                        ("rev_twin", 2, 1),
14873                    ],
14874                );
14875            },
14876        );
14877    }
14878
14879    // Only the builtin `split` takes its first pattern as a delimiter:
14880    // `My::split` is a user sub, so the pattern it is handed is an
14881    // ordinary match, as it is for `My::other` (#1467). `CORE::split`
14882    // is the builtin by its full name.
14883    #[cfg(feature = "perl")]
14884    #[test]
14885    fn perl_split_exemption_is_the_builtin_only() {
14886        check_func_space::<PerlParser, _>(
14887            "sub mine { my ($x) = @_; my @p = My::split(/foo/, $x); return @p; }\n\
14888             sub other { my ($x) = @_; my @p = My::other(/foo/, $x); return @p; }\n\
14889             sub core { my ($x) = @_; my @p = CORE::split(/foo/, $x); return @p; }\n",
14890            "foo.pl",
14891            |space| {
14892                assert_members_score(&space, &[("mine", 1, 1), ("other", 1, 1), ("core", 0, 1)]);
14893            },
14894        );
14895    }
14896
14897    // ---------- Lua ABC tests ----------
14898
14899    #[cfg(feature = "lua")]
14900    #[test]
14901    fn lua_empty_unit_zero() {
14902        check_metrics::<LuaParser>("", "empty.lua", |metric| {
14903            assert_eq!(metric.abc.assignments_sum(), 0);
14904            assert_eq!(metric.abc.branches_sum(), 0);
14905            assert_eq!(metric.abc.conditions_sum(), 0);
14906            insta::assert_json_snapshot!(metric.abc);
14907        });
14908    }
14909
14910    #[cfg(feature = "lua")]
14911    #[test]
14912    fn lua_assignments_count_locals_and_plain() {
14913        // `local x = 0` wraps an `assignment_statement` under a
14914        // `variable_declaration`; the inner wrapper still counts.
14915        // Multi-target assignment `a, b = 1, 2` is a single
14916        // `assignment_statement` and contributes 1, NOT 2 — the
14917        // wrapper is the unit of counting (matches the Python rule:
14918        // one `Assignment` node, one assignment).
14919        check_metrics::<LuaParser>(
14920            "function f()\n\
14921                 local x = 0\n\
14922                 x = 1\n\
14923                 local a, b = 1, 2\n\
14924                 a, b = b, a\n\
14925             end",
14926            "foo.lua",
14927            |metric| {
14928                assert_eq!(metric.abc.assignments_sum(), 4);
14929                assert_eq!(metric.abc.branches_sum(), 0);
14930                assert_eq!(metric.abc.conditions_sum(), 0);
14931                insta::assert_json_snapshot!(metric.abc);
14932            },
14933        );
14934    }
14935
14936    #[cfg(feature = "lua")]
14937    #[test]
14938    fn lua_calls_are_branches() {
14939        // `print(x)`, `obj.m(x)`, `obj:m(x)`, `f(g(1))` — every
14940        // call form is a `function_call` node. The nested
14941        // `f(g(1))` counts as 2 branches (one per dispatch).
14942        check_metrics::<LuaParser>(
14943            "function r(x)\n\
14944                 print(x)\n\
14945                 obj.m(x)\n\
14946                 obj:m(x)\n\
14947                 return f(g(1))\n\
14948             end",
14949            "foo.lua",
14950            |metric| {
14951                assert_eq!(metric.abc.assignments_sum(), 0);
14952                assert_eq!(metric.abc.branches_sum(), 5);
14953                assert_eq!(metric.abc.conditions_sum(), 0);
14954                insta::assert_json_snapshot!(metric.abc);
14955            },
14956        );
14957    }
14958
14959    #[cfg(feature = "lua")]
14960    #[test]
14961    fn lua_comparisons_count_logical_ops_do_not() {
14962        // Each comparison token contributes one condition; `and` /
14963        // `or` are NOT counted on their own (Fitzpatrick Rule 5;
14964        // #395) — instead each operand is counted as a unary
14965        // conditional by the walker (Rule 9; #403). The two
14966        // `a and b` / `a or b` lines add 2 walker conditions each.
14967        check_metrics::<LuaParser>(
14968            "function f(a, b)\n\
14969                 local r\n\
14970                 r = a == b\n\
14971                 r = a ~= b\n\
14972                 r = a <  b\n\
14973                 r = a >  b\n\
14974                 r = a <= b\n\
14975                 r = a >= b\n\
14976                 r = a and b\n\
14977                 r = a or  b\n\
14978             end",
14979            "foo.lua",
14980            |metric| {
14981                // 8 `r = …` reassignments, plus `local r` (no `=`).
14982                assert_eq!(metric.abc.assignments_sum(), 8);
14983                assert_eq!(metric.abc.branches_sum(), 0);
14984                // 6 comparisons (+6) + 2 logical lines × 2 walker
14985                // operands (+4) = 10.
14986                assert_eq!(metric.abc.conditions_sum(), 10);
14987                insta::assert_json_snapshot!(metric.abc);
14988            },
14989        );
14990    }
14991
14992    // #1297: Lua 5.4 brackets a variable attribute with the same two
14993    // bare tokens a comparison uses, and the `<` / `>` arm had no gate,
14994    // so `local x <const> = 1` scored two conditions. Two attributes
14995    // against one genuine `a < b` separate every mis-aim: 5 pre-fix, 1
14996    // once the arm allows `BinaryExpression`, 0 if it allows the wrong
14997    // parent or the fixture stops parsing.
14998    //
14999    // No fixture-presence anchor is available, unlike the JSX and Perl
15000    // fixtures: once excluded, a Lua attribute contributes to no ABC
15001    // axis at all (`local x <const> = 1` and `local x = 1` score
15002    // identically on assignments, branches and conditions), and it
15003    // opens no space. Deleting the two attributes would leave this test
15004    // passing on the `a < b` alone. The coverage that the attributes
15005    // are reaching the arm is the revert test: removing the gate makes
15006    // this the only failing test in the suite.
15007    #[cfg(feature = "lua")]
15008    #[test]
15009    fn lua_variable_attributes_are_not_conditions() {
15010        check_metrics::<LuaParser>(
15011            "local function f(a, b)\n\
15012                 local x <const> = 1\n\
15013                 local y <close> = nil\n\
15014                 if a < b then return x end\n\
15015                 return y\n\
15016             end",
15017            "foo.lua",
15018            |metric| {
15019                assert_eq!(metric.abc.conditions_sum(), 1);
15020            },
15021        );
15022    }
15023
15024    #[cfg(feature = "lua")]
15025    #[test]
15026    fn lua_elseif_and_else_count_conditions() {
15027        // Each elseif / else arm of the if contributes one
15028        // condition, mirroring the Python rule.
15029        check_metrics::<LuaParser>(
15030            "function f(x)\n\
15031                 if x > 0 then\n\
15032                     return 1\n\
15033                 elseif x < 0 then\n\
15034                     return -1\n\
15035                 else\n\
15036                     return 0\n\
15037                 end\n\
15038             end",
15039            "foo.lua",
15040            |metric| {
15041                // Comparisons: `>`, `<` → 2; elseif_statement → 1;
15042                // else_statement → 1. C = 4. No branches (no calls).
15043                assert_eq!(metric.abc.assignments_sum(), 0);
15044                assert_eq!(metric.abc.branches_sum(), 0);
15045                assert_eq!(metric.abc.conditions_sum(), 4);
15046                insta::assert_json_snapshot!(metric.abc);
15047            },
15048        );
15049    }
15050
15051    #[cfg(feature = "lua")]
15052    #[test]
15053    fn lua_complex_function_abc() {
15054        // Combines every category to pin the metric.
15055        check_metrics::<LuaParser>(
15056            "function run(n)\n\
15057                 local total = 0\n\
15058                 for i = 1, n do\n\
15059                     if i % 2 == 0 then\n\
15060                         do_work(i)\n\
15061                     else\n\
15062                         total = total + i\n\
15063                     end\n\
15064                 end\n\
15065                 print(\"done\")\n\
15066                 return total\n\
15067             end",
15068            "foo.lua",
15069            |metric| {
15070                // Assignments: `local total = 0` (1), `total = total + i` (1) → 2.
15071                // Branches: `do_work(i)` (1), `print(\"done\")` (1) → 2.
15072                // Conditions: `==` (1), `else_statement` (1) → 2.
15073                assert_eq!(metric.abc.assignments_sum(), 2);
15074                assert_eq!(metric.abc.branches_sum(), 2);
15075                assert_eq!(metric.abc.conditions_sum(), 2);
15076                insta::assert_json_snapshot!(metric.abc);
15077            },
15078        );
15079    }
15080
15081    #[cfg(feature = "lua")]
15082    #[test]
15083    fn lua_if_multiple_conditions() {
15084        // Fitzpatrick Rule 9 walker (issue #403). Lua's `and` / `or`
15085        // are keyword tokens inside a `binary_expression`.
15086        check_metrics::<LuaParser>(
15087            "function f(a, b, c, d)\n\
15088                 if a or b or c or d then return 1 end       -- +4c\n\
15089                 if a and b and c then return 2 end          -- +3c\n\
15090                 if not a and not b then return 3 end        -- +2c\n\
15091                 return 0\n\
15092             end",
15093            "foo.lua",
15094            |metric| {
15095                assert_eq!(metric.abc.conditions_sum(), 9);
15096                insta::assert_json_snapshot!(metric.abc);
15097            },
15098        );
15099    }
15100
15101    #[cfg(feature = "lua")]
15102    #[test]
15103    fn lua_while_conditions() {
15104        // Lua has no `do { ... } while(cond);` — `while cond do …
15105        // end` and `repeat … until cond` are the loop forms.
15106        check_metrics::<LuaParser>(
15107            "function f(a, b)\n\
15108                 while a or b do break end                   -- +2c\n\
15109                 repeat break until a and not b              -- +2c\n\
15110             end",
15111            "foo.lua",
15112            |metric| {
15113                assert_eq!(metric.abc.conditions_sum(), 4);
15114                insta::assert_json_snapshot!(metric.abc);
15115            },
15116        );
15117    }
15118
15119    #[cfg(feature = "lua")]
15120    #[test]
15121    fn lua_short_circuit_with_boolean_literal_operand() {
15122        // `a and true` reports 2 conditions: one Identifier, one
15123        // True keyword literal.
15124        check_metrics::<LuaParser>("function f(a) return a and true end", "foo.lua", |metric| {
15125            assert_eq!(metric.abc.conditions_sum(), 2);
15126            insta::assert_json_snapshot!(metric.abc);
15127        });
15128    }
15129
15130    #[cfg(feature = "lua")]
15131    #[test]
15132    fn lua_number_truthy_condition_counts() {
15133        // Regression for findings.md #2: Lua treats every non-nil,
15134        // non-false value as truthy, so `if 1 then ... end` and
15135        // `return a and 2` should each count their numeric literal
15136        // as a Fitzpatrick Rule 6 / 7 unary condition. Pre-fix,
15137        // the former `lua_bool_terminal_kinds!()` listed `True` /
15138        // `False` / `Nil` but omitted `Number`, so the walker dropped
15139        // every numeric-truthy operand. The walker comment at the top
15140        // of the former `lua_inspect_container` already promised
15141        // numbers were terminal-bool kinds; this commit closed the gap.
15142        check_metrics::<LuaParser>(
15143            "function f(a)\n\
15144             \x20   if 1 then return 1 end\n\
15145             \x20   return a and 2\n\
15146             end",
15147            "foo.lua",
15148            |metric| {
15149                // `if 1 then` → walker counts the Number literal (+1).
15150                // `a and 2` → `and` walker counts both operands:
15151                //   identifier `a` (+1), Number `2` (+1).
15152                // Total: 3.
15153                assert_eq!(metric.abc.conditions_sum(), 3);
15154                insta::assert_json_snapshot!(metric.abc);
15155            },
15156        );
15157    }
15158
15159    #[cfg(feature = "lua")]
15160    #[test]
15161    fn lua_if_boolean_literal_condition() {
15162        check_metrics::<LuaParser>(
15163            "function f()\n\
15164                 if true then end                  -- +1c\n\
15165                 if not false then end             -- +1c\n\
15166                 while true do break end           -- +1c\n\
15167                 repeat break until false          -- +1c\n\
15168             end",
15169            "foo.lua",
15170            |metric| {
15171                assert_eq!(metric.abc.conditions_sum(), 4);
15172                insta::assert_json_snapshot!(metric.abc);
15173            },
15174        );
15175    }
15176
15177    #[cfg(feature = "lua")]
15178    #[test]
15179    fn lua_methods_arguments_with_conditions() {
15180        // `m(not a, not b)` — argument list walker counts each
15181        // unary-conditional argument once. Bare-identifier args
15182        // (`m(a, b)`) do not count (list_kind != BinaryExpression).
15183        check_metrics::<LuaParser>(
15184            "function f(a, b) m(a, b); m(not a, not b) end",
15185            "foo.lua",
15186            |metric| {
15187                assert_eq!(metric.abc.branches_sum(), 2);
15188                assert_eq!(metric.abc.conditions_sum(), 2);
15189                insta::assert_json_snapshot!(metric.abc);
15190            },
15191        );
15192    }
15193
15194    #[cfg(feature = "lua")]
15195    #[test]
15196    fn lua_return_with_conditions() {
15197        // `return not (z >= 0)` → walker on `not` unwraps the paren
15198        // chain and reaches the inner BinaryExpression; the inner
15199        // `>=` comparison is the actual Fitzpatrick condition.
15200        check_metrics::<LuaParser>(
15201            "function m1(z) return not (z >= 0) end\n\
15202             function m2(x) return (((not x))) end\n\
15203             function m3(x, y) return x and y end",
15204            "foo.lua",
15205            |metric| {
15206                // m1: `>=` (1). `not` wraps a paren'd
15207                //     BinaryExpression — `lua_count_negated` peels it
15208                //     to the comparison, which scores itself, so the
15209                //     return value pays nothing extra. +1.
15210                // m2: ReturnStatement → iterate expression_list →
15211                //     `lua_count_negated` peels the parens and the
15212                //     `not` (which proves boolean) down to `x`, which
15213                //     does not score itself. +1.
15214                // m3: x and y → `and` walker counts both → +2.
15215                // Sum: 4.
15216                assert_eq!(metric.abc.conditions_sum(), 4);
15217                insta::assert_json_snapshot!(metric.abc);
15218            },
15219        );
15220    }
15221
15222    // ---------- Tcl ABC tests ----------
15223
15224    #[cfg(feature = "tcl")]
15225    #[test]
15226    fn tcl_empty_unit_zero() {
15227        check_metrics::<TclParser>("", "empty.tcl", |metric| {
15228            assert_eq!(metric.abc.assignments_sum(), 0);
15229            assert_eq!(metric.abc.branches_sum(), 0);
15230            assert_eq!(metric.abc.conditions_sum(), 0);
15231            insta::assert_json_snapshot!(metric.abc);
15232        });
15233    }
15234
15235    #[cfg(feature = "tcl")]
15236    #[test]
15237    fn tcl_set_command_counts_assignment() {
15238        // `set` has its own grammar production; each invocation is
15239        // one assignment.
15240        check_metrics::<TclParser>(
15241            "proc f {} {\n\
15242                 set x 1\n\
15243                 set y 2\n\
15244                 set x [expr {$x + $y}]\n\
15245             }",
15246            "foo.tcl",
15247            |metric| {
15248                // 3 `set` invocations → A=3. The inner `expr` is a
15249                // sub-command (`command_substitution` + `expr_cmd`),
15250                // not a `command` node, so it doesn't add a branch.
15251                assert_eq!(metric.abc.assignments_sum(), 3);
15252                assert_eq!(metric.abc.branches_sum(), 0);
15253                assert_eq!(metric.abc.conditions_sum(), 0);
15254                insta::assert_json_snapshot!(metric.abc);
15255            },
15256        );
15257    }
15258
15259    #[cfg(feature = "tcl")]
15260    #[test]
15261    fn tcl_incr_append_lappend_count_assignment() {
15262        // Variable-mutation commands (`incr`, `append`, `lappend`)
15263        // are recognised by name and count as assignments, not
15264        // branches.
15265        check_metrics::<TclParser>(
15266            "proc f {} {\n\
15267                 set x 0\n\
15268                 incr x\n\
15269                 append s \"hi\"\n\
15270                 lappend lst 1\n\
15271             }",
15272            "foo.tcl",
15273            |metric| {
15274                // `set` (1) + `incr` (1) + `append` (1) + `lappend`
15275                // (1) → A=4. No branches, no conditions.
15276                assert_eq!(metric.abc.assignments_sum(), 4);
15277                assert_eq!(metric.abc.branches_sum(), 0);
15278                assert_eq!(metric.abc.conditions_sum(), 0);
15279                insta::assert_json_snapshot!(metric.abc);
15280            },
15281        );
15282    }
15283
15284    #[cfg(feature = "tcl")]
15285    #[test]
15286    fn tcl_qualified_mutator_commands_count_assignment() {
15287        // `::incr` is `incr` through the global namespace. Anchored on
15288        // `branches_sum() == 0` as well as the assignment total: an
15289        // unresolved name falls to the branch arm, so the two columns
15290        // move in opposite directions and no single total can mask the
15291        // regression (#1381 review).
15292        check_metrics::<TclParser>(
15293            "proc f {} {\n\
15294                 ::incr x\n\
15295                 ::append s \"hi\"\n\
15296                 ::lappend lst 1\n\
15297             }",
15298            "foo.tcl",
15299            |metric| {
15300                assert_eq!(metric.abc.assignments_sum(), 3);
15301                assert_eq!(metric.abc.branches_sum(), 0);
15302                assert_eq!(metric.abc.conditions_sum(), 0);
15303            },
15304        );
15305    }
15306
15307    #[cfg(feature = "tcl")]
15308    #[test]
15309    fn tcl_namespaced_mutator_command_stays_a_branch() {
15310        // Control: only the *leading* qualifier names the core command,
15311        // so `ns::incr` is a user proc and keeps the branch column — the
15312        // direction `BRACED_WORD_VALUE_CASES` pins for `ns::eval`.
15313        check_metrics::<TclParser>(
15314            "proc f {} {\n\
15315                 ns::incr x\n\
15316             }",
15317            "foo.tcl",
15318            |metric| {
15319                assert_eq!(metric.abc.assignments_sum(), 0);
15320                assert_eq!(metric.abc.branches_sum(), 1);
15321            },
15322        );
15323    }
15324
15325    #[cfg(feature = "tcl")]
15326    #[test]
15327    fn tcl_computed_command_name_is_not_an_assignment() {
15328        // A command whose leading word is computed (`$cmd args`) names no
15329        // builtin the parser can resolve, so it stays a branch. Pins the
15330        // field-addressed, `simple_word`-gated read the classifier shares
15331        // with the Cognitive / Cyclomatic detectors (grammar-dispatch §3):
15332        // the earlier `child(0)` byte-slice addressed the slot by position
15333        // and compared whatever literal text sat there, which agrees with
15334        // the gated read on today's grammar only because no mutator name is
15335        // spelled with a leading `$`. A grammar that moved the name out of
15336        // `child(0)`, or a mutator list that grew a computed-looking entry,
15337        // would diverge — this fixture is where that surfaces.
15338        check_metrics::<TclParser>(
15339            "proc f {cmd x} {\n\
15340                 $cmd $x\n\
15341                 incr x\n\
15342             }",
15343            "foo.tcl",
15344            |metric| {
15345                // `incr x` is the only assignment; `$cmd $x` is a branch.
15346                assert_eq!(metric.abc.assignments_sum(), 1);
15347                assert_eq!(metric.abc.branches_sum(), 1);
15348                assert_eq!(metric.abc.conditions_sum(), 0);
15349            },
15350        );
15351    }
15352
15353    #[cfg(feature = "tcl")]
15354    #[test]
15355    fn tcl_generic_commands_are_branches() {
15356        // Anything that isn't `set` or a known mutator command
15357        // counts as a branch — including builtins like `puts` and
15358        // `return`.
15359        check_metrics::<TclParser>(
15360            "proc f {} {\n\
15361                 puts \"hello\"\n\
15362                 do_work 1 2\n\
15363                 return 0\n\
15364             }",
15365            "foo.tcl",
15366            |metric| {
15367                // 3 commands, all branches.
15368                assert_eq!(metric.abc.assignments_sum(), 0);
15369                assert_eq!(metric.abc.branches_sum(), 3);
15370                assert_eq!(metric.abc.conditions_sum(), 0);
15371                insta::assert_json_snapshot!(metric.abc);
15372            },
15373        );
15374    }
15375
15376    #[cfg(feature = "tcl")]
15377    #[test]
15378    fn tcl_comparisons_count_logical_ops_do_not() {
15379        // `expr` predicates expose comparison / logical tokens at
15380        // the leaf level. Each comparison token contributes one
15381        // condition; `&&` and `||` are NOT counted on their own
15382        // (Fitzpatrick Rule 5; #395) — instead each operand is
15383        // counted as a unary conditional by the walker (Rule 9;
15384        // #403). The two logical lines add 2 walker conditions
15385        // each (variable-substitution operands).
15386        check_metrics::<TclParser>(
15387            "proc f {a b} {\n\
15388                 set r [expr {$a == $b}]\n\
15389                 set r [expr {$a != $b}]\n\
15390                 set r [expr {$a <  $b}]\n\
15391                 set r [expr {$a >  $b}]\n\
15392                 set r [expr {$a <= $b}]\n\
15393                 set r [expr {$a >= $b}]\n\
15394                 set r [expr {$a eq $b}]\n\
15395                 set r [expr {$a ne $b}]\n\
15396                 set r [expr {$a && $b}]\n\
15397                 set r [expr {$a || $b}]\n\
15398             }",
15399            "foo.tcl",
15400            |metric| {
15401                // 10 `set` assignments.
15402                assert_eq!(metric.abc.assignments_sum(), 10);
15403                assert_eq!(metric.abc.branches_sum(), 0);
15404                // 8 comparisons (+8) + 2 logical lines × 2 walker
15405                // operands (+4) = 12.
15406                assert_eq!(metric.abc.conditions_sum(), 12);
15407                insta::assert_json_snapshot!(metric.abc);
15408            },
15409        );
15410    }
15411
15412    #[cfg(feature = "tcl")]
15413    #[test]
15414    fn tcl_ternary_counts_condition() {
15415        // The `ternary_expr` node is one condition and its condition
15416        // slot `$a` — a bare truthy test — is another, matching C++'s
15417        // `int r = a ? b : c;` (also 2). The two branch operands are
15418        // unnegated and so contribute nothing (#1180).
15419        check_metrics::<TclParser>(
15420            "proc f {a b c} {\n\
15421                 set r [expr {$a ? $b : $c}]\n\
15422             }",
15423            "foo.tcl",
15424            |metric| {
15425                assert_eq!(metric.abc.assignments_sum(), 1);
15426                assert_eq!(metric.abc.branches_sum(), 0);
15427                assert_eq!(metric.abc.conditions_sum(), 2);
15428                insta::assert_json_snapshot!(metric.abc);
15429            },
15430        );
15431    }
15432
15433    /// The Tcl half of the ternary slot-location guard (#1180).
15434    ///
15435    /// `ternary_expr` exposes no grammar fields, so the slots are found
15436    /// relative to the `?` and `:` tokens. A *parenthesised* condition is
15437    /// the input that discriminates that from a fixed-index reading:
15438    /// `_expr` inlines `( … )` as anonymous children of `ternary_expr`,
15439    /// so `($a) ? !$b : !$c` shifts every operand right by one and
15440    /// `child(0)` / `child(2)` / `child(4)` land on `(`, `)` and `?`.
15441    /// Without this case the whole fixed-index revert passes.
15442    #[cfg(feature = "tcl")]
15443    #[test]
15444    fn tcl_parenthesised_ternary_condition_matches_the_bare_form() {
15445        let conditions = |source: &str| {
15446            crate::test_support::metrics_verbatim(
15447                crate::LANG::Tcl,
15448                source.as_bytes(),
15449                crate::MetricsOptions::default(),
15450            )
15451            .abc
15452            .conditions_sum()
15453        };
15454        let bare = conditions("proc f {a b c} {\n set r [expr {$a ? !$b : !$c}]\n}");
15455        assert_eq!(bare, 4, "the bare form is the documented reference value");
15456        assert_eq!(
15457            conditions("proc f {a b c} {\n set r [expr {($a) ? !$b : !$c}]\n}"),
15458            bare,
15459            "parenthesising the condition must not change the count"
15460        );
15461    }
15462
15463    /// A parenthesised operand under `!` counts the same as a bare one.
15464    ///
15465    /// `_expr` inlines `( … )` as anonymous children, so `!($a)` puts
15466    /// `(` where a positional read expects the operand. The walker's
15467    /// negation branch kept a fixed `child(1)` through the first draft of
15468    /// #1180 and scored these 0 while the unparenthesised forms scored 1
15469    /// — an inconsistency the fix itself introduced, since before it
15470    /// neither form counted. Found in review, not by the tests: the
15471    /// parenthesised fixtures added with #1180 covered the ternary
15472    /// *condition* slot only.
15473    #[cfg(feature = "tcl")]
15474    #[test]
15475    fn tcl_parenthesised_negated_operands_match_the_bare_form() {
15476        let conditions = |source: &str| {
15477            crate::test_support::metrics_verbatim(
15478                crate::LANG::Tcl,
15479                source.as_bytes(),
15480                crate::MetricsOptions::default(),
15481            )
15482            .abc
15483            .conditions_sum()
15484        };
15485        for (bare, parenthesised) in [
15486            (
15487                "proc f {a} {\n if {!$a} { puts x }\n}",
15488                "proc f {a} {\n if {!($a)} { puts x }\n}",
15489            ),
15490            (
15491                "proc f {a b} {\n if {$a && !$b} { puts x }\n}",
15492                "proc f {a b} {\n if {$a && !($b)} { puts x }\n}",
15493            ),
15494            (
15495                "proc f {a b c} {\n set r [expr {$a ? !$b : !$c}]\n}",
15496                "proc f {a b c} {\n set r [expr {$a ? !($b) : !$c}]\n}",
15497            ),
15498        ] {
15499            let want = conditions(bare);
15500            assert!(want > 0, "the bare form must count something: {bare}");
15501            assert_eq!(
15502                conditions(parenthesised),
15503                want,
15504                "parenthesising the negated operand changed the count\n  bare:  {bare}\n  paren: {parenthesised}"
15505            );
15506        }
15507    }
15508
15509    #[cfg(feature = "irules")]
15510    #[test]
15511    fn irules_abc_parenthesised_negated_operands_match_the_bare_form() {
15512        let conditions = |source: &str| {
15513            crate::test_support::metrics_verbatim(
15514                crate::LANG::Irules,
15515                source.as_bytes(),
15516                crate::MetricsOptions::default(),
15517            )
15518            .abc
15519            .conditions_sum()
15520        };
15521        let want = conditions("when X {\n    if { !$a } { log local0. hi }\n}\n");
15522        assert_eq!(want, 1);
15523        assert_eq!(
15524            conditions("when X {\n    if { !($a) } { log local0. hi }\n}\n"),
15525            want
15526        );
15527    }
15528
15529    #[cfg(feature = "tcl")]
15530    #[test]
15531    fn tcl_bare_truthy_and_negated_predicates_count_one_condition() {
15532        // The headline #1180 fix, on the Tcl side: both were 0 before.
15533        let conditions = |source: &str| {
15534            crate::test_support::metrics_verbatim(
15535                crate::LANG::Tcl,
15536                source.as_bytes(),
15537                crate::MetricsOptions::default(),
15538            )
15539            .abc
15540            .conditions_sum()
15541        };
15542        assert_eq!(conditions("proc f {a} {\n if {$a} { puts x }\n}"), 1);
15543        assert_eq!(conditions("proc f {a} {\n if {!$a} { puts x }\n}"), 1);
15544        assert_eq!(conditions("proc f {a} {\n while {$a} { puts x }\n}"), 1);
15545        assert_eq!(conditions("proc f {a} {\n while {!$a} { puts x }\n}"), 1);
15546    }
15547
15548    #[cfg(feature = "tcl")]
15549    #[test]
15550    fn tcl_bare_truthy_elseif_predicate_counts_one_condition() {
15551        // The `Tcl::Elseif` arm routes its predicate through
15552        // `tcl_condition_expr` exactly as `If` / `While` do (#1180), but
15553        // a comparison predicate is counted by its leaf operator, so it
15554        // cannot tell the routing from its absence. Only a bare truthy
15555        // predicate can: `$b` scores through the routed walker or not at
15556        // all.
15557        let conditions = |source: &str| {
15558            crate::test_support::metrics_verbatim(
15559                crate::LANG::Tcl,
15560                source.as_bytes(),
15561                crate::MetricsOptions::default(),
15562            )
15563            .abc
15564            .conditions_sum()
15565        };
15566        // `$a` truthy (1) + `elseif` clause (1) + `$b` truthy (1).
15567        assert_eq!(
15568            conditions("proc f {a b} {\n if {$a} { puts x } elseif {$b} { puts y }\n}"),
15569            3
15570        );
15571    }
15572
15573    #[cfg(feature = "tcl")]
15574    #[test]
15575    fn tcl_elseif_and_else_count_conditions() {
15576        // `if` / `elseif` / `else` clause productions each
15577        // contribute one condition. The leaf comparison inside the
15578        // predicate is counted independently.
15579        check_metrics::<TclParser>(
15580            "proc f {x} {\n\
15581                 if {$x > 0} {\n\
15582                     return 1\n\
15583                 } elseif {$x < 0} {\n\
15584                     return -1\n\
15585                 } else {\n\
15586                     return 0\n\
15587                 }\n\
15588             }",
15589            "foo.tcl",
15590            |metric| {
15591                // Branches: three `return` commands → 3.
15592                // Conditions: `>` (1), `<` (1), `elseif` (1), `else`
15593                // (1) → 4.
15594                assert_eq!(metric.abc.assignments_sum(), 0);
15595                assert_eq!(metric.abc.branches_sum(), 3);
15596                assert_eq!(metric.abc.conditions_sum(), 4);
15597                insta::assert_json_snapshot!(metric.abc);
15598            },
15599        );
15600    }
15601
15602    #[cfg(feature = "tcl")]
15603    #[test]
15604    fn tcl_if_multiple_conditions() {
15605        // Fitzpatrick Rule 9 walker (issue #403). Tcl's `expr` slot
15606        // exposes `&&` / `||` operands as variable substitutions
15607        // (`$a`, `$b`, …) inside a `binop_expr`.
15608        check_metrics::<TclParser>(
15609            "proc f {a b c d} {\n\
15610                 if {[expr {$a || $b || $c || $d}]} { return 1 }    \n\
15611                 if {[expr {$a && $b && $c}]} { return 2 }          \n\
15612                 return 0\n\
15613             }",
15614            "foo.tcl",
15615            |metric| {
15616                // The two chains feed the walker: 4 + 3 = 7. Each `if`
15617                // predicate is additionally a bare truthy test of a
15618                // command substitution — `{[expr {…}]}` is structurally
15619                // `if {[somecmd]}`, which counts 1 exactly as `if {$a}`
15620                // does — so 7 + 2 = 9 (#1180). Written without the
15621                // redundant `[expr …]` wrapper, `if {$a || $b}` scores
15622                // 2, matching C++'s `if (a || b)`.
15623                assert_eq!(metric.abc.conditions_sum(), 9);
15624                insta::assert_json_snapshot!(metric.abc);
15625            },
15626        );
15627    }
15628
15629    #[cfg(feature = "tcl")]
15630    #[test]
15631    fn tcl_while_conditions() {
15632        // Tcl has no `do { ... } while(cond);` — `while {…} {…}` is
15633        // the standard loop. The walker fires on `&&` / `||` tokens
15634        // inside the `expr` predicate.
15635        check_metrics::<TclParser>(
15636            "proc f {a b} {\n\
15637                 while {[expr {$a || $b}]} { break }    \n\
15638                 while {[expr {$a && $b}]} { break }    \n\
15639             }",
15640            "foo.tcl",
15641            |metric| {
15642                // 2 + 2 from the chains, plus one bare truthy test per
15643                // `while` predicate — see `tcl_if_multiple_conditions`
15644                // (#1180).
15645                assert_eq!(metric.abc.conditions_sum(), 6);
15646                insta::assert_json_snapshot!(metric.abc);
15647            },
15648        );
15649    }
15650
15651    #[cfg(feature = "tcl")]
15652    #[test]
15653    fn tcl_short_circuit_with_boolean_literal_operand() {
15654        // `$a && 1` reports 2 conditions: a VariableSubstitution
15655        // operand plus a Number-literal operand. Confirms `Number`
15656        // is in the walker terminal set. `true` / `false` Tcl
15657        // keywords are not literal tokens in tree-sitter-tcl —
15658        // they're emitted as the operator-context word, which is
15659        // captured separately by the `Tcl::Boolean` kind for
15660        // dedicated `expr {true}` predicates but not as a `&&`
15661        // operand at this iteration; using a numeric literal keeps
15662        // the assertion grammar-stable.
15663        check_metrics::<TclParser>(
15664            "proc f {a} { return [expr {$a && 1}] }\n",
15665            "foo.tcl",
15666            |metric| {
15667                assert_eq!(metric.abc.conditions_sum(), 2);
15668                insta::assert_json_snapshot!(metric.abc);
15669            },
15670        );
15671    }
15672
15673    #[cfg(feature = "tcl")]
15674    #[test]
15675    fn tcl_complex_function_abc() {
15676        // Mixed program covering every category. Tcl's grammar
15677        // re-parses braced content that looks command-shaped as a
15678        // nested `command` node, which inflates the branch count
15679        // relative to a naive read of the source — see breakdown.
15680        check_metrics::<TclParser>(
15681            "proc run {n} {\n\
15682                 set total 0\n\
15683                 for {set i 0} {$i < $n} {incr i} {\n\
15684                     if {$i % 2 == 0} {\n\
15685                         do_work $i\n\
15686                     } else {\n\
15687                         incr total $i\n\
15688                     }\n\
15689                 }\n\
15690                 puts \"done\"\n\
15691                 return $total\n\
15692             }",
15693            "foo.tcl",
15694            |metric| {
15695                // Assignments: `set total 0` (1), `set i 0` (1),
15696                // `incr i` (1), `incr total $i` (1) → A = 4.
15697                // Branches: the outer `for …` is one `command` node;
15698                // the `{$i < $n}` predicate ALSO re-parses as a
15699                // `command` node (tree-sitter-tcl treats braced
15700                // predicates as nested commands at the pinned
15701                // grammar version); plus `do_work $i`, `puts
15702                // "done"`, and `return $total`. The for-loop body's
15703                // `incr` and `incr total $i` are assignment commands
15704                // and don't add branches. Total B = 5.
15705                // Conditions: `==` (1) and `else` (1) → C = 2. The
15706                // `<` inside `{$i < $n}` is NOT `Tcl::LT`: because
15707                // that predicate re-parses as a `command`, the `<`
15708                // is emitted as `simple_word`. Only `<` inside a
15709                // real `expr` production becomes `Tcl::LT`.
15710                assert_eq!(metric.abc.assignments_sum(), 4);
15711                assert_eq!(metric.abc.branches_sum(), 5);
15712                assert_eq!(metric.abc.conditions_sum(), 2);
15713                insta::assert_json_snapshot!(metric.abc);
15714            },
15715        );
15716    }
15717
15718    /// The dedicated `set name value` production counts as one assignment.
15719    #[cfg(feature = "irules")]
15720    #[test]
15721    fn irules_abc_set_assignment() {
15722        check_metrics::<IrulesParser>("when X {\n    set x 1\n}\n", "foo.irule", |metric| {
15723            assert_eq!(metric.abc.assignments_sum(), 1);
15724            assert_eq!(metric.abc.branches_sum(), 0);
15725            assert_eq!(metric.abc.conditions_sum(), 0);
15726        });
15727    }
15728
15729    /// Mutator commands (`incr` / `append` / `lappend`) count as
15730    /// assignments, not branches — iRules has no assignment operators, so
15731    /// mutation is always a command invocation.
15732    #[cfg(feature = "irules")]
15733    #[test]
15734    fn irules_abc_mutator_commands() {
15735        check_metrics::<IrulesParser>(
15736            "when X {\n    incr x\n    append s \"y\"\n    lappend l 1\n}\n",
15737            "foo.irule",
15738            |metric| {
15739                assert_eq!(metric.abc.assignments_sum(), 3);
15740                assert_eq!(metric.abc.branches_sum(), 0);
15741            },
15742        );
15743    }
15744
15745    /// `::incr` is `incr` through the global namespace. iRules resolves
15746    /// the leading word in `irules_command_is_assignment` rather than
15747    /// through `tcl_command_name`, so this pins the strip on that second
15748    /// path (#1381 review).
15749    #[cfg(feature = "irules")]
15750    #[test]
15751    fn irules_abc_qualified_mutator_commands() {
15752        check_metrics::<IrulesParser>(
15753            "when X {\n    ::incr x\n    ::append s \"y\"\n    ::lappend l 1\n}\n",
15754            "foo.irule",
15755            |metric| {
15756                assert_eq!(metric.abc.assignments_sum(), 3);
15757                assert_eq!(metric.abc.branches_sum(), 0);
15758            },
15759        );
15760    }
15761
15762    /// Control: `ns::incr` is a proc in `ns`, so it stays a branch.
15763    #[cfg(feature = "irules")]
15764    #[test]
15765    fn irules_abc_namespaced_mutator_command_stays_a_branch() {
15766        check_metrics::<IrulesParser>("when X {\n    ns::incr x\n}\n", "foo.irule", |metric| {
15767            assert_eq!(metric.abc.assignments_sum(), 0);
15768            assert_eq!(metric.abc.branches_sum(), 1);
15769        });
15770    }
15771
15772    /// Generic (non-mutator) commands count as branches.
15773    #[cfg(feature = "irules")]
15774    #[test]
15775    fn irules_abc_branch_commands() {
15776        check_metrics::<IrulesParser>(
15777            "when X {\n    log local0. hi\n    pool p1\n}\n",
15778            "foo.irule",
15779            |metric| {
15780                assert_eq!(metric.abc.assignments_sum(), 0);
15781                assert_eq!(metric.abc.branches_sum(), 2);
15782                assert_eq!(metric.abc.conditions_sum(), 0);
15783            },
15784        );
15785    }
15786
15787    /// A numeric comparison (`==`) is one condition; the `log` inside the
15788    /// `if` body is one branch.
15789    #[cfg(feature = "irules")]
15790    #[test]
15791    fn irules_abc_comparison_condition() {
15792        check_metrics::<IrulesParser>(
15793            "when X {\n    if { $a == 1 } { log local0. hi }\n}\n",
15794            "foo.irule",
15795            |metric| {
15796                assert_eq!(metric.abc.branches_sum(), 1);
15797                assert_eq!(metric.abc.conditions_sum(), 1);
15798            },
15799        );
15800    }
15801
15802    /// A word-form string comparator (`contains`) is a condition just like
15803    /// `==` — iRules-specific (Tcl has only `eq`/`ne`/`in`/`ni`). If
15804    /// `contains` were dropped from the condition set this would report 0.
15805    #[cfg(feature = "irules")]
15806    #[test]
15807    fn irules_abc_string_op_condition() {
15808        check_metrics::<IrulesParser>(
15809            "when X {\n    if { $a contains \"x\" } { log local0. hi }\n}\n",
15810            "foo.irule",
15811            |metric| {
15812                assert_eq!(metric.abc.branches_sum(), 1);
15813                assert_eq!(metric.abc.conditions_sum(), 1);
15814            },
15815        );
15816    }
15817
15818    /// Each `elseif` / `else` clause is one condition; the three `set`s are
15819    /// assignments. The leading `if` is not itself a condition.
15820    #[cfg(feature = "irules")]
15821    #[test]
15822    fn irules_abc_elseif_else_conditions() {
15823        check_metrics::<IrulesParser>(
15824            "when X {\n    if { $a } { set r 1 } elseif { $b } { set r 2 } else { set r 3 }\n}\n",
15825            "foo.irule",
15826            |metric| {
15827                assert_eq!(metric.abc.assignments_sum(), 3);
15828                // The `elseif` and `else` clauses are one condition each,
15829                // as before; #1180 adds the two bare truthy predicates
15830                // (`{ $a }`, `{ $b }`). C++'s
15831                // `if(a){} else if(b){} else {}` also scores 4.
15832                assert_eq!(metric.abc.conditions_sum(), 4);
15833            },
15834        );
15835    }
15836
15837    /// A ternary contributes its own condition plus the `>` comparison in
15838    /// its test: conditions 2; the `set` is one assignment.
15839    #[cfg(feature = "irules")]
15840    #[test]
15841    fn irules_abc_ternary_condition() {
15842        check_metrics::<IrulesParser>(
15843            "when X {\n    set y [expr { $a > 0 ? 1 : 0 }]\n}\n",
15844            "foo.irule",
15845            |metric| {
15846                assert_eq!(metric.abc.assignments_sum(), 1);
15847                assert_eq!(metric.abc.conditions_sum(), 2);
15848            },
15849        );
15850    }
15851
15852    /// Fitzpatrick Rule 9: the short-circuit `&&` is not itself a condition,
15853    /// but each negated bare operand (`!$a`, `!$b`) in the chain is. Guards
15854    /// the `irules_count_condition` chain-operand walk — conditions 2.
15855    #[cfg(feature = "irules")]
15856    #[test]
15857    fn irules_abc_negated_operands_in_chain() {
15858        check_metrics::<IrulesParser>(
15859            "when X {\n    if { !$a && !$b } { log local0. hi }\n}\n",
15860            "foo.irule",
15861            |metric| {
15862                assert_eq!(metric.abc.branches_sum(), 1);
15863                assert_eq!(metric.abc.conditions_sum(), 2);
15864            },
15865        );
15866    }
15867
15868    /// A bare-truthy `if {$a}` predicate is one condition (#1180).
15869    ///
15870    /// It carries no comparison, ternary or short-circuit operator, so
15871    /// before the Phase 2B slot routing landed nothing invoked the
15872    /// unary-conditional walker and the whole predicate scored 0 — this
15873    /// test previously pinned that absence, and the metrics book's ABC
15874    /// deviation table said so. Now the `if` node routes its `expr`
15875    /// predicate and the count matches C++'s `if (a)`, which is also 1.
15876    /// The `log` command remains the single branch.
15877    #[cfg(feature = "irules")]
15878    #[test]
15879    fn irules_abc_bare_truthy_counts_one_condition() {
15880        check_metrics::<IrulesParser>(
15881            "when X {\n    if { $a } { log local0. hi }\n}\n",
15882            "foo.irule",
15883            |metric| {
15884                assert_eq!(metric.abc.branches_sum(), 1);
15885                assert_eq!(metric.abc.conditions_sum(), 1);
15886            },
15887        );
15888    }
15889
15890    /// The negated form of the same predicate, which was *also* 0 before
15891    /// #1180: `!$a` reached the walker but no parent seeded boolean
15892    /// context, so the terminal operand was never counted. Distinct from
15893    /// `irules_abc_negated_operands_in_chain`, whose `&&` supplied the
15894    /// seed the bare form lacked.
15895    #[cfg(feature = "irules")]
15896    #[test]
15897    fn irules_abc_negated_bare_truthy_counts_one_condition() {
15898        check_metrics::<IrulesParser>(
15899            "when X {\n    if { !$a } { log local0. hi }\n}\n",
15900            "foo.irule",
15901            |metric| {
15902                assert_eq!(metric.abc.branches_sum(), 1);
15903                assert_eq!(metric.abc.conditions_sum(), 1);
15904            },
15905        );
15906    }
15907
15908    /// The ternary's three operand slots, located relative to the `?`
15909    /// and `:` tokens because the grammar exposes no fields (#1180).
15910    ///
15911    /// `$a ? !$b : !$c` is four: the `ternary_expr` node, the bare
15912    /// truthy condition, and one per negated branch — the same value
15913    /// Java, C#, Groovy, the C family, the JS family, PHP, Perl, Ruby
15914    /// and Python report for the identical expression.
15915    #[cfg(feature = "irules")]
15916    #[test]
15917    fn irules_abc_ternary_routes_its_operand_slots() {
15918        check_metrics::<IrulesParser>(
15919            "when X {\n    set y [expr { $a ? !$b : !$c }]\n}\n",
15920            "foo.irule",
15921            |metric| {
15922                assert_eq!(metric.abc.conditions_sum(), 4);
15923            },
15924        );
15925    }
15926
15927    /// The #1161 control: a ternary whose condition is a *comparison*
15928    /// must not move. The `>` already supplied its condition and the
15929    /// branches are unnegated, so routing the slots adds nothing.
15930    #[cfg(feature = "irules")]
15931    #[test]
15932    fn irules_abc_comparison_ternary_is_unchanged_by_slot_routing() {
15933        check_metrics::<IrulesParser>(
15934            "when X {\n    set y [expr { $a > 0 ? 1 : 0 }]\n}\n",
15935            "foo.irule",
15936            |metric| {
15937                assert_eq!(metric.abc.conditions_sum(), 2);
15938            },
15939        );
15940    }
15941
15942    /// A parenthesised ternary condition scores the same as a bare one.
15943    ///
15944    /// The grammar inlines `( … )` as anonymous children of
15945    /// `ternary_expr` rather than wrapping them in a node, so a
15946    /// fixed-index reading of the slots would shift right by one and
15947    /// mis-assign every operand. This is the input that discriminates
15948    /// the token-relative location the fix uses.
15949    #[cfg(feature = "irules")]
15950    #[test]
15951    fn irules_abc_parenthesised_ternary_condition_matches_the_bare_form() {
15952        // `check_metrics` takes a bare `fn`, so it cannot carry the
15953        // first measurement into the second comparison; `metrics_verbatim`
15954        // returns a value instead.
15955        let conditions = |source: &str| {
15956            crate::test_support::metrics_verbatim(
15957                crate::LANG::Irules,
15958                source.as_bytes(),
15959                crate::MetricsOptions::default(),
15960            )
15961            .abc
15962            .conditions_sum()
15963        };
15964        let bare = conditions("when X {\n    set y [expr { $a ? !$b : !$c }]\n}\n");
15965        assert_eq!(bare, 4, "the bare form is the documented reference value");
15966        assert_eq!(
15967            conditions("when X {\n    set y [expr { ($a) ? !$b : !$c }]\n}\n"),
15968            bare,
15969            "parenthesising the condition must not change the count"
15970        );
15971    }
15972
15973    // #1454's five-language sweep of #1422's C# rule: a pattern-match
15974    // guard is a condition *slot*, so every spelling of one contributes
15975    // exactly one condition, and the guard is a cyclomatic decision the
15976    // arm it guards does not already pay for.
15977    //
15978    // Each fixture is the §11 triple — an operator guard (`> 5`, whose
15979    // comparison token already counted before the fix), a call guard,
15980    // and a bare-identifier guard (the two that counted nothing) —
15981    // plus a parenthesised guard, which is the only member that
15982    // exercises the boolean-context seed, plus an unguarded control
15983    // that must stay where it was. A single-spelling fixture cannot
15984    // show the slot works: before the fix the operator member already
15985    // read the right number.
15986    //
15987    // The controls are load-bearing twice over. `none` pins that the
15988    // slot fires on the guard and not on the arm — a `+1` on every arm
15989    // would satisfy every guarded row and break this one — and the
15990    // helper (`is_even` / `isEven`) pins that the call-guard row is
15991    // scoring its guard rather than its call, since a call is an ABC
15992    // *branch* everywhere and a condition nowhere.
15993    //
15994    // `assert_members_score` rather than `assert_every_member_scores`:
15995    // the guarded members and their control sit at different values,
15996    // which is the comparison these tests exist to make.
15997
15998    #[cfg(feature = "rust")]
15999    #[test]
16000    fn rust_match_guard_scores_one_condition_however_spelled() {
16001        let src = "fn is_even(n: i32) -> bool {
16002                n == 0
16003            }
16004            fn tok(x: i32) -> i32 {
16005                match x { n if n > 5 => 1, _ => 0 }
16006            }
16007            fn call(x: i32) -> i32 {
16008                match x { n if is_even(n) => 1, _ => 0 }
16009            }
16010            fn bare(x: i32, b: bool) -> i32 {
16011                match x { _ if b => 1, _ => 0 }
16012            }
16013            fn paren(x: i32, b: bool) -> i32 {
16014                match x { n if (b) => n, _ => 0 }
16015            }
16016            fn none(x: i32) -> i32 {
16017                match x { 1 => 1, _ => 0 }
16018            }";
16019        assert_fixture_spells::<RustParser>(
16020            src,
16021            "foo.rs",
16022            &[
16023                // The guard's own `if` keyword, one per guarded member.
16024                // It is also how Rust cyclomatic already saw the guard,
16025                // which is why only the ABC half moved here.
16026                (Rust::If as u16, 4, "match guards"),
16027                (Rust::GT as u16, 1, "`tok`'s `>`"),
16028                (Rust::CallExpression as u16, 1, "the `is_even` call"),
16029                (
16030                    Rust::ParenthesizedExpression as u16,
16031                    1,
16032                    "`paren`'s parenthesised guard",
16033                ),
16034                // Two arms per member: the guarded one and its bare `_`.
16035                (Rust::MatchPattern as u16, 10, "match arms"),
16036            ],
16037        );
16038        check_func_space::<RustParser, _>(src, "foo.rs", |space| {
16039            assert_members_score(
16040                &space,
16041                &[
16042                    ("is_even", 1, 1),
16043                    ("tok", 2, 3),
16044                    // Was 1 — the guard spelling nothing counted.
16045                    ("call", 2, 3),
16046                    ("bare", 2, 3),
16047                    ("paren", 2, 3),
16048                    ("none", 1, 2),
16049                ],
16050            );
16051        });
16052    }
16053
16054    #[cfg(feature = "java")]
16055    #[test]
16056    fn java_pattern_switch_guard_scores_one_condition_however_spelled() {
16057        let src = "class T {
16058                static boolean isEven(int i) { return i == 0; }
16059                int tok(Object o) { return switch (o) { case Integer i when i > 5 -> 1; default -> 0; }; }
16060                int call(Object o) { return switch (o) { case Integer i when isEven(i) -> 1; default -> 0; }; }
16061                int bare(Object o, boolean b) { return switch (o) { case Integer i when b -> 1; default -> 0; }; }
16062                int paren(Object o, boolean b) { return switch (o) { case Integer i when (b) -> 1; default -> 0; }; }
16063                int none(Object o) { return switch (o) { case Integer i -> 1; default -> 0; }; }
16064            }";
16065        assert_fixture_spells::<JavaParser>(
16066            src,
16067            "foo.java",
16068            &[
16069                (Java::Guard as u16, 4, "`when` guards"),
16070                (Java::GT as u16, 1, "`tok`'s `>`"),
16071                (Java::MethodInvocation as u16, 1, "the `isEven` call"),
16072                // Five `switch (o)` subjects plus `paren`'s `when (b)`.
16073                // Java wraps a switch subject in the same kind, so this
16074                // count moves if either is edited out.
16075                (
16076                    Java::ParenthesizedExpression as u16,
16077                    6,
16078                    "the switch subjects and `paren`'s guard",
16079                ),
16080            ],
16081        );
16082        check_func_space::<JavaParser, _>(src, "foo.java", |space| {
16083            assert_members_score(
16084                &space.spaces[0],
16085                &[
16086                    ("isEven", 1, 1),
16087                    // Was 2 conditions against a *flat* cyclomatic of 2:
16088                    // Java had neither half of the rule, so the guard
16089                    // was invisible to cyclomatic and visible to ABC
16090                    // only through whatever operator it spelled.
16091                    ("tok", 2, 3),
16092                    ("call", 2, 3),
16093                    ("bare", 2, 3),
16094                    ("paren", 2, 3),
16095                    ("none", 1, 2),
16096                ],
16097            );
16098        });
16099    }
16100
16101    #[cfg(feature = "python")]
16102    #[test]
16103    fn python_case_guard_scores_one_condition_however_spelled() {
16104        let src = "def is_even(n):
16105    return n == 0
16106def tok(x):
16107    match x:
16108        case n if n > 5:
16109            return 1
16110        case _:
16111            return 0
16112def call(x):
16113    match x:
16114        case n if is_even(x):
16115            return 1
16116        case _:
16117            return 0
16118def bare(x, b):
16119    match x:
16120        case _ if b:
16121            return 1
16122        case _:
16123            return 0
16124def paren(x, b):
16125    match x:
16126        case n if (b):
16127            return n
16128        case _:
16129            return 0
16130def none(x):
16131    match x:
16132        case 1:
16133            return 1
16134        case _:
16135            return 0
16136";
16137        assert_fixture_spells::<PythonParser>(
16138            src,
16139            "foo.py",
16140            &[
16141                // The `case` guard clause. A comprehension filter is the
16142                // same kind in a different role, and the `guard` field
16143                // is what keeps it out of the slot — there is none in
16144                // this fixture, and `python_comprehension_if_clause_is_not_a_case_guard`
16145                // is where that separation is pinned.
16146                (Python::IfClause as u16, 4, "`case` guards"),
16147                (
16148                    Python::ComparisonOperator as u16,
16149                    2,
16150                    "`tok`'s `>` and `is_even`'s `==`",
16151                ),
16152                (Python::Call as u16, 1, "the `is_even` call"),
16153                (
16154                    Python::ParenthesizedExpression as u16,
16155                    1,
16156                    "`paren`'s parenthesised guard",
16157                ),
16158            ],
16159        );
16160        check_func_space::<PythonParser, _>(src, "foo.py", |space| {
16161            assert_members_score(
16162                &space,
16163                &[
16164                    ("is_even", 1, 1),
16165                    ("tok", 2, 3),
16166                    ("call", 2, 3),
16167                    ("bare", 2, 3),
16168                    ("paren", 2, 3),
16169                    ("none", 1, 2),
16170                ],
16171            );
16172        });
16173    }
16174
16175    // A comprehension's `if` filter is an `if_clause` too, and it is not
16176    // a `case` guard: the slot reads `case_clause`'s `guard` field, so
16177    // the two cannot be confused by construction. Python cyclomatic does
16178    // count the filter (through the same `If` keyword token it counts a
16179    // guard by), and ABC does not — a pre-existing divergence this
16180    // change deliberately leaves where it found it. The test is here so
16181    // that a later `IfClause` arm added without the field read fails
16182    // loudly rather than moving comprehensions silently.
16183    #[cfg(feature = "python")]
16184    #[test]
16185    fn python_comprehension_if_clause_is_not_a_case_guard() {
16186        let src = "def m(xs):
16187    return [x for x in xs if x]
16188";
16189        assert_fixture_spells::<PythonParser>(
16190            src,
16191            "foo.py",
16192            &[(Python::IfClause as u16, 1, "the comprehension filter")],
16193        );
16194        check_func_space::<PythonParser, _>(src, "foo.py", |space| {
16195            assert_members_score(&space, &[("m", 0, 3)]);
16196        });
16197    }
16198
16199    #[cfg(feature = "ruby")]
16200    #[test]
16201    fn ruby_in_clause_guard_scores_one_condition_however_spelled() {
16202        let src = "def is_even(x)
16203  x == 0
16204end
16205def tok(x)
16206  case x
16207  in [n] if n > 5 then 1
16208  in _ then 0
16209  end
16210end
16211def call(x)
16212  case x
16213  in [n] if n.even? then 1
16214  in _ then 0
16215  end
16216end
16217def bare(x, b)
16218  case x
16219  in [n] if b then 1
16220  in _ then 0
16221  end
16222end
16223def unguard(x, b)
16224  case x
16225  in [n] unless b then 1
16226  in _ then 0
16227  end
16228end
16229def paren(x, b)
16230  case x
16231  in [n] if (b) then 1
16232  in _ then 0
16233  end
16234end
16235def none(x)
16236  case x
16237  in [n] then 1
16238  in _ then 0
16239  end
16240end
16241";
16242        assert_fixture_spells::<RubyParser>(
16243            src,
16244            "foo.rb",
16245            &[
16246                (Ruby::IfGuard as u16, 4, "`if` guards"),
16247                (Ruby::UnlessGuard as u16, 1, "the `unless` guard"),
16248                (Ruby::GT as u16, 1, "`tok`'s `>`"),
16249                (
16250                    Ruby::ParenthesizedStatements as u16,
16251                    1,
16252                    "`paren`'s parenthesised guard",
16253                ),
16254            ],
16255        );
16256        // `Ruby::Guard` (210) is the hidden `_guard` supertype: the two
16257        // dispatchers list it beside the concrete kinds so a grammar
16258        // that starts emitting it keeps working, and this pins that it
16259        // does not emit it today — otherwise the defensive arm is
16260        // indistinguishable from a dead one
16261        // (`.claude/rules/grammar-dispatch.md` §2).
16262        let parser = RubyParser::new(
16263            src.as_bytes().to_vec(),
16264            &std::path::PathBuf::from("foo.rb"),
16265            None,
16266        );
16267        assert!(
16268            !ast_has_kind_id(&parser, Ruby::Guard as u16),
16269            "`_guard` stopped being hidden — the defensive arms now fire"
16270        );
16271        check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
16272            assert_members_score(
16273                &space,
16274                &[
16275                    ("is_even", 1, 1),
16276                    // Was 2 / 2: Ruby had neither half either, and the
16277                    // arm's own condition supplied the 2 that made the
16278                    // operator spelling look correct.
16279                    ("tok", 2, 3),
16280                    ("call", 2, 3),
16281                    ("bare", 2, 3),
16282                    ("unguard", 2, 3),
16283                    ("paren", 2, 3),
16284                    ("none", 1, 2),
16285                ],
16286            );
16287        });
16288    }
16289
16290    // Elixir is the inverse of the four above: ABC counted the `when`
16291    // token from the start and cyclomatic had no arm at all, so a guard
16292    // read as a condition with no decision behind it. #1454 adds the
16293    // decision, which is why `tok` / `call` / `bare` move on the
16294    // cyclomatic axis here and on the ABC axis everywhere else.
16295    //
16296    // Elixir was also the one language of the six that #1454 left
16297    // *without* the slot. Its `when` arm added a flat one on top of
16298    // whatever the guard's sub-structure already scored, so `tok` came
16299    // back 3 against `call` and `bare`'s 2 — the spelling-dependence the
16300    // batch exists to remove, and a `.claude/rules/grammar-dispatch.md`
16301    // §5 double count with the `>` token arm. A whole-branch review of
16302    // the batch found it; all three rows now read 2, level with the
16303    // sibling fixtures above and with their own decision counts.
16304    #[cfg(feature = "elixir")]
16305    #[test]
16306    fn elixir_guard_is_a_decision_however_spelled() {
16307        let src = "defmodule T do
16308  def is_even(x) do
16309    x == 0
16310  end
16311  def tok(x) do
16312    case x do
16313      n when n > 5 -> 1
16314      _ -> 0
16315    end
16316  end
16317  def call(x) do
16318    case x do
16319      n when is_integer(n) -> 1
16320      _ -> 0
16321    end
16322  end
16323  def bare(x, b) do
16324    case x do
16325      _n when b -> 1
16326      _ -> 0
16327    end
16328  end
16329  def none(x) do
16330    case x do
16331      1 -> 1
16332      _ -> 0
16333    end
16334  end
16335end
16336";
16337        assert_fixture_spells::<ElixirParser>(
16338            src,
16339            "foo.ex",
16340            &[
16341                (Elixir::When as u16, 3, "`when` guards"),
16342                (Elixir::GT as u16, 1, "`tok`'s `>`"),
16343            ],
16344        );
16345        check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
16346            assert_members_score(
16347                &space.spaces[0],
16348                &[
16349                    ("is_even", 1, 1),
16350                    // All three guarded members were cyclomatic 2 —
16351                    // level with `none` — before the decision arm, and
16352                    // `tok` was abc 3 before the slot.
16353                    ("tok", 2, 3),
16354                    ("call", 2, 3),
16355                    ("bare", 2, 3),
16356                    ("none", 1, 2),
16357                ],
16358            );
16359        });
16360    }
16361
16362    // A statement modifier's `condition` is the same boolean slot as the
16363    // block form's, so `g if (b)` must score what `if (b) then g end`
16364    // does. The modifier kinds were missing from the peel's seed, so a
16365    // parenthesised operand unwrapped into a context only a negation
16366    // could mark boolean and scored 0 (#1521). Every member here is one
16367    // decision, so each reads `conditions == cyclomatic - 1`; `bare` and
16368    // `block` are the analogues the modifier rows must match.
16369    #[cfg(feature = "ruby")]
16370    #[test]
16371    fn ruby_parenthesised_modifier_condition_scores_one() {
16372        let src = "def bare(b)
16373  g if b
16374end
16375def block(b)
16376  if (b) then g end
16377end
16378def if1(b)
16379  g if (b)
16380end
16381def if2(b)
16382  g if ((b))
16383end
16384def unless1(b)
16385  g unless (b)
16386end
16387def unless2(b)
16388  g unless ((b))
16389end
16390def while1(b)
16391  g while (b)
16392end
16393def while2(b)
16394  g while ((b))
16395end
16396def until1(b)
16397  g until (b)
16398end
16399def until2(b)
16400  g until ((b))
16401end
16402";
16403        assert_fixture_spells::<RubyParser>(
16404            src,
16405            "foo.rb",
16406            &[
16407                (Ruby::IfModifier as u16, 3, "`if` modifiers"),
16408                (Ruby::UnlessModifier as u16, 2, "`unless` modifiers"),
16409                (Ruby::WhileModifier as u16, 2, "`while` modifiers"),
16410                (Ruby::UntilModifier as u16, 2, "`until` modifiers"),
16411                (
16412                    Ruby::ParenthesizedStatements as u16,
16413                    13,
16414                    "one layer per `(b)`, two per `((b))`",
16415                ),
16416            ],
16417        );
16418        check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
16419            assert_members_score(
16420                &space,
16421                &[
16422                    ("bare", 1, 2),
16423                    ("block", 1, 2),
16424                    ("if1", 1, 2),
16425                    ("if2", 1, 2),
16426                    ("unless1", 1, 2),
16427                    ("unless2", 1, 2),
16428                    ("while1", 1, 2),
16429                    ("while2", 1, 2),
16430                    ("until1", 1, 2),
16431                    ("until2", 1, 2),
16432                ],
16433            );
16434        });
16435    }
16436
16437    // The slot's whole claim, on every spelling the Elixir grammar gives
16438    // a guard — and on `assert_every_member_scores`, which asserts §8
16439    // parity where the fixture above cannot (its unguarded control and
16440    // its bare-comparison member legitimately sit off their decision
16441    // counts, which is why that one takes a per-member table).
16442    //
16443    // Nine members, nine routes to the same 2:
16444    //
16445    //   * `tok` / `eq` — the guard is an operator application, so the
16446    //     slot adds nothing and the `>` / `==` token arm supplies the
16447    //     one. These were 3 before the slot.
16448    //   * `call` / `bare` — no operator counts a `Call` or an
16449    //     `Identifier`, so the slot supplies the one directly.
16450    //   * `paren` / `negated` — `block` and `unary_operator` are
16451    //     wrappers, peeled by `elixir_wrapper_operand` before the slot
16452    //     asks whether an operator already paid. `negated` covers the
16453    //     keyword `not`, which the peel once did not recognise as a
16454    //     negation.
16455    //   * `membership` / `nonmembership` — `in` / `not in` are
16456    //     relational operators, given the by-use arm #1461 gave the
16457    //     other five languages. Without it these two would score zero,
16458    //     since the slot correctly declines an operator application.
16459    //
16460    // A repeated guard (`when a when b`) is deliberately absent: it is
16461    // an or-chain and scores one per alternative, so it belongs with
16462    // `elixir_repeated_guard_matches_the_or_chain` below rather than in
16463    // a table whose every row reads 2.
16464    //
16465    // §11: each bullet is an independent path, and no other member can
16466    // stand in for it — deleting the `in` arm leaves `membership` and
16467    // `nonmembership` alone failing, deleting the `not` recognition
16468    // leaves `negated` alone failing.
16469    #[cfg(feature = "elixir")]
16470    #[test]
16471    fn elixir_guard_scores_one_however_spelled() {
16472        let src = "defmodule T do
16473  def tok(x) do
16474    case x do
16475      n when n > 5 -> 1
16476      _ -> 0
16477    end
16478  end
16479  def eq(x) do
16480    case x do
16481      n when n == 5 -> 1
16482      _ -> 0
16483    end
16484  end
16485  def call(x) do
16486    case x do
16487      n when is_integer(n) -> 1
16488      _ -> 0
16489    end
16490  end
16491  def bare(x, b) do
16492    case x do
16493      _n when b -> 1
16494      _ -> 0
16495    end
16496  end
16497  def paren(x, b) do
16498    case x do
16499      _n when (b) -> 1
16500      _ -> 0
16501    end
16502  end
16503  def negated(x, b) do
16504    case x do
16505      _n when not b -> 1
16506      _ -> 0
16507    end
16508  end
16509  def membership(x) do
16510    case x do
16511      n when n in [1, 2] -> 1
16512      _ -> 0
16513    end
16514  end
16515  def nonmembership(x) do
16516    case x do
16517      n when n not in [1, 2] -> 1
16518      _ -> 0
16519    end
16520  end
16521end
16522";
16523        assert_fixture_spells::<ElixirParser>(
16524            src,
16525            "foo.ex",
16526            &[
16527                (Elixir::When as u16, 8, "one `when` guard per member"),
16528                (Elixir::GT as u16, 1, "`tok`'s `>`"),
16529                (Elixir::EQEQ as u16, 1, "`eq`'s `==`"),
16530                (Elixir::Block as u16, 1, "`paren`'s parenthesised guard"),
16531                (Elixir::Not as u16, 1, "`negated`'s `not`"),
16532                (Elixir::In as u16, 1, "`membership`'s `in`"),
16533                (Elixir::Notin as u16, 1, "`nonmembership`'s `not in`"),
16534            ],
16535        );
16536        check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
16537            assert_every_member_scores(
16538                &space.spaces[0],
16539                8,
16540                2,
16541                "one for the `case` arm and one for the guard, however the guard is spelled",
16542            );
16543        });
16544    }
16545
16546    // Elixir's repeated guard is an or-chain, not a longer spelling of
16547    // one guard: each `when` expression is tried in turn and evaluation
16548    // moves to the next when the previous one is false *or raises*. So
16549    // `when a when b` carries the alternatives `when a or b` does, and
16550    // the two spellings have to score alike.
16551    //
16552    // They did not. The nesting is right-associative
16553    // (`head when (a when (b when c))`), only the outermost `when` sat
16554    // on the anchor `elixir_when_is_guard` tests, and the `Abc` slot
16555    // peeled to the last alternative and scored that one — so a
16556    // repeated guard read *identically to a single guard* on both axes
16557    // while the `or` form read one higher per alternative.
16558    //
16559    // Asserted as an equality against the `or` spelling rather than
16560    // against separate literals, so neither row can drift alone; the
16561    // literals are here too, because a pair of equal wrong numbers
16562    // would satisfy the equality on its own.
16563    //
16564    // Both anchors, because the climb has to reach each: a definition
16565    // `Call`'s `arguments` (`repeated` / `ored`) and a `stab_clause`'s
16566    // `left` (`clause` / `clause_or`). `single` is the control the
16567    // repeated rows must sit *above*, which is the comparison that
16568    // failed before.
16569    #[cfg(feature = "elixir")]
16570    #[test]
16571    fn elixir_repeated_guard_matches_the_or_chain() {
16572        let src = "defmodule T do
16573  def repeated(n) when is_integer(n) when is_float(n) when is_atom(n) do
16574    n
16575  end
16576  def ored(n) when is_integer(n) or is_float(n) or is_atom(n) do
16577    n
16578  end
16579  def single(n) when is_integer(n) do
16580    n
16581  end
16582  def clause(x) do
16583    case x do
16584      n when is_integer(n) when is_float(n) -> 1
16585      _ -> 0
16586    end
16587  end
16588  def clause_or(x) do
16589    case x do
16590      n when is_integer(n) or is_float(n) -> 1
16591      _ -> 0
16592    end
16593  end
16594end
16595";
16596        assert_fixture_spells::<ElixirParser>(
16597            src,
16598            "foo.ex",
16599            &[
16600                (
16601                    Elixir::When as u16,
16602                    8,
16603                    "three repeated, one `or`-spelled, one single, two \
16604                     repeated and one `or`-spelled in a clause",
16605                ),
16606                (Elixir::Or as u16, 3, "the two `or` spellings' operators"),
16607            ],
16608        );
16609        check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
16610            // Positional: a `def` whose head carries a guard parses its
16611            // name out of a `binary_operator` rather than a plain
16612            // `Call`, so `repeated`, `ored` and `single` all come back
16613            // `<anonymous>` (the same naming gap
16614            // `elixir_typespec_when_is_not_a_guard` reads around).
16615            let members: Vec<(u64, u64)> = space.spaces[0]
16616                .spaces
16617                .iter()
16618                .map(|m| {
16619                    (
16620                        m.metrics.abc.conditions(),
16621                        m.metrics.cyclomatic.cyclomatic(),
16622                    )
16623                })
16624                .collect();
16625            assert_eq!(
16626                members,
16627                vec![(3, 4), (3, 4), (1, 2), (3, 4), (3, 4)],
16628                "a repeated guard scores one alternative per `when`; the \
16629                 single guard is the control one alternative lower"
16630            );
16631            assert_eq!(
16632                members[0], members[1],
16633                "`when a when b when c` is the or-chain `when a or b or c`"
16634            );
16635            assert_eq!(
16636                members[3], members[4],
16637                "and the same holds on a `stab_clause` anchor"
16638            );
16639        });
16640    }
16641
16642    // The gate that makes the Elixir arm safe. Elixir has no dedicated
16643    // guard production, and a typespec's binding clause spells the same
16644    // `when` token — so an ungated arm would have made type syntax a
16645    // decision. It was already an ABC condition against no decision
16646    // anywhere, which this removes.
16647    //
16648    // Both members carry the same `@spec`; only `guarded` carries a real
16649    // head guard, so the difference between the two rows is the guard
16650    // and nothing else.
16651    #[cfg(feature = "elixir")]
16652    #[test]
16653    fn elixir_typespec_when_is_not_a_guard() {
16654        let src = "defmodule T do
16655  @spec plain(a) :: a when a: integer
16656  def plain(x) do
16657    x
16658  end
16659  @spec guarded(a) :: a when a: integer
16660  def guarded(x) when is_integer(x) do
16661    x
16662  end
16663end
16664";
16665        assert_fixture_spells::<ElixirParser>(
16666            src,
16667            "foo.ex",
16668            &[(
16669                Elixir::When as u16,
16670                3,
16671                "two typespec `when`s and one head guard",
16672            )],
16673        );
16674        check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
16675            let module = &space.spaces[0];
16676            // Positional rather than by name: a `def` whose head carries
16677            // a guard parses its name out of a `binary_operator` instead
16678            // of a plain `Call` target, and the space comes back
16679            // `<anonymous>`. That naming gap predates this change and is
16680            // why `assert_members_score` cannot serve here.
16681            let members: Vec<(u64, u64)> = module
16682                .spaces
16683                .iter()
16684                .map(|m| {
16685                    (
16686                        m.metrics.abc.conditions(),
16687                        m.metrics.cyclomatic.cyclomatic(),
16688                    )
16689                })
16690                .collect();
16691            assert_eq!(
16692                members,
16693                vec![(0, 1), (1, 2)],
16694                "`plain` scores nothing; `guarded` scores its head guard \
16695                 in both metrics"
16696            );
16697            // The typespecs sit in the module body, outside either
16698            // member, so their (non-)contribution has to be read off
16699            // the container. Was 2 before the gate: one per `@spec`.
16700            assert_eq!(
16701                module.metrics.abc.conditions(),
16702                0,
16703                "a typespec `when` is type syntax, not a guard"
16704            );
16705        });
16706    }
16707
16708    // The other half of that gate's allowlist. `elixir_when_is_guard`
16709    // accepts a definition `Call`'s `arguments` anchor when the call's
16710    // keyword is a method macro *or* one of `defguard` / `defguardp`,
16711    // and the second disjunct is its own path: `elixir_is_method_macro`
16712    // spells only `def` / `defp` / `defmacro` / `defmacrop`, so a
16713    // `defguard` head reaches the anchor test solely through the
16714    // `matches!`. Nothing in the suite spelled `defguard` before this,
16715    // which left the disjunct scored by no fixture at all.
16716    //
16717    // `defguard` is where a guard is most obviously a decision — the
16718    // macro exists to name one — so an ungated head would have scored
16719    // the construct zero on both axes while the `def f(x) when g`
16720    // spelling it expands into scores one.
16721    //
16722    // Read off the *module*, not off a member: `defguard` is not a
16723    // method macro, so it opens no function space and its guard lands
16724    // in the container's body. `plain` is the control that pins the
16725    // module's rows to the two guards and nothing else, and the guard
16726    // bodies are calls (`is_integer/1`, `is_atom/1`) rather than
16727    // comparisons so neither can supply a condition of its own.
16728    #[cfg(feature = "elixir")]
16729    #[test]
16730    fn elixir_defguard_head_is_a_guard() {
16731        let src = "defmodule T do
16732  defguard is_int(x) when is_integer(x)
16733  defguardp is_at(x) when is_atom(x)
16734  def plain(x) do
16735    x
16736  end
16737end
16738";
16739        assert_fixture_spells::<ElixirParser>(
16740            src,
16741            "foo.ex",
16742            &[(
16743                Elixir::When as u16,
16744                2,
16745                "one per `defguard` / `defguardp` head",
16746            )],
16747        );
16748        check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
16749            let module = &space.spaces[0];
16750            assert_eq!(
16751                (
16752                    module.metrics.abc.conditions(),
16753                    module.metrics.cyclomatic.cyclomatic()
16754                ),
16755                (2, 3),
16756                "each of `defguard` and `defguardp` carries one guard, on \
16757                 both axes (cyclomatic counts from its base of 1)"
16758            );
16759            let members: Vec<(u64, u64)> = module
16760                .spaces
16761                .iter()
16762                .map(|m| {
16763                    (
16764                        m.metrics.abc.conditions(),
16765                        m.metrics.cyclomatic.cyclomatic(),
16766                    )
16767                })
16768                .collect();
16769            assert_eq!(
16770                members,
16771                vec![(0, 1)],
16772                "`defguard` opens no function space, so `plain` is the \
16773                 module's only member and scores nothing"
16774            );
16775        });
16776    }
16777
16778    // #1461's structural half: a relational operator scores by *use*,
16779    // a value-bearing operand scores in a boolean *slot*.
16780    //
16781    // Until this, the relational constructs a grammar spells as their
16782    // own production — C#'s two `is` tests, Java's and Groovy's
16783    // `instanceof`, Groovy's `in`, Kotlin's `is` / `in`, Ruby's
16784    // one-line `in` — reached `stats.conditions` only through
16785    // `<lang>_bool_terminal_kinds!()`, which every walker consults
16786    // inside a boolean slot and nowhere else. The comparison token
16787    // beside each of them carried no such gate, so one language scored
16788    // two spellings of the same relational test differently by
16789    // position:
16790    //
16791    //     val b = a == c        // 1 condition
16792    //     val b = a is String   // 0 conditions
16793    //
16794    // Fitzpatrick Rule 5 scores a relational operator wherever it is
16795    // written, so the asymmetry was in the mechanism. Each construct
16796    // now has an unconditional arm in its language's `compute` and has
16797    // left the terminal set; listing it in both would score it twice
16798    // (`.claude/rules/grammar-dispatch.md` §5).
16799    //
16800    // Every fixture below carries the same four shapes, because they
16801    // are structurally independent paths and covering one says nothing
16802    // about the others (§11):
16803    //
16804    // - `out*` — the construct with no boolean slot above it. This is
16805    //   the case the change exists for, and the one no prior fixture
16806    //   covered: measured against the pre-change binary, every `out*`
16807    //   member here scored **0**.
16808    // - `outCtl` — the same member spelling `==`. It read 1 before and
16809    //   after, so it is the anchor that makes `out*`'s 1 a claim about
16810    //   the construct rather than about the member shape
16811    //   (`.claude/rules/testing.md`, "Perturb the fixture as well as
16812    //   the production line").
16813    // - `in*` / `inAnd` — the predicate slot and the `&&`-chain walker,
16814    //   the two paths that already owned the count. They must be
16815    //   *unchanged*; a §5 double count surfaces here as a 2 against the
16816    //   control's 1, which is how #1459 caught the Kotlin `as?`
16817    //   collision.
16818    // - `forIn` (Kotlin / Groovy / Ruby) — the `for … in …` header,
16819    //   which spells the same keyword from a different production and
16820    //   must stay at 0. It is the reason those three count the node and
16821    //   not the token.
16822
16823    #[cfg(feature = "csharp")]
16824    #[test]
16825    fn csharp_is_tests_score_outside_a_boolean_slot() {
16826        let src = "class A {
16827                bool outIs(object x) => x is int;
16828                bool outPat(object x) => x is null;
16829                bool outCtl(int x) => x == 1;
16830                int inIs(object x) { if (x is int) { return 1; } return 0; }
16831                int inAnd(object x, bool b) { if (x is int && b) { return 1; } return 0; }
16832            }";
16833        assert_csharp_fixture_spells(
16834            src,
16835            &[
16836                (Csharp::IsExpression as u16, 3, "the bare `is` type tests"),
16837                (
16838                    Csharp::IsPatternExpression as u16,
16839                    1,
16840                    "`outPat`'s null pattern",
16841                ),
16842                (Csharp::EQEQ as u16, 1, "`outCtl`'s comparison"),
16843            ],
16844        );
16845        check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
16846            assert_members_score(
16847                &space.spaces[0],
16848                &[
16849                    // Both were 0, against `outCtl`'s 1 on the same
16850                    // expression-bodied shape.
16851                    ("outIs", 1, 1),
16852                    ("outPat", 1, 1),
16853                    ("outCtl", 1, 1),
16854                    // Unchanged: the slot no longer counts the test, the
16855                    // new arm does, and the total stays put.
16856                    ("inIs", 1, 2),
16857                    ("inAnd", 2, 3),
16858                ],
16859            );
16860        });
16861    }
16862
16863    #[cfg(feature = "java")]
16864    #[test]
16865    fn java_instanceof_scores_outside_a_boolean_slot() {
16866        let src = "class A {
16867                boolean outOf(Object x) { return x instanceof String; }
16868                boolean outCtl(int x) { return x == 1; }
16869                int inOf(Object x) { if (x instanceof String) { return 1; } return 0; }
16870                int inAnd(Object x, boolean b) { if (x instanceof String && b) { return 1; } return 0; }
16871            }";
16872        assert_fixture_spells::<JavaParser>(
16873            src,
16874            "foo.java",
16875            &[
16876                (
16877                    Java::InstanceofExpression as u16,
16878                    3,
16879                    "the `instanceof` tests",
16880                ),
16881                (Java::EQEQ as u16, 1, "`outCtl`'s comparison"),
16882            ],
16883        );
16884        check_func_space::<JavaParser, _>(src, "foo.java", |space| {
16885            assert_members_score(
16886                &space.spaces[0],
16887                &[
16888                    // Was 0, against `outCtl`'s 1 on the same
16889                    // `return`-a-predicate shape.
16890                    ("outOf", 1, 1),
16891                    ("outCtl", 1, 1),
16892                    ("inOf", 1, 2),
16893                    ("inAnd", 2, 3),
16894                ],
16895            );
16896        });
16897    }
16898
16899    // Groovy carries item 4 as well as the structural pass. `<=>`
16900    // yields -1 / 0 / 1 rather than a boolean, which is why it never
16901    // belonged in the terminal set — that set holds operands — but it
16902    // is a relational operator, and `LTEQGT` is already a condition
16903    // token in Ruby, PHP, C++ and Mozcpp. Groovy was the outlier at 0.
16904    #[cfg(feature = "groovy")]
16905    #[test]
16906    fn groovy_relational_productions_score_outside_a_boolean_slot() {
16907        let src = "class A {
16908                def outIs(x) { def b = x instanceof String; return b }
16909                def outIn(x, l) { def b = x in l; return b }
16910                def outShip(a, c) { def r = a <=> c; return r }
16911                def outCtl(x) { def b = x == 1; return b }
16912                def inIs(x) { if (x instanceof String) { return 1 }; return 0 }
16913                def inIn(x, l) { if (x in l) { return 1 }; return 0 }
16914                def forIn(l) { for (q in l) { }; return 0 }
16915            }";
16916        assert_fixture_spells::<GroovyParser>(
16917            src,
16918            "foo.groovy",
16919            &[
16920                (
16921                    Groovy::InstanceofExpression as u16,
16922                    2,
16923                    "the `instanceof` tests",
16924                ),
16925                (Groovy::MembershipExpression as u16, 2, "the `in` tests"),
16926                (Groovy::SpaceshipExpression as u16, 1, "`outShip`'s `<=>`"),
16927                (
16928                    Groovy::ForInStatement as u16,
16929                    1,
16930                    "`forIn`'s loop header, which spells the same `in`",
16931                ),
16932                (Groovy::EQEQ as u16, 1, "`outCtl`'s comparison"),
16933            ],
16934        );
16935        check_func_space::<GroovyParser, _>(src, "foo.groovy", |space| {
16936            assert_members_score(
16937                &space.spaces[0],
16938                &[
16939                    // All three were 0, against `outCtl`'s 1 on the
16940                    // identical declare-and-return shape.
16941                    ("outIs", 1, 1),
16942                    ("outIn", 1, 1),
16943                    ("outShip", 1, 1),
16944                    ("outCtl", 1, 1),
16945                    ("inIs", 1, 2),
16946                    ("inIn", 1, 2),
16947                    // The loop header is not a membership test and
16948                    // stays at 0 — counting the `in` *token* would have
16949                    // scored it.
16950                    ("forIn", 0, 2),
16951                ],
16952            );
16953        });
16954    }
16955
16956    #[cfg(feature = "kotlin")]
16957    #[test]
16958    fn kotlin_is_and_in_score_outside_a_boolean_slot() {
16959        let src = "class K {
16960                fun outIs(a: Any): Boolean { val b = a is String; return b }
16961                fun outIn(a: Int): Boolean { val b = a in 1..2; return b }
16962                fun outCtl(a: Int): Boolean { val b = a == 1; return b }
16963                fun inIs(a: Any): Int { if (a is String) { return 1 }; return 0 }
16964                fun forIn(l: List<Int>): Int { for (q in l) { }; return 0 }
16965            }";
16966        assert_kotlin_class_members(
16967            src,
16968            &[
16969                (Kotlin::IsExpression as u16, 2, "the `is` tests"),
16970                (Kotlin::InExpression as u16, 1, "`outIn`'s `in` test"),
16971                (
16972                    Kotlin::ForStatement as u16,
16973                    1,
16974                    "`forIn`'s loop header, which spells the same `in`",
16975                ),
16976                (Kotlin::EQEQ as u16, 1, "`outCtl`'s comparison"),
16977            ],
16978            &[
16979                // Both were 0, against `outCtl`'s 1 on the identical
16980                // `val`-and-return shape.
16981                ("outIs", 1, 1),
16982                ("outIn", 1, 1),
16983                ("outCtl", 1, 1),
16984                ("inIs", 1, 2),
16985                ("forIn", 0, 2),
16986            ],
16987        );
16988    }
16989
16990    #[cfg(feature = "ruby")]
16991    #[test]
16992    fn ruby_test_pattern_scores_outside_a_boolean_slot() {
16993        let src = "def out_in(a)
16994  b = a in Integer
16995  b
16996end
16997def out_ctl(a)
16998  b = a == 1
16999  b
17000end
17001def in_in(a)
17002  if a in Integer
17003    return 1
17004  end
17005  0
17006end
17007def for_in(l)
17008  for q in l do end
17009  0
17010end
17011";
17012        assert_fixture_spells::<RubyParser>(
17013            src,
17014            "foo.rb",
17015            &[
17016                (Ruby::TestPattern as u16, 2, "the one-line `in` tests"),
17017                (
17018                    Ruby::For as u16,
17019                    1,
17020                    "`for_in`'s loop header, which spells the same `in`",
17021                ),
17022                (Ruby::EQEQ as u16, 1, "`out_ctl`'s comparison"),
17023            ],
17024        );
17025        check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
17026            assert_members_score(
17027                &space,
17028                &[
17029                    // Was 0, against `out_ctl`'s 1 on the identical
17030                    // assign-and-return shape.
17031                    ("out_in", 1, 1),
17032                    ("out_ctl", 1, 1),
17033                    ("in_in", 1, 2),
17034                    ("for_in", 0, 2),
17035                ],
17036            );
17037        });
17038    }
17039
17040    // #1461 item 1. The walrus is an *operand*, not an operator, so it
17041    // joined the former `python_bool_terminal_kinds!()` and stays slot-scoped —
17042    // `b = (n := g())` outside a predicate is a binding, not a
17043    // decision. Inside one the slot tests `g()`'s truth, and
17044    // `if (n := g()):` scored 0 where the `if g():` it is a
17045    // refactoring of scored 1.
17046    //
17047    // `assignments` is asserted beside `conditions` because the walrus
17048    // is the one construct in the survey that pays on two axes, and
17049    // that is deliberate rather than a §5 double count: ABC's axes are
17050    // independent measurements of the same source, and `if (n := g()):`
17051    // genuinely both binds a name and decides a branch. Without this
17052    // row a later reader has no way to tell the intent from an
17053    // oversight.
17054    #[cfg(feature = "python")]
17055    #[test]
17056    fn python_walrus_is_a_unary_condition() {
17057        let src = "def in_slot(g):
17058    if (n := g()):
17059        return 1
17060    return 0
17061
17062def chain(g, b):
17063    return b and (n := g())
17064
17065def ctrl(g):
17066    if g():
17067        return 1
17068    return 0
17069";
17070        assert_fixture_spells::<PythonParser>(
17071            src,
17072            "foo.py",
17073            &[(Python::NamedExpression as u16, 2, "the walrus bindings")],
17074        );
17075        check_func_space::<PythonParser, _>(src, "foo.py", |space| {
17076            assert_members_score(
17077                &space,
17078                &[
17079                    // Was 0, against `ctrl`'s 1 for the same predicate
17080                    // without the binding.
17081                    ("in_slot", 1, 2),
17082                    // The `and`-chain walker is the second, independent
17083                    // path (§11): was 1, the bare `b` operand alone.
17084                    ("chain", 2, 2),
17085                    ("ctrl", 1, 2),
17086                ],
17087            );
17088            for (name, assignments) in [("in_slot", 1u64), ("chain", 1), ("ctrl", 0)] {
17089                assert_eq!(
17090                    child_space(&space, name).metrics.abc.assignments(),
17091                    assignments,
17092                    "{name}: the walrus also pays on the A axis, on purpose"
17093                );
17094            }
17095        });
17096    }
17097
17098    // #1461 item 2. A macro in a boolean slot expands to a boolean
17099    // expression, and `if matches!(x, Some(_))` scored 0 against a
17100    // cyclomatic decision of 1.
17101    //
17102    // `cfg_slot` is in the fixture because the arm's *breadth* is the
17103    // decision, not a side effect: these sets discriminate on slot and
17104    // never on return type, so `cfg!` counts exactly as `matches!`
17105    // does. It is also the construct the corpus actually moves —
17106    // serde's one changed snapshot is `if cfg!(no_underscore_consts)`,
17107    // not a `matches!` — so asserting only `matches!` would leave the
17108    // measured case uncovered.
17109    #[cfg(feature = "rust")]
17110    #[test]
17111    fn rust_macro_invocation_is_a_unary_condition() {
17112        let src = "fn in_slot(x: Option<u8>) -> u8 { if matches!(x, Some(_)) { 1 } else { 0 } }
17113fn cfg_slot() -> u8 { if cfg!(unix) { 1 } else { 0 } }
17114fn chain(x: Option<u8>, b: bool) -> bool { b && matches!(x, Some(_)) }
17115fn ctrl(x: Option<u8>) -> u8 { if x.is_some() { 1 } else { 0 } }
17116";
17117        assert_fixture_spells::<RustParser>(
17118            src,
17119            "foo.rs",
17120            &[(
17121                Rust::MacroInvocation as u16,
17122                3,
17123                "the `matches!` / `cfg!` predicates",
17124            )],
17125        );
17126        check_func_space::<RustParser, _>(src, "foo.rs", |space| {
17127            assert_members_score(
17128                &space,
17129                &[
17130                    // Each was 1 — the `else` alone — against `ctrl`'s
17131                    // 2 for the same shape with a method call in the
17132                    // predicate.
17133                    ("in_slot", 2, 2),
17134                    ("cfg_slot", 2, 2),
17135                    // The `&&`-chain walker, the second independent
17136                    // path (§11): was 1, the bare `b` operand alone.
17137                    ("chain", 2, 2),
17138                    ("ctrl", 2, 2),
17139                ],
17140            );
17141        });
17142    }
17143}
17144
17145/// A comment inside a ternary must not change its ABC conditions
17146/// (#1181).
17147///
17148/// Two opposite defects, one cause: tree-sitter counts a comment among a
17149/// node's children, so it is the operand's previous sibling *and* it
17150/// shifts every positional index.
17151///
17152/// * Languages whose seed asked "is my previous sibling `?` or `:`"
17153///   (C family, PHP, Perl, JS family) read the comment as "not a
17154///   ternary token", flipped the boolean-context seed on for a *branch*
17155///   slot, and **over**-counted: `a ? /*n*/ (b) : c` scored 3 where
17156///   `a ? (b) : c` scores 2.
17157/// * Languages whose branch walk read `child(2)` / `child(4)` (Java,
17158///   C#, Groovy) landed on the comment instead of the operand, never
17159///   inspected it, and **under**-counted: `a ? /*n*/ !b : c` scored 2
17160///   where `a ? !b : c` scores 3.
17161///
17162/// Both slots are now addressed by grammar field. The parenthesised
17163/// operand is the only input that discriminates the first defect and
17164/// the negated operand the only one that discriminates the second —
17165/// existing ternary fixtures use neither.
17166// Gated on the union of the features [`ternary_comment_invariance::cases`]
17167// has a row for, so a build enabling none of them — `--no-default-features
17168// --features go` — drops both tests rather than tripping their
17169// `checked > 0` guards (`.claude/rules/testing.md`, #1286). Both tests
17170// read the same table, so one gate on the module is exact for each.
17171#[cfg(test)]
17172#[cfg(any(
17173    feature = "c",
17174    feature = "cpp",
17175    feature = "csharp",
17176    feature = "groovy",
17177    feature = "java",
17178    feature = "javascript",
17179    feature = "mozcpp",
17180    feature = "mozjs",
17181    feature = "objc",
17182    feature = "perl",
17183    feature = "php",
17184    feature = "typescript"
17185))]
17186mod ternary_comment_invariance {
17187    use crate::test_support::metrics_verbatim;
17188    use crate::{LANG, MetricsOptions};
17189
17190    fn conditions(lang: LANG, source: &str) -> u64 {
17191        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
17192            .abc
17193            .conditions_sum()
17194    }
17195
17196    /// `(base, with_comment)` for a parenthesised and a negated branch
17197    /// operand, per language.
17198    fn cases(lang: LANG) -> Option<[(String, String); 2]> {
17199        // `{}` marks the consequence slot; `/*n*/` the inserted comment.
17200        let (template, paren, negated, comment) = match lang {
17201            LANG::Cpp | LANG::C | LANG::Objc | LANG::Mozcpp => {
17202                ("int f(){ int x = a ? {} : c; }", "(b)", "!b", "/*n*/ ")
17203            }
17204            LANG::Java => (
17205                "class K{ void f(){ int x = a ? {} : c; } }",
17206                "(b)",
17207                "!b",
17208                "/*n*/ ",
17209            ),
17210            LANG::Csharp => (
17211                "class K{ void f(){ var x = a ? {} : c; } }",
17212                "(b)",
17213                "!b",
17214                "/*n*/ ",
17215            ),
17216            LANG::Groovy => ("def f(){ def x = a ? {} : c }", "(b)", "!b", "/*n*/ "),
17217            LANG::Javascript | LANG::Typescript | LANG::Tsx | LANG::Mozjs => {
17218                ("function f(){ var x = a ? {} : c; }", "(b)", "!b", "/*n*/ ")
17219            }
17220            LANG::Php => (
17221                "<?php function f(){ $x = $a ? {} : $c; }",
17222                "($b)",
17223                "!$b",
17224                "/*n*/ ",
17225            ),
17226            // Perl has no block comment: `#` runs to end of line, so the
17227            // comment must carry its own newline.
17228            LANG::Perl => ("sub f { my $x = $a ? {} : $c; }", "($b)", "!$b", "# n\n "),
17229            _ => return None,
17230        };
17231        let build = |operand: &str, with_comment: bool| {
17232            let slot = if with_comment {
17233                format!("{comment}{operand}")
17234            } else {
17235                operand.to_owned()
17236            };
17237            template.replace("{}", &slot)
17238        };
17239        Some([
17240            (build(paren, false), build(paren, true)),
17241            (build(negated, false), build(negated, true)),
17242        ])
17243    }
17244
17245    #[test]
17246    fn a_comment_before_a_branch_operand_changes_nothing() {
17247        let mut checked = 0;
17248        for lang in LANG::into_enum_iter() {
17249            if !lang.is_enabled() {
17250                continue;
17251            }
17252            let Some(pairs) = cases(lang) else { continue };
17253            checked += 1;
17254            for (base, commented) in pairs {
17255                assert_eq!(
17256                    conditions(lang, &commented),
17257                    conditions(lang, &base),
17258                    "{lang:?}: a comment changed the ABC conditions of a ternary\n  \
17259                     without: {base}\n  with:    {commented}"
17260                );
17261            }
17262        }
17263        assert!(
17264            checked > 0,
17265            "no ternary language enabled; this test asserted nothing"
17266        );
17267    }
17268
17269    /// The absolute values the invariance test compares against, so a
17270    /// regression that moved *both* sides equally still fails.
17271    ///
17272    /// expected: `a ? (b) : c` counts the `?` marker plus the condition
17273    /// `a` in boolean context = 2. Negating the consequence adds one
17274    /// more, since `!b` establishes boolean content for that slot = 3.
17275    #[test]
17276    fn the_baseline_values_are_two_and_three() {
17277        let mut checked = 0;
17278        for lang in LANG::into_enum_iter() {
17279            if !lang.is_enabled() {
17280                continue;
17281            }
17282            let Some([(paren, _), (negated, _)]) = cases(lang) else {
17283                continue;
17284            };
17285            assert_eq!(
17286                conditions(lang, &paren),
17287                2,
17288                "{lang:?}: parenthesised branch"
17289            );
17290            assert_eq!(conditions(lang, &negated), 3, "{lang:?}: negated branch");
17291            checked += 1;
17292        }
17293        assert!(
17294            checked > 0,
17295            "no ternary language enabled; this test asserted nothing"
17296        );
17297    }
17298}
17299
17300/// A keyword negation must score like its symbolic twin (#1182).
17301///
17302/// `not b` and `!b` are the same negation — they differ in precedence,
17303/// not in meaning, and ABC counts the negation rather than the parse.
17304/// Ruby and Perl tested only the `!` token, so `if not b` scored 0
17305/// against `if !b`'s 1, and a `not` ternary scored 2 against the `!`
17306/// form's 4.
17307///
17308/// Lua and Elixir were checked in the same sweep and were already
17309/// correct: Lua's only negation keyword *is* `not` and it was the token
17310/// being tested, and Elixir reaches the same count by another path.
17311/// They are exercised here so a future edit cannot regress them
17312/// silently. Python counts `not` through its own dispatcher arm and has
17313/// no `!` spelling to compare against.
17314// Gated on the union of every row below — the three `pairs` languages
17315// plus Lua, whose test is separate — so a build enabling none of them
17316// drops the module instead of failing its guards
17317// (`.claude/rules/testing.md`, #1286 / #1411). The three tests do *not*
17318// share a row set, so each carries its own narrower gate: a Lua-only
17319// build must not be asked to run the Ruby/Perl baselines.
17320#[cfg(test)]
17321#[cfg(any(
17322    feature = "elixir",
17323    feature = "lua",
17324    feature = "perl",
17325    feature = "ruby"
17326))]
17327mod keyword_negation_parity {
17328    use crate::test_support::metrics_verbatim;
17329    use crate::{LANG, MetricsOptions};
17330
17331    fn conditions(lang: LANG, source: &str) -> u64 {
17332        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
17333            .abc
17334            .conditions_sum()
17335    }
17336
17337    /// `(bang_form, keyword_form)` pairs that must score identically.
17338    #[cfg(any(feature = "elixir", feature = "perl", feature = "ruby"))]
17339    fn pairs(lang: LANG) -> Option<Vec<(String, String)>> {
17340        let build =
17341            |t: &str| -> (String, String) { (t.replace("{NOT}", "!"), t.replace("{NOT}", "not ")) };
17342        let templates: &[&str] = match lang {
17343            LANG::Ruby => &[
17344                "def f(b)\n  if {NOT}b\n    1\n  end\nend\n",
17345                "def f(a, b, c)\n  x = a ? ({NOT}b) : ({NOT}c)\nend\n",
17346                "def f(a, b, c)\n  x = a ? b : ({NOT}c)\nend\n",
17347            ],
17348            LANG::Perl => &[
17349                "sub f { if ({NOT}$b) { 1; } }",
17350                "sub f { my $x = $a ? ({NOT}$b) : ({NOT}$c); }",
17351                "sub f { my $x = $a ? $b : ({NOT}$c); }",
17352            ],
17353            // Already correct before #1182; pinned so they stay that way.
17354            LANG::Elixir => &["def f(b) do\n  if {NOT}b do\n    1\n  end\nend\n"],
17355            _ => return None,
17356        };
17357        Some(templates.iter().map(|t| build(t)).collect())
17358    }
17359
17360    #[test]
17361    #[cfg(any(feature = "elixir", feature = "perl", feature = "ruby"))]
17362    fn the_not_keyword_scores_like_bang() {
17363        let mut checked = 0;
17364        for lang in LANG::into_enum_iter() {
17365            if !lang.is_enabled() {
17366                continue;
17367            }
17368            let Some(pairs) = pairs(lang) else { continue };
17369            checked += 1;
17370            for (bang, keyword) in pairs {
17371                assert_eq!(
17372                    conditions(lang, &keyword),
17373                    conditions(lang, &bang),
17374                    "{lang:?}: `not` and `!` scored differently\n  bang:    {bang}\n  keyword: {keyword}"
17375                );
17376            }
17377        }
17378        assert!(
17379            checked > 0,
17380            "no language enabled; this test asserted nothing"
17381        );
17382    }
17383
17384    /// The absolute values, so a regression that moved both spellings
17385    /// equally still fails.
17386    ///
17387    /// expected: `if !b` is one condition — the negated bare operand.
17388    /// `a ? (!b) : (!c)` is four: the `?` marker, the condition `a` in
17389    /// boolean context, and one per negated branch operand.
17390    #[test]
17391    #[cfg(any(feature = "perl", feature = "ruby"))]
17392    fn the_baseline_values_are_one_and_four() {
17393        let mut checked = 0;
17394        for (lang, guard, ternary) in [
17395            (
17396                LANG::Ruby,
17397                "def f(b)\n  if not b\n    1\n  end\nend\n",
17398                "def f(a, b, c)\n  x = a ? (not b) : (not c)\nend\n",
17399            ),
17400            (
17401                LANG::Perl,
17402                "sub f { if (not $b) { 1; } }",
17403                "sub f { my $x = $a ? (not $b) : (not $c); }",
17404            ),
17405        ] {
17406            if !lang.is_enabled() {
17407                continue;
17408            }
17409            assert_eq!(conditions(lang, guard), 1, "{lang:?}: `if not b`");
17410            assert_eq!(conditions(lang, ternary), 4, "{lang:?}: `not` ternary");
17411            checked += 1;
17412        }
17413        assert!(
17414            checked > 0,
17415            "no language enabled; this test asserted nothing"
17416        );
17417    }
17418
17419    /// Lua's only negation keyword is `not`, so it has no `!` twin to
17420    /// compare against — its guard is that the keyword counts at all.
17421    #[test]
17422    #[cfg(feature = "lua")]
17423    fn lua_counts_its_only_negation_keyword() {
17424        // Was a silent `if !is_enabled() { return; }`, which made the
17425        // whole test a no-op rather than absent whenever the runtime
17426        // check and the feature disagreed. The `cfg` above is the
17427        // absence half; this is the loud residual half (#1286).
17428        assert!(
17429            LANG::Lua.is_enabled(),
17430            "compiled under `feature = \"lua\"` but `LANG::Lua` reports disabled; \
17431             this test asserted nothing"
17432        );
17433        assert_eq!(
17434            conditions(
17435                LANG::Lua,
17436                "function f(b)\n  if not b then return 1 end\nend\n"
17437            ),
17438            1
17439        );
17440    }
17441}
17442
17443/// A numeric literal used as a bare `&&` / `||` operand is a Fitzpatrick
17444/// Rule 9 unary condition in every truthy-valued language (#1379).
17445///
17446/// Ruby's terminal-bool set named `integer` and none of `float` /
17447/// `rational` / `complex`, Elixir's named `integer` and not `float`, and
17448/// Perl's named no numeric kind at all — so `a && 1.0` scored one
17449/// condition where `a && 1` scored two, and in Perl even `$a && 1`
17450/// scored one. Python had both kinds since #772 and is the control the
17451/// other three were brought level with.
17452///
17453/// #1410 closed the six sets #1379 deferred: PHP, Groovy, and the
17454/// name-keyed C / C++ / Mozcpp / Objective-C set, where the omission
17455/// was visible *within* one language — `if (true)` scored one condition
17456/// and `if (1)` none, because `"true"` was in the set and
17457/// `number_literal` was not.
17458///
17459/// The headline claim of each case is a *comparison*: a numeric operand
17460/// must score exactly what an identifier operand scores in the same slot.
17461/// That is what discriminates the defect — with the kind missing from the
17462/// set the numeric form drops while the identifier form does not — and it
17463/// gives a failure message naming both spellings.
17464/// `every_operand_scores_its_recorded_values` then pins the absolute
17465/// numbers, so a regression moving *both* sides equally still fails, and
17466/// carries the cross-metric anchor `.claude/rules/grammar-dispatch.md` §8
17467/// asks for.
17468///
17469/// Two slots per language, because the sets feed two independent walker
17470/// paths (grammar-dispatch §11) and Perl's defect showed in both: the
17471/// operands of a `&&` chain, and the predicate of an `if`. A fixture of
17472/// only the first leaves `perl_count_condition` and `ruby_count_condition`
17473/// untested. Since #1520 Ruby's predicate pays for any operand no other
17474/// arm counts, terminal or not, so for Ruby only the chain half can tell
17475/// a kind missing from the set; the predicate half pins the slot itself.
17476///
17477/// The recorded cyclomatic figure is the same 3 for both slots — one file
17478/// space, one function space, one decision — while conditions differ (2
17479/// for the chain, 1 for the predicate, since ABC scores an `if` through
17480/// its predicate rather than the keyword). So §8's `conditions ==
17481/// cyclomatic - 1` identity is chain-specific arithmetic, not a law; what
17482/// generalises is that cyclomatic must not move when only the operand
17483/// spelling does, which is what makes a `conditions` move unambiguously
17484/// ABC's.
17485///
17486/// `for_each_case` guards the two ways this table could decay into
17487/// asserting nothing: an emptied `numerics` slice, and a template that
17488/// loses its `{}` slot (which would make every comparison `x == x`).
17489/// Both were reachable in the first draft and neither failed a test.
17490#[cfg(test)]
17491#[cfg(any(
17492    feature = "ruby",
17493    feature = "elixir",
17494    feature = "perl",
17495    feature = "python",
17496    feature = "lua",
17497    feature = "javascript",
17498    feature = "php",
17499    feature = "groovy",
17500    feature = "c",
17501    feature = "cpp",
17502    feature = "mozcpp",
17503    feature = "objc"
17504))]
17505mod numeric_bool_operands {
17506    use crate::test_support::metrics_verbatim;
17507    use crate::{LANG, MetricsOptions};
17508
17509    /// One fixture shape: a source template with a `{}` operand slot, and
17510    /// the `abc.conditions_sum` / `cyclomatic_sum` every spelling of that
17511    /// operand must produce.
17512    type Slot = (&'static str, u64, u64);
17513
17514    /// A language's two slots, its identifier baseline operand, the
17515    /// truthy literal operands that must score the same, and how many of
17516    /// those there should be. Every row but the C family's lists numeric
17517    /// literals only; that one adds `char_literal`, which is a second
17518    /// integral-literal kind rather than a second numeric spelling.
17519    ///
17520    /// The count is not bookkeeping. `for_each_case` counts *languages*,
17521    /// so trimming a row's operand list back to `&["1"]` — which is
17522    /// exactly the pre-#1379 fixture — left the whole module green when
17523    /// measured. Pinning the length makes that a deliberate two-line
17524    /// edit instead of a silent one.
17525    type Case = ([Slot; 2], &'static str, &'static [&'static str], usize);
17526
17527    fn conditions(lang: LANG, source: &str) -> u64 {
17528        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
17529            .abc
17530            .conditions_sum()
17531    }
17532
17533    fn cyclomatic_sum(lang: LANG, source: &str) -> u64 {
17534        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
17535            .cyclomatic
17536            .cyclomatic_sum()
17537    }
17538
17539    /// `([chain_slot, condition_slot], identifier, numerics)` per language.
17540    ///
17541    /// `{}` is the right-hand operand of a two-operand short-circuit chain
17542    /// in the first slot and the whole `if` predicate in the second, so
17543    /// every fixture differs from its own baseline in exactly one token.
17544    ///
17545    /// Each row lists **one spelling per numeric kind the grammar emits**,
17546    /// which is the unit that matters: #1379's misses were sibling rules
17547    /// under a shared supertype, not aliases of one rule.
17548    ///
17549    /// - Ruby: `integer` / `float` / `rational` / `complex`. `1r` parses
17550    ///   to `rational(integer)`, `2i` to `complex(integer)` and `1ri` to
17551    ///   `complex(rational(integer))` — verified by `bca dump` — and the
17552    ///   walker cannot descend into any wrapper, so listing the wrapper
17553    ///   is the only way each scores at all. Radix prefixes and `_`
17554    ///   separators fold into `integer`.
17555    /// - Perl: all five members of the hidden `_numeric_literals` choice.
17556    ///   `0xff` and `1.5e10` are here because the first cut of #1379
17557    ///   named only `integer` and `floating_point` and left them scoring
17558    ///   1 against an identifier's 2. `017` is *not* a sixth row — it
17559    ///   lexes as `integer`, and the `octal` kind is unreachable.
17560    /// - Elixir: `integer` / `float` / `char`. `?a` is the codepoint 97,
17561    ///   a numeric literal wearing a sigil.
17562    /// - PHP: `integer` / `float`. Every radix prefix, `_` separator and
17563    ///   exponent folds into those two, and the `float` *type* keyword
17564    ///   `Float2` is a third id rendering to the same name that must
17565    ///   stay out of the set (#1410).
17566    /// - Groovy: one consolidated `number_literal`, type suffixes
17567    ///   (`1L`, `1.5f`, `1G`) included.
17568    /// - C, C++, Mozcpp and Objective-C: one consolidated
17569    ///   `number_literal`, plus `char_literal` — not a numeric spelling
17570    ///   but a second integral-literal *kind*, since `'c'` has type
17571    ///   `int` in C and `char` in C++ and is truthy for the same reason.
17572    ///   All four share one name-keyed set and so run the same fixture,
17573    ///   which is the only coverage `Mozcpp` can have: it owns no file
17574    ///   extension, and `metrics_verbatim` is the one entry point that
17575    ///   reaches a `LANG` without going through one.
17576    ///
17577    /// Python, Lua and JavaScript were already correct and ride along as
17578    /// controls — they are what the first three were measured against,
17579    /// and a future edit that breaks them fails here too. Each folds
17580    /// every radix into one `Number` / `integer` kind, checked by
17581    /// measurement.
17582    fn cases(lang: LANG) -> Option<Case> {
17583        Some(match lang {
17584            LANG::Ruby => (
17585                [
17586                    ("def f(a)\n  a && {}\nend\n", 2, 3),
17587                    ("def f\n  if {}\n    1\n  end\nend\n", 1, 3),
17588                ],
17589                "b",
17590                &["1", "1.0", "1r", "2i", "1ri"],
17591                5,
17592            ),
17593            LANG::Elixir => (
17594                [
17595                    ("def f(a) do\n  a && {}\nend\n", 2, 3),
17596                    ("def f() do\n  if {} do\n    1\n  end\nend\n", 1, 3),
17597                ],
17598                "b",
17599                &["1", "1.0", "?a"],
17600                3,
17601            ),
17602            LANG::Perl => (
17603                [
17604                    ("sub f {\n  my $x = $a && {};\n}\n", 2, 3),
17605                    ("sub f {\n  if ({}) { 1; }\n}\n", 1, 3),
17606                ],
17607                "$b",
17608                &["1", "1.0", "1.5e10", "0xff"],
17609                4,
17610            ),
17611            LANG::Python => (
17612                [
17613                    ("def f(a):\n    return a and {}\n", 2, 3),
17614                    ("def f():\n    if {}:\n        return 1\n", 1, 3),
17615                ],
17616                "b",
17617                &["1", "1.0", "0xff", "1j"],
17618                4,
17619            ),
17620            LANG::Lua => (
17621                [
17622                    ("function f(a)\n  return a and {}\nend\n", 2, 3),
17623                    ("function f()\n  if {} then return 1 end\nend\n", 1, 3),
17624                ],
17625                "b",
17626                &["1", "1.0", "0xff"],
17627                3,
17628            ),
17629            LANG::Javascript => (
17630                [
17631                    ("function f(a) {\n  return a && {};\n}\n", 2, 3),
17632                    ("function f() {\n  if ({}) { return 1; }\n}\n", 1, 3),
17633                ],
17634                "b",
17635                &["1", "1.0", "0xff"],
17636                3,
17637            ),
17638            LANG::Php => (
17639                [
17640                    ("<?php\nfunction f($a) {\n  return $a && {};\n}\n", 2, 3),
17641                    ("<?php\nfunction f() {\n  if ({}) { return 1; }\n}\n", 1, 3),
17642                ],
17643                "$b",
17644                &["1", "1.0"],
17645                2,
17646            ),
17647            LANG::Groovy => (
17648                [
17649                    ("def f(a) {\n  return a && {}\n}\n", 2, 3),
17650                    ("def f() {\n  if ({}) { return 1 }\n}\n", 1, 3),
17651                ],
17652                "b",
17653                &["1"],
17654                1,
17655            ),
17656            LANG::C | LANG::Cpp | LANG::Mozcpp | LANG::Objc => (
17657                [
17658                    ("int f(int a) {\n  return a && {};\n}\n", 2, 3),
17659                    ("int f() {\n  if ({}) { return 1; }\n}\n", 1, 3),
17660                ],
17661                "b",
17662                &["1", "'c'"],
17663                2,
17664            ),
17665            _ => return None,
17666        })
17667    }
17668
17669    /// Runs `check` once per enabled language that has a case, having
17670    /// first established that the case can still assert something.
17671    ///
17672    /// Three guards, all of which had to be added after the first draft
17673    /// shipped without them and a measured perturbation of each left the
17674    /// whole module green:
17675    ///
17676    /// - **`checked > 0`** is the non-vacuity half of the rule in
17677    ///   `.claude/rules/testing.md`. The `#[cfg(any(feature = …))]` on
17678    ///   this module makes the tests *absent* when no truthy-valued
17679    ///   language is compiled in; this catches the residual case where
17680    ///   the runtime `is_enabled()` check stops agreeing with the feature
17681    ///   it compiled under.
17682    /// - **the `numerics` list is non-empty and keeps its recorded
17683    ///   length** — `checked` counts *languages*, so trimming a row's
17684    ///   numeric list back to `&["1"]` (the pre-#1379 fixture) or
17685    ///   emptying it left every test passing when measured, with that
17686    ///   language's coverage deleted. Two assertions, because the
17687    ///   length check passes an emptied row whose count was zeroed with
17688    ///   it.
17689    /// - **every template keeps its `{}`** — without the slot,
17690    ///   `str::replace` is a no-op, baseline and candidate are computed
17691    ///   from the same string, and the comparison degenerates to
17692    ///   `x == x`.
17693    ///
17694    /// Sharing one driver means no test can lose any of the three.
17695    fn for_each_case(check: impl Fn(LANG, Case)) {
17696        let mut checked = 0;
17697        for lang in LANG::into_enum_iter() {
17698            if !lang.is_enabled() {
17699                continue;
17700            }
17701            let Some(case @ (slots, _, numerics, expected_kinds)) = cases(lang) else {
17702                continue;
17703            };
17704            // Emptying a row is the half the length check below cannot
17705            // see: `&[]` against an `expected_kinds` of 0 agrees with
17706            // itself, the inner loops run zero times, and `checked` —
17707            // which counts languages, not spellings — still reads
17708            // satisfied.
17709            assert!(
17710                !numerics.is_empty(),
17711                "{lang:?}: the numeric-operand list is empty; this language asserted nothing"
17712            );
17713            assert_eq!(
17714                numerics.len(),
17715                expected_kinds,
17716                "{lang:?}: the numeric-operand list no longer covers one spelling \
17717                 per grammar numeric kind"
17718            );
17719            for (template, _, _) in slots {
17720                assert!(
17721                    template.contains("{}"),
17722                    "{lang:?}: template lost its `{{}}` operand slot: {template}"
17723                );
17724            }
17725            check(lang, case);
17726            checked += 1;
17727        }
17728        assert!(
17729            checked > 0,
17730            "no truthy-valued language enabled; this test asserted nothing"
17731        );
17732    }
17733
17734    #[test]
17735    fn a_numeric_operand_scores_like_an_identifier_operand() {
17736        for_each_case(|lang, (slots, identifier, numerics, _)| {
17737            for (template, _, _) in slots {
17738                let baseline = conditions(lang, &template.replace("{}", identifier));
17739                for numeric in numerics {
17740                    let source = template.replace("{}", numeric);
17741                    let scored = conditions(lang, &source);
17742                    assert_eq!(
17743                        scored, baseline,
17744                        "{lang:?}: `{numeric}` scored {scored} unary conditions against \
17745                         `{identifier}`'s {baseline}\n  source: {source}"
17746                    );
17747                }
17748            }
17749        });
17750    }
17751
17752    /// The absolute anchor under the comparison above: every operand
17753    /// spelling must produce the slot's recorded `conditions`, and must
17754    /// leave `cyclomatic` alone. The second half is what rules out a
17755    /// regression that moved both metrics together.
17756    #[test]
17757    fn every_operand_scores_its_recorded_values() {
17758        for_each_case(|lang, (slots, identifier, numerics, _)| {
17759            for (template, expected_conditions, expected_cyclomatic) in slots {
17760                for operand in std::iter::once(identifier).chain(numerics.iter().copied()) {
17761                    let source = template.replace("{}", operand);
17762                    assert_eq!(
17763                        conditions(lang, &source),
17764                        expected_conditions,
17765                        "{lang:?}: `{operand}` conditions\n  source: {source}"
17766                    );
17767                    assert_eq!(
17768                        cyclomatic_sum(lang, &source),
17769                        expected_cyclomatic,
17770                        "{lang:?}: `{operand}` cyclomatic_sum\n  source: {source}"
17771                    );
17772                }
17773            }
17774        });
17775    }
17776}
17777
17778/// A relational construct with its own grammar production must score
17779/// like an identifier in the same boolean slot (#1449, #1461).
17780///
17781/// Every language here spells at least one boolean test as a dedicated
17782/// node rather than as a `binary_expression`, so no comparison-token
17783/// arm ever sees it. A kind missing from the former per-language
17784/// terminal-kind sets scored **zero**, silently, and zero is
17785/// indistinguishable from a legitimate zero — which is why the headline
17786/// claim of each case is a *comparison* against an identifier control
17787/// in the identical slot rather than an absolute number.
17788///
17789/// The constructs, and the route each fix took:
17790///
17791/// - **Groovy** `a in l` / `a !in l` (`membership_expression`), counted
17792///   as a node by `groovy_count_token_condition`. The other four —
17793///   `a === b` / `a !== b` (`identity_expression`) and `s =~ /p/` /
17794///   `s ==~ /p/` (`regex_find_expression`, `regex_match_expression`) —
17795///   are counted there as operator tokens, so all five score outside a
17796///   boolean slot as `==` does, and `groovy_condition_scores_itself`
17797///   lists each so the slot holding one does not pay again; a construct
17798///   takes exactly one of the two routes, never both
17799///   (`.claude/rules/grammar-dispatch.md` §5).
17800/// - **Perl** `/^#/` (`pattern_matcher`) and `m{^#}`
17801///   (`pattern_matcher_m`), the two spellings of a match against the
17802///   implicit `$_`. They are sibling rules, not aliases, so both are
17803///   covered — a fixture carrying only the first would have left the
17804///   `m{}` half at zero and read as covered. Since #1467 they take the
17805///   other route: a gated arm in `PerlCode::compute` counts them by use,
17806///   and they have left the former `perl_bool_terminal_kinds!()`.
17807/// - **Ruby** `a in Integer` (`test_pattern`), the one-line pattern
17808///   test. Decided against `match_pattern` (`expr => pat`), which
17809///   raises rather than yielding a boolean.
17810/// - **Objective-C** `@available(iOS 13.0, *)` and its
17811///   `__builtin_available` synonym (`available_expression`), the runtime
17812///   OS-version check. Not relational, but boolean by definition and
17813///   invisible to every comparison-token arm, so it measured short in
17814///   exactly the same way (#1457). Its entry landed in the *name*-keyed
17815///   former `cpp_bool_terminal_kinds!()` that C, C++ and Mozcpp shared, which is
17816///   why `the_c_family_grammars_do_not_emit_available_expression` below
17817///   pins the inertness the other three rely on.
17818///
17819/// Rust's `matches!(…)` measures short in the same way, and is
17820/// deliberately **not** here: its fix is `macro_invocation`, which also
17821/// catches `cfg!` / `dbg!` / `todo!`, and the one corpus snapshot it
17822/// moves is a `cfg!` rather than a `matches!`. That breadth is what
17823/// #1461 item 2 exists to decide, so it is left to that issue rather
17824/// than settled by inclusion here.
17825///
17826/// Two slots per language, because they are two independent walker
17827/// paths (§11) and every construct above measured short in **both**: the
17828/// operands of a `&&` chain, and the predicate of an `if`. A fixture of
17829/// only one leaves the other untested.
17830///
17831/// Each row also carries the construct's negated spelling, which reaches
17832/// the construct only through the language's value peel: the `!…` /
17833/// `(…)` wrapper is peeled before `count_boolean_slot` asks
17834/// `<lang>_condition_scores_itself` whether the operand already paid.
17835/// Without a negated row the peel is the path no fixture here exercises.
17836///
17837/// Since #1526 no language here has a terminal-kind set: the chain
17838/// operands and the slots both score through the shared
17839/// `count_boolean_slot` rule, and Perl's rows score through the by-use
17840/// arm (#1467).
17841///
17842/// The `cyclomatic` half of `every_construct_scores_its_recorded_values`
17843/// is what rules out a regression that moved both metrics together:
17844/// cyclomatic must not move when only the operand spelling does, which
17845/// is what makes a `conditions` move unambiguously ABC's. The recorded
17846/// figures differ per slot (Rust's chain slot carries the extra `&&`
17847/// decision), so each slot pins its own pair rather than sharing one.
17848// Gated on the union of the three languages with rows, so a build
17849// enabling none of them drops the module instead of failing its
17850// guards (`.claude/rules/testing.md`, #1286 / #1411).
17851#[cfg(test)]
17852#[cfg(any(
17853    feature = "groovy",
17854    feature = "perl",
17855    feature = "ruby",
17856    feature = "objc"
17857))]
17858mod own_production_bool_constructs {
17859    use crate::test_support::metrics_verbatim;
17860    use crate::{LANG, MetricsOptions};
17861
17862    /// One fixture shape: a source template with a `{}` slot for the
17863    /// whole boolean expression, and the `abc.conditions_sum` /
17864    /// `cyclomatic_sum` every spelling in that slot must produce.
17865    type Slot = (&'static str, u64, u64);
17866
17867    /// A language's two slots, its identifier control, the constructs
17868    /// that must score the same, and how many of those there should be.
17869    ///
17870    /// The count is not bookkeeping. `for_each_case` counts *languages*,
17871    /// so trimming Groovy's list back to a single construct — or
17872    /// dropping Perl's `m{}` half, which is a separate grammar rule from
17873    /// its `//` half — leaves the module green with that coverage
17874    /// deleted. Pinning the length makes such a trim a deliberate
17875    /// two-line edit instead of a silent one.
17876    type Case = ([Slot; 2], &'static str, &'static [&'static str], usize);
17877
17878    fn conditions(lang: LANG, source: &str) -> u64 {
17879        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
17880            .abc
17881            .conditions_sum()
17882    }
17883
17884    fn cyclomatic_sum(lang: LANG, source: &str) -> u64 {
17885        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
17886            .cyclomatic
17887            .cyclomatic_sum()
17888    }
17889
17890    /// `([chain_slot, predicate_slot], identifier, constructs)` per
17891    /// language.
17892    ///
17893    /// `{}` is the left-hand operand of a two-operand short-circuit
17894    /// chain in the first slot and the whole `if` predicate in the
17895    /// second, so every fixture differs from its own control in exactly
17896    /// the construct under test.
17897    ///
17898    /// Ruby's chain slot parenthesises the slot because `in` binds
17899    /// looser than `&&` there; the control is parenthesised identically,
17900    /// so the parens cancel out of the comparison and the peel unwraps
17901    /// them for both sides alike.
17902    fn cases(lang: LANG) -> Option<Case> {
17903        Some(match lang {
17904            LANG::Groovy => (
17905                [
17906                    ("def f(a, b, l, s) {\n  return {} && b\n}\n", 2, 3),
17907                    ("def f(a, b, l, s) {\n  if ({}) { return 1 }\n}\n", 1, 3),
17908                ],
17909                "b",
17910                &[
17911                    "a in l",
17912                    "a !in l",
17913                    "a === b",
17914                    "a !== b",
17915                    "s =~ /p/",
17916                    "s ==~ /p/",
17917                    "!(a in l)",
17918                    // The indexing / navigation kinds added by #1466.
17919                    // These two are the cyclomatic-neutral ones, so
17920                    // they belong in this module's same-as-the-control
17921                    // shape. The three *null-safe* spellings (`a?.b`,
17922                    // `a??.b`, `l?[0]`) each short-circuit on a null
17923                    // receiver and so add a cyclomatic decision, which
17924                    // a row whose contract is "the control's
17925                    // cyclomatic" cannot express; they get their own
17926                    // test,
17927                    // `groovy_safe_navigation_closes_the_two_below_gap`,
17928                    // which anchors them on that axis explicitly.
17929                    "l[0]",
17930                    "a.@b",
17931                ],
17932                9,
17933            ),
17934            LANG::Perl => (
17935                [
17936                    ("sub f {\n  my $x = {} && $b;\n}\n", 2, 3),
17937                    ("sub f {\n  if ({}) { 1; }\n}\n", 1, 3),
17938                ],
17939                "$a",
17940                &["/^#/", "m{^#}", "!/^#/"],
17941                3,
17942            ),
17943            LANG::Ruby => (
17944                [
17945                    ("def f(a)\n  ({}) && b\nend\n", 2, 3),
17946                    ("def f(a)\n  if {}\n    1\n  end\nend\n", 1, 3),
17947                ],
17948                "a",
17949                &["a in Integer", "!(a in Integer)"],
17950                2,
17951            ),
17952            // Objective-C's `@available` is not relational like the
17953            // constructs above, but it reaches the terminal set by the
17954            // route Groovy's `in` takes: a dedicated `available_expression`
17955            // production whose value is a boolean, seen by no
17956            // comparison-token arm (#1457). Both spellings are listed
17957            // because they are alternatives of one rule's leading token
17958            // rather than two rules — so unlike Perl's `m{}` above they
17959            // cannot drift apart, and the second row is here to keep that
17960            // claim measured rather than assumed.
17961            LANG::Objc => (
17962                [
17963                    ("int f(int a) {\n  return {} && b;\n}\n", 2, 3),
17964                    ("int f() {\n  if ({}) { return 1; }\n}\n", 1, 3),
17965                ],
17966                "a",
17967                &[
17968                    "@available(iOS 13.0, *)",
17969                    "__builtin_available(macOS 10.15, *)",
17970                    "!@available(iOS 13.0, *)",
17971                ],
17972                3,
17973            ),
17974            _ => return None,
17975        })
17976    }
17977
17978    /// Runs `check` once per enabled language that has a case, having
17979    /// first established that the case can still assert something.
17980    ///
17981    /// The four guards mirror `numeric_bool_operands::for_each_case`,
17982    /// where each was added only after a measured perturbation of it
17983    /// left that module green: `checked > 0` for the runtime half of
17984    /// the feature gate, a non-empty construct list and its recorded
17985    /// count so a trimmed row cannot vanish silently, and the `{}` slot
17986    /// so `str::replace` does not degenerate into comparing a string
17987    /// with itself.
17988    fn for_each_case(check: impl Fn(LANG, Case)) {
17989        let mut checked = 0;
17990        for lang in LANG::into_enum_iter() {
17991            if !lang.is_enabled() {
17992                continue;
17993            }
17994            let Some(case @ (slots, _, constructs, expected_constructs)) = cases(lang) else {
17995                continue;
17996            };
17997            // Both halves are needed, and the second does not imply the
17998            // first. The length check alone only asserts that the list
17999            // and its recorded count *agree*, so emptying a row and
18000            // setting its count to 0 — the shape a careless "the test
18001            // failed, fix the number" edit takes — deleted that
18002            // language's coverage with both tests still green when
18003            // measured.
18004            assert!(
18005                !constructs.is_empty(),
18006                "{lang:?}: the construct list is empty; this language asserted nothing"
18007            );
18008            assert_eq!(
18009                constructs.len(),
18010                expected_constructs,
18011                "{lang:?}: the construct list no longer covers every own-production \
18012                 boolean spelling this language was fixed for"
18013            );
18014            for (template, _, _) in slots {
18015                assert!(
18016                    template.contains("{}"),
18017                    "{lang:?}: template lost its `{{}}` slot: {template}"
18018                );
18019            }
18020            check(lang, case);
18021            checked += 1;
18022        }
18023        assert!(
18024            checked > 0,
18025            "no language with an own-production boolean construct enabled; \
18026             this test asserted nothing"
18027        );
18028    }
18029
18030    #[test]
18031    fn an_own_production_construct_scores_like_an_identifier() {
18032        for_each_case(|lang, (slots, identifier, constructs, _)| {
18033            for (template, _, _) in slots {
18034                let baseline = conditions(lang, &template.replace("{}", identifier));
18035                for construct in constructs {
18036                    let source = template.replace("{}", construct);
18037                    let scored = conditions(lang, &source);
18038                    assert_eq!(
18039                        scored, baseline,
18040                        "{lang:?}: `{construct}` scored {scored} conditions against \
18041                         `{identifier}`'s {baseline}\n  source: {source}"
18042                    );
18043                }
18044            }
18045        });
18046    }
18047
18048    /// Groovy's three null-safe spellings, on the cyclomatic axis.
18049    ///
18050    /// They cannot ride the rows above, whose contract is "scores the
18051    /// control's `conditions` *and* the control's `cyclomatic`":
18052    /// the boolean-slot rule (`groovy_count_condition`) does not move cyclomatic, but
18053    /// `?.` (`QMARKDOT`), `??.` (`QMARKQMARKDOT`) and `?[`
18054    /// (`QMARKLBRACK`) are already cyclomatic decisions in their own
18055    /// right (`src/metrics/cyclomatic/groovy.rs`), so each spelling
18056    /// scores the control's conditions against the control's cyclomatic
18057    /// **plus one**. That is the whole reason this family was the worst
18058    /// case in #1466: before the fix ABC sat *two* below its own
18059    /// decision count on `if (a?.b)`, against one below for every other
18060    /// spelling.
18061    ///
18062    /// `l?[0]` joined the family in #1471. It sat in the neutral rows
18063    /// above until then, passing only because Groovy cyclomatic had no
18064    /// `?[` arm — a fixture that would have rejected the fix for the
18065    /// very gap it recorded.
18066    ///
18067    /// Asserting the offset rather than the bare conditions is what
18068    /// makes this a §5 double-count guard as well. If a later change
18069    /// added `QMARKDOT` to `groovy_count_token_condition`'s arm while
18070    /// the wrapper stayed in the terminal set, `conditions` would go to
18071    /// `control + 1` and this test — not a snapshot — would say so.
18072    #[test]
18073    #[cfg(feature = "groovy")]
18074    fn groovy_safe_navigation_closes_the_two_below_gap() {
18075        for (template, control_conditions, control_cyclomatic) in [
18076            ("def f(a, b, l) {\n  return {} && b\n}\n", 2, 3),
18077            ("def f(a, b, l) {\n  if ({}) { return 1 }\n}\n", 1, 3),
18078        ] {
18079            let control = template.replace("{}", "b");
18080            assert_eq!(conditions(LANG::Groovy, &control), control_conditions);
18081            assert_eq!(cyclomatic_sum(LANG::Groovy, &control), control_cyclomatic);
18082
18083            for spelling in ["a?.b", "a??.b", "l?[0]"] {
18084                let source = template.replace("{}", spelling);
18085                assert_eq!(
18086                    conditions(LANG::Groovy, &source),
18087                    control_conditions,
18088                    "`{spelling}` conditions\n  source: {source}"
18089                );
18090                assert_eq!(
18091                    cyclomatic_sum(LANG::Groovy, &source),
18092                    control_cyclomatic + 1,
18093                    "`{spelling}` cyclomatic_sum: the navigation operator's own \
18094                     decision has moved\n  source: {source}"
18095                );
18096            }
18097        }
18098    }
18099
18100    /// The `!` operand is read by field, so an `extra` cannot hide it.
18101    ///
18102    /// `groovy_wrapper_operand` took `child(1)` until #1466, which is
18103    /// the `!` operand only when nothing sits between the two. A comment
18104    /// is an `extra` and occupies that slot, so `if (! /*c*/ a)` scored
18105    /// zero conditions where `if (!a)` scored one — the positional-read
18106    /// class grammar-dispatch §3 is about, and the observable half of
18107    /// the peel rewrite.
18108    ///
18109    /// `parenthesized_expression` names nothing in node-types.json, so
18110    /// it kept the positional read, and the bug, until #1455 read it as
18111    /// the first operand that is not an extra.
18112    #[test]
18113    #[cfg(feature = "groovy")]
18114    fn groovy_negation_operand_survives_an_interposed_comment() {
18115        let template = "def f(a) {\n  if ({}) { return 1 }\n}\n";
18116
18117        assert_eq!(conditions(LANG::Groovy, &template.replace("{}", "!a")), 1);
18118        assert_eq!(
18119            conditions(LANG::Groovy, &template.replace("{}", "! /*c*/ a")),
18120            1,
18121            "the `!` operand is being read positionally again; a comment displaces it"
18122        );
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 parenthesised operand is being read positionally again; a comment \
18129             displaces it"
18130        );
18131    }
18132
18133    /// The arithmetic unary operators are values, not wrappers.
18134    ///
18135    /// `groovy_count_condition` routed every `unary_expression` to the
18136    /// peel while the peel handled only the `!` spelling, so the arm
18137    /// claimed `~a` / `-a` / `+a` and dropped them
18138    /// (grammar-dispatch §7). Since #1526 the slot pays for any
18139    /// predicate no other arm counts — Groovy truth makes `if (-a)` a
18140    /// decision — so the three spellings score what `a` does. Outside a
18141    /// slot they are no negation, so a call argument scores nothing for
18142    /// them, while `!-a` scores its negation once.
18143    #[test]
18144    #[cfg(feature = "groovy")]
18145    fn groovy_arithmetic_unary_is_a_value_not_a_wrapper() {
18146        let template = "def f(a) {\n  if ({}) { return 1 }\n}\n";
18147        let call = "def f(a) {\n  g({})\n}\n";
18148
18149        assert_eq!(conditions(LANG::Groovy, &template.replace("{}", "a")), 1);
18150        for spelling in ["~a", "-a", "+a"] {
18151            assert_eq!(
18152                conditions(LANG::Groovy, &template.replace("{}", spelling)),
18153                1,
18154                "`{spelling}` in a slot pays as `a` does"
18155            );
18156            assert_eq!(
18157                conditions(LANG::Groovy, &call.replace("{}", spelling)),
18158                0,
18159                "`{spelling}` outside a slot is not a negation"
18160            );
18161            let negated = format!("!{spelling}");
18162            assert_eq!(
18163                conditions(LANG::Groovy, &call.replace("{}", &negated)),
18164                1,
18165                "`{negated}` outside a slot is one negation"
18166            );
18167        }
18168    }
18169
18170    /// The two Perl spellings must remain two distinct grammar kinds.
18171    ///
18172    /// The bare-match arm of `PerlCode::compute` lists `PatternMatcher`
18173    /// **and** `PatternMatcherM` on the stated grounds that `/^#/` and `m{^#}`
18174    /// are sibling rules rather than aliases of one. Nothing above pins
18175    /// that: if a grammar bump collapsed `m{}` onto `pattern_matcher`,
18176    /// both fixtures would keep scoring through the surviving entry,
18177    /// the `PatternMatcherM` arm would become dead code, and every test
18178    /// in this module would stay green — the silent-drift shape lesson
18179    /// 34 and grammar-dispatch §2 exist to catch.
18180    ///
18181    /// Asserting the ids differ is not enough on its own, since two
18182    /// distinct enum variants prove nothing about what the parser
18183    /// emits. So each spelling is parsed and required to produce its
18184    /// own kind and not the other's.
18185    #[test]
18186    #[cfg(feature = "perl")]
18187    fn the_two_perl_match_spellings_are_distinct_kinds() {
18188        use crate::{ParserTrait, Perl, PerlParser};
18189        use big_code_analysis_ast::test_support::ast_has_kind_id;
18190
18191        assert_ne!(
18192            Perl::PatternMatcher as u16,
18193            Perl::PatternMatcherM as u16,
18194            "the two spellings share one kind id; one bare-match arm entry is dead"
18195        );
18196
18197        for (src, present, absent) in [
18198            (
18199                "sub f { if (/^#/) { 1; } }",
18200                ("pattern_matcher", Perl::PatternMatcher as u16),
18201                ("pattern_matcher_m", Perl::PatternMatcherM as u16),
18202            ),
18203            (
18204                "sub f { if (m{^#}) { 1; } }",
18205                ("pattern_matcher_m", Perl::PatternMatcherM as u16),
18206                ("pattern_matcher", Perl::PatternMatcher as u16),
18207            ),
18208        ] {
18209            let parser = PerlParser::new(
18210                src.as_bytes().to_vec(),
18211                &std::path::PathBuf::from("f.pl"),
18212                None,
18213            );
18214            let (present_name, present_id) = present;
18215            let (absent_name, absent_id) = absent;
18216            assert!(
18217                ast_has_kind_id(&parser, present_id),
18218                "`{src}` no longer emits `{present_name}`"
18219            );
18220            assert!(
18221                !ast_has_kind_id(&parser, absent_id),
18222                "`{src}` now also emits `{absent_name}`; the two spellings have \
18223                 collapsed and one bare-match arm entry is dead"
18224            );
18225        }
18226    }
18227
18228    /// The absolute anchor under the comparison above: every spelling
18229    /// must produce the slot's recorded `conditions`, and must leave
18230    /// `cyclomatic` alone.
18231    #[test]
18232    fn every_construct_scores_its_recorded_values() {
18233        for_each_case(|lang, (slots, identifier, constructs, _)| {
18234            for (template, expected_conditions, expected_cyclomatic) in slots {
18235                for spelling in std::iter::once(identifier).chain(constructs.iter().copied()) {
18236                    let source = template.replace("{}", spelling);
18237                    assert_eq!(
18238                        conditions(lang, &source),
18239                        expected_conditions,
18240                        "{lang:?}: `{spelling}` conditions\n  source: {source}"
18241                    );
18242                    assert_eq!(
18243                        cyclomatic_sum(lang, &source),
18244                        expected_cyclomatic,
18245                        "{lang:?}: `{spelling}` cyclomatic_sum\n  source: {source}"
18246                    );
18247                }
18248            }
18249        });
18250    }
18251}
18252
18253/// A Perl statement modifier must score like the block form it is
18254/// shorthand for (#1464).
18255///
18256/// `return 1 if $x;` and `if ($x) { return 1; }` are the same decision
18257/// written two ways, and the modifier is the idiomatic Perl spelling —
18258/// `next unless $ok;`, `warn "..." if $debug;`. Perl's *cyclomatic*
18259/// dispatcher already counted all six modifier kinds, so this was a
18260/// straight ABC undercount against Perl's own decision count rather
18261/// than a policy disagreement between the two metrics.
18262///
18263/// Each pair is asserted equal **and** equal to a literal, so a
18264/// regression that zeroed both spellings still fails. The fixtures are
18265/// spell-anchored because the modifier is the only thing in them that
18266/// scores a condition: deleting `if $x` from the modifier fixture would
18267/// otherwise turn it into a silent copy of nothing at all
18268/// (`.claude/rules/testing.md`, "Perturb the fixture as well as the
18269/// production line").
18270#[cfg(test)]
18271#[cfg(feature = "perl")]
18272mod perl_statement_modifier_parity {
18273    use crate::test_support::{assert_perl_fixture_spells, metrics_verbatim};
18274    use crate::{LANG, MetricsOptions, Perl};
18275
18276    fn conditions(source: &str) -> u64 {
18277        metrics_verbatim(LANG::Perl, source.as_bytes(), MetricsOptions::default())
18278            .abc
18279            .conditions_sum()
18280    }
18281
18282    /// `(keyword, kind id, kind name)` — the four modifier keywords
18283    /// whose grammar slot is a `condition`. `for` / `foreach` is the
18284    /// fifth spelling and is covered by its own test below; `when` is
18285    /// the sixth and has no fixture at all, being unreachable from
18286    /// valid Perl (see `perl_walk_statement_modifier`).
18287    const CONDITION_MODIFIERS: [(&str, u16, &str); 4] = [
18288        ("if", Perl::IfSimpleStatement as u16, "if_simple_statement"),
18289        (
18290            "unless",
18291            Perl::UnlessSimpleStatement as u16,
18292            "unless_simple_statement",
18293        ),
18294        (
18295            "while",
18296            Perl::WhileSimpleStatement as u16,
18297            "while_simple_statement",
18298        ),
18299        (
18300            "until",
18301            Perl::UntilSimpleStatement as u16,
18302            "until_simple_statement",
18303        ),
18304    ];
18305
18306    #[test]
18307    fn a_modifier_scores_like_its_block_form() {
18308        assert!(
18309            LANG::Perl.is_enabled(),
18310            "compiled under `feature = \"perl\"` but `LANG::Perl` reports disabled; \
18311             this test asserted nothing"
18312        );
18313        for (keyword, kind, name) in CONDITION_MODIFIERS {
18314            let modifier = format!("sub f {{ my $x = shift; return 1 {keyword} $x; }}");
18315            let block = format!("sub f {{ my $x = shift; {keyword} ($x) {{ return 1; }} }}");
18316            assert_perl_fixture_spells(&modifier, &[(kind, 1, name)]);
18317            assert_eq!(
18318                conditions(&modifier),
18319                conditions(&block),
18320                "`{keyword}` modifier and block form disagree\n  modifier: {modifier}\n  block:    {block}"
18321            );
18322            assert_eq!(
18323                conditions(&modifier),
18324                1,
18325                "`{keyword}` modifier: the bare scalar predicate is one condition"
18326            );
18327        }
18328    }
18329
18330    /// The modifier's `condition` field goes through the shared
18331    /// condition classifier, not a flat `+1`, so a compound predicate
18332    /// keeps its sub-structure — three shapes the block form already
18333    /// scores above and below one.
18334    #[test]
18335    fn a_modifier_condition_keeps_its_substructure() {
18336        for (predicate, expected) in [("!$x", 1), ("($x)", 1), ("$x && $x", 2), ("$x > 2", 1)] {
18337            let modifier = format!("sub f {{ my $x = shift; return 1 if {predicate}; }}");
18338            let block = format!("sub f {{ my $x = shift; if ({predicate}) {{ return 1; }} }}");
18339            assert_perl_fixture_spells(
18340                &modifier,
18341                &[(Perl::IfSimpleStatement as u16, 1, "if_simple_statement")],
18342            );
18343            assert_eq!(
18344                conditions(&modifier),
18345                expected,
18346                "`if {predicate}` modifier\n  source: {modifier}"
18347            );
18348            assert_eq!(
18349                conditions(&block),
18350                expected,
18351                "`if ({predicate})` block form\n  source: {block}"
18352            );
18353        }
18354    }
18355
18356    /// The `for` / `foreach` modifier iterates a list and is not a
18357    /// boolean test — the grammar names its slot `list`, not
18358    /// `condition`. Pinned against the `if` modifier so the zero reads
18359    /// as a decision rather than as an arm nothing reaches.
18360    ///
18361    /// Measured: simply *adding* `ForSimpleStatement` to the dispatch
18362    /// arm does not fail this test, because the walker reads the slot by
18363    /// field name and `for_simple_statement` exposes no `condition` —
18364    /// the exclusion is structural, not a listed omission. What it does
18365    /// catch is a rework that reaches the slot some other way (a child
18366    /// index, the `list` field) or a grammar that renames the field.
18367    #[test]
18368    fn the_for_modifier_is_not_a_condition() {
18369        for keyword in ["for", "foreach"] {
18370            let source = format!("sub f {{ my $x = shift; print 5 {keyword} @$x; }}");
18371            assert_perl_fixture_spells(
18372                &source,
18373                &[(Perl::ForSimpleStatement as u16, 1, "for_simple_statement")],
18374            );
18375            let abc =
18376                metrics_verbatim(LANG::Perl, source.as_bytes(), MetricsOptions::default()).abc;
18377            assert_eq!(abc.conditions_sum(), 0, "`{keyword}` modifier: {source}");
18378            // The expected value *is* the default, so the zero above
18379            // proves nothing on its own — a fixture that stopped
18380            // parsing would satisfy it too. `my $x = shift` is the
18381            // file's one assignment, and `shift` and `print 5` its two
18382            // branches; all three must survive.
18383            assert_eq!(
18384                abc.assignments_sum(),
18385                1,
18386                "`{keyword}`: fixture stopped scoring"
18387            );
18388            assert_eq!(
18389                abc.branches_sum(),
18390                2,
18391                "`{keyword}`: fixture stopped scoring"
18392            );
18393        }
18394        assert_eq!(
18395            conditions("sub f { my $x = shift; return 1 if $x; }"),
18396            1,
18397            "control: the `if` modifier still counts, so the zero above is a \
18398             policy decision and not a dead arm"
18399        );
18400    }
18401}
18402
18403/// A non-numeric literal in a boolean operand slot must score like an
18404/// identifier in the same slot (#1462).
18405///
18406/// The sibling module above pins the *numeric* half, closed by #1410.
18407/// The non-numeric literals were never swept and were missing from
18408/// every set the numerics were added to — same mechanism, same silent
18409/// zero, same file. Measured before the fix, every row below scored
18410/// exactly one condition short of its identifier control, in **both**
18411/// slots: `x || "default"` — the language's commonest truthy-default
18412/// idiom — reported 1 against `x || y`'s 2.
18413///
18414/// Scope is the eleven truthy-valued sets. #1462 shipped eight of them
18415/// and called that complete; a whole-branch review of the batch found
18416/// Perl, Ruby and Elixir in neither the swept list nor the
18417/// compile-error exemption, each already carrying `False` / `Nil`, and
18418/// each reproducing the gap exactly. They were swept on the same terms
18419/// and are rows here. Tcl and iRules are truthy too and needed nothing:
18420/// their `quoted_word` / `braced_word_simple` / `number` kinds already
18421/// cover every literal an `expr {…}` operand can hold, so they have no
18422/// row rather than a vacuous one.
18423///
18424/// C#, Java, Kotlin, Rust and Go name no literal kind at all and stay
18425/// that way: a bare literal in a boolean slot is a compile error there,
18426/// so there is nothing to count. The C family is integer-truthy, and
18427/// #1462 deferred it for its corpus exposure. #1526 then closed its gap
18428/// without naming a kind, by making every slot pay for an occupant no
18429/// other arm counts; its rows here (#1469) pin that answer, because a
18430/// kind `cpp_condition_scores_itself` wrongly claimed would drop a slot
18431/// exactly as the missing terminal kind once did. Its `true`, `false`
18432/// and `null` rows are the exception to "measured short": the former
18433/// set already named them, and they stay so the pin covers every kind.
18434///
18435/// Three things each row pins that a conditions comparison alone
18436/// cannot:
18437///
18438/// - **The `kind_id` each spelling parses to.** This is the whole bug
18439///   class (`.claude/rules/grammar-dispatch.md` §1): the grammars here
18440///   emit `string` under two ids in JavaScript, Mozjs and Tsx and under
18441///   three in Tsx counting the type keyword, and listing the wrong one
18442///   compiles, runs, and returns the unfixed number. Asserting the id
18443///   turns that measurement into a standing claim rather than a note in
18444///   an issue. It is also the fixture anchor `.claude/rules/testing.md`
18445///   asks for — editing `"s"` to `1` in a row now fails by name instead
18446///   of quietly turning that row into a copy of the numeric module.
18447/// - **`cyclomatic` does not move.** Only the operand spelling changes
18448///   between a row and its control, so a `conditions` difference is
18449///   unambiguously ABC's.
18450/// - **Both walker paths.** The sets feed two structurally independent
18451///   consumers (§11) — the operands of a `&&` / `and` chain, and the
18452///   predicate of an `if` — and every row measured short in both. A
18453///   fixture of only one leaves the other path invisible.
18454///
18455/// The JS-family rows go through `js_literals!` rather than four hand-
18456/// written arrays, because the four languages differ in exactly one
18457/// place — which `string` alias an operand slot carries — and making
18458/// that the macro's only argument states the asymmetry instead of
18459/// leaving a reader to diff four near-identical lists for it.
18460#[cfg(test)]
18461#[cfg(any(
18462    feature = "javascript",
18463    feature = "mozjs",
18464    feature = "typescript",
18465    feature = "python",
18466    feature = "lua",
18467    feature = "php",
18468    feature = "groovy",
18469    feature = "perl",
18470    feature = "ruby",
18471    feature = "elixir",
18472    feature = "c",
18473    feature = "cpp",
18474    feature = "mozcpp",
18475    feature = "objc"
18476))]
18477mod literal_bool_operands {
18478    use crate::test_support::{assert_fixture_spells, metrics_verbatim};
18479    use crate::{LANG, MetricsOptions};
18480
18481    /// One fixture shape: a source template with a `{}` operand slot,
18482    /// and the `abc.conditions_sum` / `cyclomatic_sum` every spelling of
18483    /// that operand must produce.
18484    type Slot = (&'static str, u64, u64);
18485
18486    /// One literal operand: its spelling, and the `kind_id` the grammar
18487    /// must emit for it.
18488    type Literal = (&'static str, u16);
18489
18490    /// A language's two slots, its identifier baseline operand, the
18491    /// literal operands that must score the same, and how many of those
18492    /// there should be.
18493    ///
18494    /// The count is not bookkeeping. `for_each_case` counts *languages*,
18495    /// so trimming a row's literal list — the pre-#1462 state is the
18496    /// empty list — would leave the whole module green.
18497    type Case = ([Slot; 2], &'static str, &'static [Literal], usize);
18498
18499    /// The seven non-numeric literal kinds of a JS-family grammar, with
18500    /// the `string` alias an operand slot carries passed in: `String2`
18501    /// for JavaScript, Mozjs and Tsx, `String` for TypeScript. That one
18502    /// argument is the entire difference between the four rows, and it
18503    /// is the same split `Checker::is_string` makes (§7).
18504    macro_rules! js_literals {
18505        ($Lang:ident, $string:ident) => {
18506            &[
18507                ("\"s\"", crate::$Lang::$string as u16),
18508                ("`t`", crate::$Lang::TemplateString as u16),
18509                ("/re/", crate::$Lang::Regex as u16),
18510                ("null", crate::$Lang::Null as u16),
18511                ("undefined", crate::$Lang::Undefined as u16),
18512                ("{}", crate::$Lang::Object as u16),
18513                ("[]", crate::$Lang::Array as u16),
18514            ]
18515        };
18516    }
18517
18518    const JS_SLOTS: [Slot; 2] = [
18519        ("function f(a) {\n  return a && {};\n}\n", 2, 3),
18520        ("function f() {\n  if ({}) { return 1; }\n}\n", 1, 3),
18521    ];
18522
18523    /// The five non-numeric literal kinds every C-family grammar shares,
18524    /// then the grammar's own. The string and null spellings are passed
18525    /// in because Objective-C writes its strings `@"s"` and C++ its null
18526    /// `nullptr`; each still parses to the shared `string_literal` /
18527    /// `null` kind, so the row stays one spelling per kind.
18528    macro_rules! c_family_literals {
18529        ($Lang:ident, $string:literal, $null:literal $(, $extra:expr)* $(,)?) => {
18530            &[
18531                ($string, crate::$Lang::StringLiteral as u16),
18532                ("\"a\" \"b\"", crate::$Lang::ConcatenatedString as u16),
18533                ("true", crate::$Lang::True as u16),
18534                ("false", crate::$Lang::False as u16),
18535                ($null, crate::$Lang::Null as u16),
18536                $($extra,)*
18537            ]
18538        };
18539    }
18540
18541    const C_FAMILY_SLOTS: [Slot; 2] = [
18542        ("int f(int a) {\n  return a && {};\n}\n", 2, 3),
18543        ("int f() {\n  if ({}) { return 1; }\n}\n", 1, 3),
18544    ];
18545
18546    fn conditions(lang: LANG, source: &str) -> u64 {
18547        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
18548            .abc
18549            .conditions_sum()
18550    }
18551
18552    fn cyclomatic_sum(lang: LANG, source: &str) -> u64 {
18553        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
18554            .cyclomatic
18555            .cyclomatic_sum()
18556    }
18557
18558    /// Asserts the fixture still spells the construct under test, by
18559    /// parsing it with the language's own parser. Split out of `cases`
18560    /// because only the parser types are feature-gated; the `kind_id`
18561    /// enums are compiled unconditionally.
18562    fn assert_spells(lang: LANG, source: &str, kinds: &[(u16, usize, &str)]) {
18563        match lang {
18564            #[cfg(feature = "javascript")]
18565            LANG::Javascript => {
18566                assert_fixture_spells::<crate::JavascriptParser>(source, "f.js", kinds);
18567            }
18568            #[cfg(feature = "mozjs")]
18569            LANG::Mozjs => assert_fixture_spells::<crate::MozjsParser>(source, "f.jsm", kinds),
18570            #[cfg(feature = "typescript")]
18571            LANG::Typescript => {
18572                assert_fixture_spells::<crate::TypescriptParser>(source, "f.ts", kinds);
18573            }
18574            #[cfg(feature = "typescript")]
18575            LANG::Tsx => assert_fixture_spells::<crate::TsxParser>(source, "f.tsx", kinds),
18576            #[cfg(feature = "python")]
18577            LANG::Python => assert_fixture_spells::<crate::PythonParser>(source, "f.py", kinds),
18578            #[cfg(feature = "lua")]
18579            LANG::Lua => assert_fixture_spells::<crate::LuaParser>(source, "f.lua", kinds),
18580            #[cfg(feature = "php")]
18581            LANG::Php => assert_fixture_spells::<crate::PhpParser>(source, "f.php", kinds),
18582            #[cfg(feature = "groovy")]
18583            LANG::Groovy => {
18584                assert_fixture_spells::<crate::GroovyParser>(source, "f.groovy", kinds);
18585            }
18586            #[cfg(feature = "perl")]
18587            LANG::Perl => assert_fixture_spells::<crate::PerlParser>(source, "f.pl", kinds),
18588            #[cfg(feature = "ruby")]
18589            LANG::Ruby => assert_fixture_spells::<crate::RubyParser>(source, "f.rb", kinds),
18590            #[cfg(feature = "elixir")]
18591            LANG::Elixir => assert_fixture_spells::<crate::ElixirParser>(source, "f.ex", kinds),
18592            #[cfg(feature = "c")]
18593            LANG::C => assert_fixture_spells::<crate::CParser>(source, "f.c", kinds),
18594            #[cfg(feature = "cpp")]
18595            LANG::Cpp => assert_fixture_spells::<crate::CppParser>(source, "f.cpp", kinds),
18596            #[cfg(feature = "mozcpp")]
18597            LANG::Mozcpp => assert_fixture_spells::<crate::MozcppParser>(source, "f.cpp", kinds),
18598            #[cfg(feature = "objc")]
18599            LANG::Objc => assert_fixture_spells::<crate::ObjcParser>(source, "f.m", kinds),
18600            other => panic!("{other:?} has a case row but no parser arm"),
18601        }
18602    }
18603
18604    /// `([chain_slot, condition_slot], identifier, literals)` per
18605    /// language, one spelling per literal *kind* the grammar emits.
18606    ///
18607    /// - JS family: `string`, `template_string`, `regex`, `null`,
18608    ///   `undefined`, `object`, `array`. TypeScript's `string` / `object`
18609    ///   type keywords (`a: string`) are different ids rendering to the
18610    ///   same names and are deliberately not in the sets, so a row that
18611    ///   accidentally named one would fail the id anchor here.
18612    /// - Python: `string` covers `'s'`, `"""s"""`, `f'x'` and `b'x'`,
18613    ///   while the implicit-join `'a' 'b'` is the separate
18614    ///   `concatenated_string` rule — the sibling-rule-under-a-supertype
18615    ///   shape #1379's Perl numerals were, which an alias sweep cannot
18616    ///   see. Then `none`, the four collection displays, and `ellipsis`.
18617    /// - Lua: one `string` kind for `"s"`, `'s'` and `[[s]]`, plus
18618    ///   `table_constructor`. Lua's is the strongest case of the eight:
18619    ///   everything but `false` and `nil` is truthy, which is why
18620    ///   `cond and "a" or "b"` is the language's ternary.
18621    /// - PHP: `string` (single-quoted) and `encapsed_string`
18622    ///   (interpolating) are separate rules, as are `heredoc` and
18623    ///   `nowdoc`; then `array_creation_expression`, `null`, the
18624    ///   backtick `shell_command_expression`, and `cast_expression` —
18625    ///   the second finding of #1462, PHP being the only set in the
18626    ///   Java / C# / Groovy / PHP group that named no cast kind.
18627    /// - Groovy: `string_literal` (which also covers the slashy `/re/`),
18628    ///   `null_literal`, `list_literal`, `map_literal`.
18629    /// - Perl: the four string productions the grammar keeps separate
18630    ///   (`'s'`, `q()`, `"s"`, `qq()`), the two command substitutions
18631    ///   (`qx()`, backticks), the `qw()` / `[…]` / `{…}` collection
18632    ///   literals, `qr//`, and `special_literal` (`__FILE__`).
18633    ///   `heredoc_initializer` is the twelfth kind and cannot live in
18634    ///   this table — a Perl heredoc body follows the *statement*, not
18635    ///   the operand — so it has its own test below.
18636    /// - Ruby: `string` (covering `"s"`, `'s'`, `%q()`, `%Q()`),
18637    ///   `chained_string` (the adjacent-literal join `"a" "b"`, a
18638    ///   sibling rule an alias sweep cannot see), `subshell`, the four
18639    ///   collection literals `array` / `hash` / `%w[]` / `%i[]`,
18640    ///   `regex`, the one-character `?a`, and both symbol productions.
18641    ///   `heredoc_beginning` is the twelfth and shares the Perl heredoc
18642    ///   test for the same reason.
18643    /// - Elixir: `string` (one kind for `"s"` and the `"""` heredoc),
18644    ///   `charlist`, `sigil` (one kind for `~r//`, `~s()`, `~w()`),
18645    ///   `quoted_atom` — a separate production from `atom`, so
18646    ///   `:"q a"` scored zero while `:atom` scored one — and the four
18647    ///   collection literals `list` / `tuple` / `map` / `bitstring`.
18648    /// - C family: `string_literal` (one kind for `"s"`, `L"s"`, `u8"s"`
18649    ///   and Objective-C's `@"s"`), `concatenated_string`, `true`,
18650    ///   `false` and `null` (`NULL`, and C++'s `nullptr`, which the
18651    ///   grammar wraps in a `null` node). C++ adds `raw_string_literal`
18652    ///   and `user_defined_literal` (`"s"_x`, and `1.0_km` around a
18653    ///   number); Objective-C adds its boxed forms — `array_literal`,
18654    ///   `dictionary_literal`, `at_expression` (`@1`, `@YES`, `@(b)`,
18655    ///   `@protocol(P)`), `selector_expression` and `encode_expression`.
18656    ///   `char_literal` is numeric and pinned in the sibling module.
18657    ///   Objective-C's `nil`, `YES` and `NO` parse as plain identifiers,
18658    ///   so they need no row of their own.
18659    ///
18660    /// Shapes that measured short and are not rows here, none of them a
18661    /// literal: JavaScript's `this` and Groovy's
18662    /// `object_creation_expression` (recorded in #1462); Perl's
18663    /// `s///` and `tr///` (operations on `$_` evaluating to a count),
18664    /// `anonymous_function` and `array_dereference`; Ruby's `lambda`;
18665    /// and Elixir's `anonymous_function` and `&f/1` capture. Every one
18666    /// pays its slot since #1526 (Elixir's since #1527), and
18667    /// `slot_pays_tests` pins them against an identifier control (#1475).
18668    fn cases(lang: LANG) -> Option<Case> {
18669        Some(match lang {
18670            LANG::Javascript => (JS_SLOTS, "b", js_literals!(Javascript, String2), 7),
18671            LANG::Mozjs => (JS_SLOTS, "b", js_literals!(Mozjs, String2), 7),
18672            LANG::Typescript => (JS_SLOTS, "b", js_literals!(Typescript, String), 7),
18673            LANG::Tsx => (JS_SLOTS, "b", js_literals!(Tsx, String2), 7),
18674            LANG::Python => (
18675                [
18676                    ("def f(a):\n    return a and {}\n", 2, 3),
18677                    ("def f():\n    if {}:\n        return 1\n", 1, 3),
18678                ],
18679                "b",
18680                &[
18681                    ("'s'", crate::Python::String as u16),
18682                    ("'a' 'b'", crate::Python::ConcatenatedString as u16),
18683                    ("None", crate::Python::None as u16),
18684                    ("[]", crate::Python::List as u16),
18685                    ("{1}", crate::Python::Set as u16),
18686                    ("()", crate::Python::Tuple as u16),
18687                    ("{}", crate::Python::Dictionary as u16),
18688                    ("...", crate::Python::Ellipsis as u16),
18689                ],
18690                8,
18691            ),
18692            LANG::Lua => (
18693                [
18694                    ("function f(a)\n  return a and {}\nend\n", 2, 3),
18695                    ("function f()\n  if {} then return 1 end\nend\n", 1, 3),
18696                ],
18697                "b",
18698                &[
18699                    ("\"s\"", crate::Lua::String as u16),
18700                    ("{}", crate::Lua::TableConstructor as u16),
18701                ],
18702                2,
18703            ),
18704            LANG::Php => (
18705                [
18706                    ("<?php\nfunction f($a) {\n  return $a && {};\n}\n", 2, 3),
18707                    ("<?php\nfunction f() {\n  if ({}) { return 1; }\n}\n", 1, 3),
18708                ],
18709                "$b",
18710                &[
18711                    ("'s'", crate::Php::String as u16),
18712                    ("\"x$b\"", crate::Php::EncapsedString as u16),
18713                    ("<<<EOT\ns\nEOT", crate::Php::Heredoc as u16),
18714                    ("<<<'EOT'\ns\nEOT", crate::Php::Nowdoc as u16),
18715                    ("[]", crate::Php::ArrayCreationExpression as u16),
18716                    ("null", crate::Php::Null as u16),
18717                    ("`ls`", crate::Php::ShellCommandExpression as u16),
18718                    ("(bool)$b", crate::Php::CastExpression as u16),
18719                ],
18720                8,
18721            ),
18722            LANG::Groovy => (
18723                [
18724                    ("def f(a) {\n  return a && {}\n}\n", 2, 3),
18725                    ("def f() {\n  if ({}) { return 1 }\n}\n", 1, 3),
18726                ],
18727                "b",
18728                &[
18729                    ("\"s\"", crate::Groovy::StringLiteral as u16),
18730                    ("null", crate::Groovy::NullLiteral as u16),
18731                    ("[]", crate::Groovy::ListLiteral as u16),
18732                    ("[:]", crate::Groovy::MapLiteral as u16),
18733                ],
18734                4,
18735            ),
18736            LANG::Perl => (
18737                [
18738                    (
18739                        "sub f {\n  my ($a, $b) = @_;\n  if ($a && {}) { print 1; }\n}\n",
18740                        2,
18741                        4,
18742                    ),
18743                    (
18744                        "sub f {\n  my ($a, $b) = @_;\n  if ({}) { print 1; }\n}\n",
18745                        1,
18746                        3,
18747                    ),
18748                ],
18749                "$b",
18750                &[
18751                    ("'s'", crate::Perl::StringSingleQuoted as u16),
18752                    ("q(s)", crate::Perl::StringQQuoted as u16),
18753                    ("\"s\"", crate::Perl::StringDoubleQuoted as u16),
18754                    ("qq(s)", crate::Perl::StringQqQuoted as u16),
18755                    ("qx(ls)", crate::Perl::CommandQxQuoted as u16),
18756                    ("`ls`", crate::Perl::BacktickQuoted as u16),
18757                    ("qw(a b)", crate::Perl::WordListQw as u16),
18758                    ("[1, 2]", crate::Perl::ArrayRef as u16),
18759                    ("{ a => 1 }", crate::Perl::HashRef as u16),
18760                    ("qr/re/", crate::Perl::RegexPatternQr as u16),
18761                    ("__FILE__", crate::Perl::SpecialLiteral as u16),
18762                ],
18763                11,
18764            ),
18765            LANG::Ruby => (
18766                [
18767                    ("def f(a, b)\n  if a && {} then 1 else 0 end\nend\n", 3, 4),
18768                    ("def f(a, b)\n  if {} then 1 else 0 end\nend\n", 2, 3),
18769                ],
18770                "b",
18771                &[
18772                    ("\"s\"", crate::Ruby::String as u16),
18773                    ("\"a\" \"b\"", crate::Ruby::ChainedString as u16),
18774                    ("`ls`", crate::Ruby::Subshell as u16),
18775                    ("[1, 2]", crate::Ruby::Array as u16),
18776                    ("{ a: 1 }", crate::Ruby::Hash as u16),
18777                    ("%w[a b]", crate::Ruby::StringArray as u16),
18778                    ("%i[a b]", crate::Ruby::SymbolArray as u16),
18779                    ("/re/", crate::Ruby::Regex as u16),
18780                    ("?a", crate::Ruby::Character as u16),
18781                    (":sym", crate::Ruby::SimpleSymbol as u16),
18782                    (":\"sym\"", crate::Ruby::DelimitedSymbol as u16),
18783                ],
18784                11,
18785            ),
18786            LANG::Elixir => (
18787                [
18788                    (
18789                        "defmodule M do\n  def f(a, b) do\n    if a && {} do\n      a\n    end\n  end\nend\n",
18790                        2,
18791                        5,
18792                    ),
18793                    (
18794                        "defmodule M do\n  def f(a, b) do\n    if {} do\n      a\n    end\n  end\nend\n",
18795                        1,
18796                        4,
18797                    ),
18798                ],
18799                "b",
18800                &[
18801                    ("\"s\"", crate::Elixir::String as u16),
18802                    ("'c'", crate::Elixir::Charlist as u16),
18803                    ("~r/re/", crate::Elixir::Sigil as u16),
18804                    (":\"q a\"", crate::Elixir::QuotedAtom as u16),
18805                    ("[1, 2]", crate::Elixir::List as u16),
18806                    ("{1, 2}", crate::Elixir::Tuple as u16),
18807                    ("%{a: 1}", crate::Elixir::Map as u16),
18808                    ("<<1>>", crate::Elixir::Bitstring as u16),
18809                ],
18810                8,
18811            ),
18812            LANG::C => (
18813                C_FAMILY_SLOTS,
18814                "b",
18815                c_family_literals!(C, "\"s\"", "NULL"),
18816                5,
18817            ),
18818            LANG::Cpp => (
18819                C_FAMILY_SLOTS,
18820                "b",
18821                c_family_literals!(
18822                    Cpp,
18823                    "\"s\"",
18824                    "nullptr",
18825                    ("R\"(x)\"", crate::Cpp::RawStringLiteral as u16),
18826                    ("\"s\"_x", crate::Cpp::UserDefinedLiteral as u16),
18827                ),
18828                7,
18829            ),
18830            LANG::Mozcpp => (
18831                C_FAMILY_SLOTS,
18832                "b",
18833                c_family_literals!(
18834                    Mozcpp,
18835                    "\"s\"",
18836                    "nullptr",
18837                    ("R\"(x)\"", crate::Mozcpp::RawStringLiteral as u16),
18838                    ("\"s\"_x", crate::Mozcpp::UserDefinedLiteral as u16),
18839                ),
18840                7,
18841            ),
18842            LANG::Objc => (
18843                C_FAMILY_SLOTS,
18844                "b",
18845                c_family_literals!(
18846                    Objc,
18847                    "@\"s\"",
18848                    "NULL",
18849                    ("@[]", crate::Objc::ArrayLiteral as u16),
18850                    ("@{}", crate::Objc::DictionaryLiteral as u16),
18851                    ("@1", crate::Objc::AtExpression as u16),
18852                    ("@selector(x)", crate::Objc::SelectorExpression as u16),
18853                    ("@encode(int)", crate::Objc::EncodeExpression as u16),
18854                ),
18855                10,
18856            ),
18857            _ => return None,
18858        })
18859    }
18860
18861    /// Runs `check` once per enabled language that has a case, having
18862    /// first established that the case can still assert something. The
18863    /// four guards are the sibling module's, for the same four ways
18864    /// this table could decay into asserting nothing: no language
18865    /// enabled, an emptied literal list, a list that no longer covers
18866    /// every kind, and a template that lost its `{}` slot (which makes
18867    /// every comparison `x == x`).
18868    fn for_each_case(check: impl Fn(LANG, Case)) {
18869        let mut checked = 0;
18870        for lang in LANG::into_enum_iter() {
18871            if !lang.is_enabled() {
18872                continue;
18873            }
18874            let Some(case @ (slots, _, literals, expected_kinds)) = cases(lang) else {
18875                continue;
18876            };
18877            // The length check does not imply this one: it asserts only
18878            // that the list and its recorded count *agree*, which an
18879            // emptied row with `expected_kinds` set to 0 satisfies —
18880            // the shape a "the test failed, fix the number" edit takes.
18881            // `checked` counts languages, so that row still increments
18882            // it and the non-vacuity guard below reads satisfied.
18883            assert!(
18884                !literals.is_empty(),
18885                "{lang:?}: the literal-operand list is empty; this language asserted nothing"
18886            );
18887            assert_eq!(
18888                literals.len(),
18889                expected_kinds,
18890                "{lang:?}: the literal-operand list no longer covers one spelling \
18891                 per grammar literal kind"
18892            );
18893            for (template, _, _) in slots {
18894                assert!(
18895                    template.contains("{}"),
18896                    "{lang:?}: template lost its `{{}}` operand slot: {template}"
18897                );
18898            }
18899            check(lang, case);
18900            checked += 1;
18901        }
18902        assert!(
18903            checked > 0,
18904            "no truthy-valued language enabled; this test asserted nothing"
18905        );
18906    }
18907
18908    #[test]
18909    fn a_literal_operand_scores_like_an_identifier_operand() {
18910        for_each_case(|lang, (slots, identifier, literals, _)| {
18911            for (template, _, _) in slots {
18912                let baseline = conditions(lang, &template.replace("{}", identifier));
18913                for (literal, _) in literals {
18914                    let source = template.replace("{}", literal);
18915                    let scored = conditions(lang, &source);
18916                    assert_eq!(
18917                        scored, baseline,
18918                        "{lang:?}: `{literal}` scored {scored} unary conditions against \
18919                         `{identifier}`'s {baseline}\n  source: {source}"
18920                    );
18921                }
18922            }
18923        });
18924    }
18925
18926    /// The absolute anchor under the comparison above: every operand
18927    /// spelling must produce the slot's recorded `conditions`, and must
18928    /// leave `cyclomatic` alone.
18929    #[test]
18930    fn every_literal_operand_scores_its_recorded_values() {
18931        for_each_case(|lang, (slots, identifier, literals, _)| {
18932            for (template, expected_conditions, expected_cyclomatic) in slots {
18933                let operands = std::iter::once(identifier)
18934                    .chain(literals.iter().map(|(spelling, _)| *spelling));
18935                for operand in operands {
18936                    let source = template.replace("{}", operand);
18937                    assert_eq!(
18938                        conditions(lang, &source),
18939                        expected_conditions,
18940                        "{lang:?}: `{operand}` conditions\n  source: {source}"
18941                    );
18942                    assert_eq!(
18943                        cyclomatic_sum(lang, &source),
18944                        expected_cyclomatic,
18945                        "{lang:?}: `{operand}` cyclomatic_sum\n  source: {source}"
18946                    );
18947                }
18948            }
18949        });
18950    }
18951
18952    /// The twelfth Perl and Ruby literal kind, which the table above
18953    /// cannot hold: in both languages a heredoc body follows the
18954    /// *statement*, so the operand slot carries only the introducer and
18955    /// the rest of the literal lands on later lines. Substituting one
18956    /// into a single-line template produces an unterminated heredoc and
18957    /// an `ERROR` parse, not a measurement.
18958    ///
18959    /// That shape is also why exactly one kind is listed per language
18960    /// (§5): `heredoc_initializer` / `heredoc_beginning` is the node in
18961    /// the operand slot, while `heredoc_body_statement` /
18962    /// `heredoc_body` is a sibling the walker never reaches. Listing
18963    /// both would score one literal twice.
18964    #[cfg(any(feature = "perl", feature = "ruby"))]
18965    #[test]
18966    fn a_heredoc_operand_scores_like_an_identifier_operand() {
18967        #[cfg(feature = "perl")]
18968        {
18969            let heredoc = "sub f {\n  my ($a, $b) = @_;\n  if ($a && <<\"EOT\") { print 1; }\nhello\nEOT\n}\n";
18970            assert_spells(
18971                LANG::Perl,
18972                heredoc,
18973                &[(crate::Perl::HeredocInitializer as u16, 1, "<<\"EOT\"")],
18974            );
18975            assert_eq!(
18976                conditions(LANG::Perl, heredoc),
18977                conditions(
18978                    LANG::Perl,
18979                    "sub f {\n  my ($a, $b) = @_;\n  if ($a && $b) { print 1; }\n}\n"
18980                ),
18981                "Perl heredoc operand"
18982            );
18983        }
18984        #[cfg(feature = "ruby")]
18985        {
18986            let heredoc =
18987                "def f(a, b)\n  if a && <<~TXT then 1 else 0 end\n    hello\n  TXT\nend\n";
18988            assert_spells(
18989                LANG::Ruby,
18990                heredoc,
18991                &[(crate::Ruby::HeredocBeginning as u16, 1, "<<~TXT")],
18992            );
18993            assert_eq!(
18994                conditions(LANG::Ruby, heredoc),
18995                conditions(
18996                    LANG::Ruby,
18997                    "def f(a, b)\n  if a && b then 1 else 0 end\nend\n"
18998                ),
18999                "Ruby heredoc operand"
19000            );
19001        }
19002    }
19003
19004    /// Every spelling must parse to the `kind_id` its row names — the
19005    /// §1 alias claim, which is the one thing a conditions comparison
19006    /// cannot check: a row naming the wrong alias of a multi-id kind
19007    /// would simply keep reporting the unfixed number.
19008    #[test]
19009    fn every_literal_spelling_parses_to_the_kind_its_row_names() {
19010        for_each_case(|lang, (slots, _, literals, _)| {
19011            for (template, _, _) in slots {
19012                for (literal, kind) in literals {
19013                    let source = template.replace("{}", literal);
19014                    assert_spells(lang, &source, &[(*kind, 1, literal)]);
19015                }
19016            }
19017        });
19018    }
19019}
19020
19021/// The condition slot and the wrapper peel agree on which kinds are
19022/// wrappers, in each language whose slot used to restate the list
19023/// (#1470).
19024///
19025/// `<lang>_count_condition` decided which slot expressions to hand the
19026/// peel, and the former `<lang>_inspect_container` which it could descend.
19027/// Spelled as two lists they drifted three times — Kotlin (#1459),
19028/// Groovy (#1466) and C# (#1463) — each time scoring a valid predicate
19029/// zero. The six languages here now ask `<lang>_wrapper_operand`
19030/// instead, which removed the duplication without moving a number: the
19031/// wrapper matrix measured identical before and after, so a revert
19032/// cannot fail these tests. What they pin is the *answer* both halves
19033/// must keep giving:
19034///
19035/// - **Routed.** Every wrapper the peel accepts scores its slot once.
19036///   A slot that stops asking the peel scores these zero.
19037/// - **Declined.** A unary the peel does not accept (`-b`, or a valid
19038///   non-wrapper such as `b & b`) stops the peel: a slot pays for it
19039///   once (#1526; Go's terminal-kind slot still scores `-b` nothing),
19040///   and outside a slot it scores nothing, so a peel that starts
19041///   accepting arithmetic operators by accident is caught here rather
19042///   than in a corpus snapshot.
19043/// - **Proves boolean.** A negation counts its operand outside any
19044///   boolean slot and a parenthesis does not — the second half of the
19045///   peel's answer, which the slot never sees.
19046///
19047/// Each template's `@` is the operand under test.
19048#[cfg(test)]
19049#[cfg(any(
19050    feature = "rust",
19051    feature = "go",
19052    feature = "java",
19053    feature = "python",
19054    feature = "ruby",
19055    feature = "lua"
19056))]
19057mod wrapper_peel_routing {
19058    use crate::test_support::metrics_verbatim;
19059    use crate::{LANG, MetricsOptions};
19060
19061    fn conditions(lang: LANG, source: &str) -> u64 {
19062        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
19063            .abc
19064            .conditions_sum()
19065    }
19066
19067    /// Asserts `conditions` for every `(operand, expected)` row of one
19068    /// template.
19069    fn assert_operands(lang: LANG, template: &str, rows: &[(&str, u64)]) {
19070        assert!(template.contains('@'), "template has no operand slot");
19071        for &(operand, expected) in rows {
19072            let source = template.replace('@', operand);
19073            assert_eq!(
19074                conditions(lang, &source),
19075                expected,
19076                "`{operand}` in {lang:?}\n  source: {source}"
19077            );
19078        }
19079    }
19080
19081    /// Rust's slot pays for any predicate no other arm counted (#1526),
19082    /// so the declined half reads the valid non-wrapper `b & b`: the
19083    /// peel stops on it, the slot pays for it, and the call argument
19084    /// below does not.
19085    #[test]
19086    #[cfg(feature = "rust")]
19087    fn rust_condition_slot_routes_what_the_peel_accepts() {
19088        assert_operands(
19089            LANG::Rust,
19090            "fn f(b: bool) { if @ {} }\n",
19091            &[
19092                ("b", 1),
19093                ("(b)", 1),
19094                ("!b", 1),
19095                ("!(b)", 1),
19096                ("b & b", 1),
19097                ("(b & b)", 1),
19098                ("!(b & b)", 1),
19099            ],
19100        );
19101        assert_operands(
19102            LANG::Rust,
19103            "fn f(b: bool) { g(@); }\n",
19104            &[("!b", 1), ("(b)", 0), ("b & b", 0), ("!(b & b)", 1)],
19105        );
19106    }
19107
19108    #[test]
19109    #[cfg(feature = "go")]
19110    fn go_condition_slot_routes_what_the_peel_accepts() {
19111        assert_operands(
19112            LANG::Go,
19113            "package p\nfunc f(b bool) { if @ {} }\n",
19114            &[
19115                ("b", 1),
19116                ("(b)", 1),
19117                ("!b", 1),
19118                ("!(b)", 1),
19119                ("-b", 0),
19120                ("(-b)", 0),
19121            ],
19122        );
19123        assert_operands(
19124            LANG::Go,
19125            "package p\nfunc f(b bool) { g(@) }\n",
19126            &[("!b", 1), ("(b)", 0), ("-b", 0)],
19127        );
19128    }
19129
19130    /// The ternary condition is a slot like the `if` one; the `?` itself
19131    /// is the base 1. Java's slot pays for any predicate no other arm
19132    /// counted (#1526), so the declined half reads the valid non-wrapper
19133    /// `b & b`: the peel stops on it, the slot pays for it, and the call
19134    /// argument below does not.
19135    #[test]
19136    #[cfg(feature = "java")]
19137    fn java_condition_slot_routes_what_the_peel_accepts() {
19138        assert_operands(
19139            LANG::Java,
19140            "class A { int f(boolean b) { return @ ? 1 : 2; } }\n",
19141            &[
19142                ("b", 2),
19143                ("(b)", 2),
19144                ("!b", 2),
19145                ("!(b)", 2),
19146                ("b & b", 2),
19147                ("(b & b)", 2),
19148            ],
19149        );
19150        assert_operands(
19151            LANG::Java,
19152            "class A { void f(boolean b) { g(@); } }\n",
19153            &[("!b", 1), ("(b)", 0), ("b & b", 0), ("!(b & b)", 1)],
19154        );
19155    }
19156
19157    /// Python peels parentheses and the walrus only: `not` is counted by
19158    /// its own `NotOperator` arm, so `python_wrapper_operand` carries no
19159    /// proves-boolean flag and there is no outside-a-slot half to pin.
19160    /// Python is truthy-valued, so the slot pays for any predicate no
19161    /// other arm counted, `-b` included (#1526), as Ruby's does.
19162    #[test]
19163    #[cfg(feature = "python")]
19164    fn python_condition_slot_routes_what_the_peel_accepts() {
19165        assert_operands(
19166            LANG::Python,
19167            "def f(b):\n    if @:\n        pass\n",
19168            &[("b", 1), ("(b)", 1), ("((b))", 1), ("-b", 1), ("(-b)", 1)],
19169        );
19170    }
19171
19172    /// Ruby has no counted call-argument slot, so the outside-a-slot
19173    /// half reads a ternary *branch*, which is type-free: only a
19174    /// negation there proves its operand boolean. That branch is also
19175    /// where the declined half lives for Ruby: its condition slot pays
19176    /// for any predicate no other arm counted, `-b` included (#1520), so
19177    /// `if -b` scores 1 rather than 0.
19178    #[test]
19179    #[cfg(feature = "ruby")]
19180    fn ruby_condition_slot_routes_what_the_peel_accepts() {
19181        assert_operands(
19182            LANG::Ruby,
19183            "def f(b)\n  if @ then 1 end\nend\n",
19184            &[
19185                ("b", 1),
19186                ("(b)", 1),
19187                ("!b", 1),
19188                ("not b", 1),
19189                ("!(b)", 1),
19190                ("-b", 1),
19191                ("(-b)", 1),
19192            ],
19193        );
19194        assert_operands(
19195            LANG::Ruby,
19196            "def f(b, c)\n  x = c ? 1 : @\nend\n",
19197            &[
19198                ("!b", 3),
19199                ("not b", 3),
19200                ("(b)", 2),
19201                ("-b", 2),
19202                // A negated branch is a slot whatever it negates (#1529).
19203                ("!self", 3),
19204                ("not -b", 3),
19205                ("!(b > 1)", 3),
19206            ],
19207        );
19208        // A statement sequence evaluates to its last statement, so that is
19209        // the operand the slot reads: `(b; b > 1)` is the comparison,
19210        // already counted by its own `>`, and `(b > 1; b)` is the bare `b`,
19211        // which the slot counts beside that `>`. Reading the first
19212        // statement inverted both. The subject-less `when` goes through
19213        // the same peel.
19214        let seqs: &[(&str, u64)] = &[("(b; b > 1)", 1), ("(b > 1; b)", 2)];
19215        assert_operands(LANG::Ruby, "def f(b)\n  if @ then 1 end\nend\n", seqs);
19216        assert_operands(
19217            LANG::Ruby,
19218            "def f(b)\n  case\n  when @ then 1\n  end\nend\n",
19219            seqs,
19220        );
19221    }
19222
19223    /// A subject-less `when` whose pattern is a chain adds nothing of its
19224    /// own: the Rule 9 walker scores every operand as a slot, so each
19225    /// chain already pays at least one (#1529). Before that the walker
19226    /// skipped an operand outside its terminal list, the clause had to
19227    /// pay for `-a && -b` (review of #1453), and a chain scored as many
19228    /// conditions as it had *listed* operands plus one at most. Each row
19229    /// now scores what its plain-identifier twin (`a && b`, `a || b ||
19230    /// c`) does. An assignment operand is its value (#1520), so
19231    /// `(a = 1) && (b = 2)` is `1 && 2`.
19232    #[test]
19233    #[cfg(feature = "ruby")]
19234    fn ruby_subjectless_when_chain_pays_per_operand() {
19235        assert_operands(
19236            LANG::Ruby,
19237            "def f(a, b, c)\n  case\n  when @ then 1\n  end\nend\n",
19238            &[
19239                ("a", 1),
19240                ("-a", 1),
19241                ("a && b", 2),
19242                ("-a && -b", 2),
19243                ("(a = 1) && (b = 2)", 2),
19244                ("a || b || c", 3),
19245                ("-a || -b || -c", 3),
19246                ("-a && b", 2),
19247                ("a > 1 && b", 2),
19248                ("a > 1 && -b", 2),
19249                ("a && (b || c)", 3),
19250                ("-a && (-b || c)", 3),
19251            ],
19252        );
19253    }
19254
19255    /// #1529: an operand of a Ruby `&&` / `||` / `and` / `or` chain is
19256    /// scored by the rule #1520 gave every condition slot — one, unless
19257    /// another arm already counts it — wherever the chain is written.
19258    /// The walker used to pay only for an operand that peeled down to a
19259    /// fixed list of terminal kinds, so every spelling in `UNLISTED`
19260    /// scored one below its plain-identifier twin `b` in every position
19261    /// below. `CONTROLS` must not move: `!b` and `(b)` already scored
19262    /// their twin, and a comparison, a pattern test, or a negation of one
19263    /// is counted by its own arm, so paying the operand as well would
19264    /// count that decision twice.
19265    #[test]
19266    #[cfg(feature = "ruby")]
19267    fn ruby_chain_operand_pays_what_no_other_arm_counted() {
19268        const UNLISTED: &[&str] = &[
19269            "self",
19270            "Foo::Bar",
19271            "-b",
19272            "(-b)",
19273            "defined?(b)",
19274            "b + 1",
19275            "!self",
19276            "not self",
19277            "(y ||= b)",
19278            "(y = self)",
19279            "begin self end",
19280        ];
19281        const CONTROLS: &[&str] = &["!b", "(b)", "c > 1", "!(c > 1)", "(c in Integer)"];
19282        // (template, conditions with `b` in the `@` operand)
19283        let positions: &[(&str, u64)] = &[
19284            ("def f(a, b, c)\n  g if a && @\nend\n", 2),
19285            ("def f(a, b, c)\n  g if (a && @)\nend\n", 2),
19286            ("def f(a, b, c)\n  x = a && @\nend\n", 2),
19287            ("def f(a, b, c)\n  return a || @\nend\n", 2),
19288            ("def f(a, b, c)\n  h(@ && a)\nend\n", 2),
19289            ("def f(a, b, c)\n  x = (a and @)\nend\n", 2),
19290            ("def f(a, b, c)\n  x = (a or @)\nend\n", 2),
19291            ("def f(a, b, c)\n  y ||= a && @\nend\n", 2),
19292            ("def f(a, b, c)\n  y &&= a || @\nend\n", 2),
19293            ("def f(a, b, c)\n  x = a && (c || @)\nend\n", 3),
19294            // A comment is a named child of the `binary` too, and must not
19295            // pay as a third operand.
19296            ("def f(a, b, c)\n  x = a && # c\n    @\nend\n", 2),
19297            ("def f(a, b, c)\n  while a && @ do g end\nend\n", 2),
19298            (
19299                "def f(a, b, c)\n  if a then 1 elsif a && @ then 2 end\nend\n",
19300                4,
19301            ),
19302            (
19303                "def f(a, b, c)\n  case\n  when a && @ then 1\n  end\nend\n",
19304                2,
19305            ),
19306        ];
19307        for &(template, twin) in positions {
19308            // The twin is measured, not only assumed, so a template that
19309            // stopped parsing as a chain fails here rather than agreeing
19310            // with every row at some other value.
19311            assert_operands(LANG::Ruby, template, &[("b", twin)]);
19312            let rows: Vec<_> = UNLISTED
19313                .iter()
19314                .chain(CONTROLS)
19315                .map(|&operand| (operand, twin))
19316                .collect();
19317            assert_operands(LANG::Ruby, template, &rows);
19318        }
19319    }
19320
19321    #[test]
19322    #[cfg(feature = "lua")]
19323    fn lua_condition_slot_routes_what_the_peel_accepts() {
19324        // Lua is truthy-valued, so the slot pays for any predicate no
19325        // other arm counted, `-b` and `#b` included (#1526).
19326        assert_operands(
19327            LANG::Lua,
19328            "function f(b)\n  if @ then end\nend\n",
19329            &[
19330                ("b", 1),
19331                ("(b)", 1),
19332                ("not b", 1),
19333                ("not (b)", 1),
19334                ("-b", 1),
19335                ("#b", 1),
19336                ("(-b)", 1),
19337            ],
19338        );
19339        assert_operands(
19340            LANG::Lua,
19341            "function f(b)\n  g(@)\nend\n",
19342            &[("not b", 1), ("(b)", 0), ("-b", 0)],
19343        );
19344    }
19345}
19346
19347/// A comment inside a condition or value slot changes nothing (#1455).
19348///
19349/// Every walker read its slots by fixed child index, and every peel
19350/// stepped into a `(…)` / negation with `child(1)` or the first named
19351/// child. tree-sitter counts a comment among a node's children and
19352/// marks it *named*, so `if (/*c*/ b)` handed the comment to the slot,
19353/// which declined it, and the condition scored zero. Each pair below is
19354/// one spelling with and without a comment; the two are asserted equal
19355/// to each other *and* to the expected literal, so a regression that
19356/// moved both sides alike — to zero, say — still fails.
19357///
19358/// The forms, per slot and per comment spelling of the language: a
19359/// comment before the operand, inside a nested parenthesis, before a
19360/// negation, between a negation and its operand, and — where the
19361/// language's syntax puts parentheses around the slot — before those
19362/// parentheses. Value slots (`return`, declarator, assignment, lambda
19363/// body) take the negated forms only, since a bare operand there is no
19364/// condition, and only the first comment spelling: a line comment after
19365/// `return` ends the statement in Go.
19366#[cfg(test)]
19367#[cfg(any(
19368    feature = "c",
19369    feature = "cpp",
19370    feature = "csharp",
19371    feature = "elixir",
19372    feature = "go",
19373    feature = "groovy",
19374    feature = "java",
19375    feature = "javascript",
19376    feature = "kotlin",
19377    feature = "lua",
19378    feature = "mozcpp",
19379    feature = "mozjs",
19380    feature = "objc",
19381    feature = "perl",
19382    feature = "php",
19383    feature = "python",
19384    feature = "ruby",
19385    feature = "rust",
19386    feature = "typescript"
19387))]
19388mod slot_comment_invariance {
19389    use crate::test_support::metrics_verbatim;
19390    use crate::{LANG, MetricsOptions};
19391
19392    fn conditions(lang: LANG, source: &str) -> u64 {
19393        metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
19394            .abc
19395            .conditions_sum()
19396    }
19397
19398    /// How one language spells the slots under test.
19399    struct Spelling {
19400        /// Comment spellings, each carrying the whitespace — a newline
19401        /// for a line comment — the operand after it needs.
19402        comments: &'static [&'static str],
19403        operand: &'static str,
19404        /// `None` where the peel does not descend the negation: Python
19405        /// counts `not` through its own arm, and tree-sitter-python
19406        /// fails to parse the valid `(not # c⏎ b)` at the pinned grammar.
19407        negation: Option<&'static str>,
19408        /// The slot is written inside parentheses: either the syntax
19409        /// requires them (`if (…)`), or a line comment needs them to
19410        /// stay inside the expression (Python, Ruby, Elixir).
19411        parenthesised: bool,
19412        /// The parentheses belong to the slot's syntax, so a comment
19413        /// may also sit before them (`if /*c*/ (b)`).
19414        syntactic_parens: bool,
19415        /// `(template, expected)`; `@` marks a condition slot.
19416        conditions: &'static [(&'static str, u64)],
19417        /// Templates whose `@` is a value slot; a negation there scores 1.
19418        values: &'static [&'static str],
19419    }
19420
19421    const C_COMMENTS: &[&str] = &["/*c*/ ", "//c\n "];
19422
19423    const C_FAMILY: Spelling = Spelling {
19424        comments: C_COMMENTS,
19425        operand: "b",
19426        negation: Some("!"),
19427        parenthesised: true,
19428        syntactic_parens: true,
19429        conditions: &[
19430            ("int f(){ if @ { x(); } }", 1),
19431            ("int f(){ while @ { x(); } }", 1),
19432            ("int f(){ do { x(); } while @; }", 1),
19433        ],
19434        values: &["int f(){ return @; }"],
19435    };
19436
19437    const JS_FAMILY: Spelling = Spelling {
19438        comments: C_COMMENTS,
19439        operand: "b",
19440        negation: Some("!"),
19441        parenthesised: true,
19442        syntactic_parens: true,
19443        conditions: &[
19444            ("function f(){ if @ { x(); } }", 1),
19445            ("function f(){ while @ { x(); } }", 1),
19446            ("function f(){ do { x(); } while @; }", 1),
19447        ],
19448        values: &["function f(){ return @; }"],
19449    };
19450
19451    fn spelling(lang: LANG) -> Option<Spelling> {
19452        Some(match lang {
19453            LANG::C | LANG::Cpp | LANG::Mozcpp | LANG::Objc => C_FAMILY,
19454            LANG::Javascript | LANG::Mozjs | LANG::Typescript | LANG::Tsx => JS_FAMILY,
19455            LANG::Java => Spelling {
19456                conditions: &[
19457                    ("class K{ void f(){ if @ { x(); } } }", 1),
19458                    ("class K{ void f(){ while @ { x(); } } }", 1),
19459                    ("class K{ void f(){ do { x(); } while @; } }", 1),
19460                ],
19461                values: &[
19462                    "class K{ boolean f(){ return @; } }",
19463                    "class K{ void f(){ boolean y = @; } }",
19464                    "class K{ void f(){ y = @; } }",
19465                    "class K{ void f(){ Supplier<Boolean> s = () -> @; } }",
19466                ],
19467                ..C_FAMILY
19468            },
19469            LANG::Csharp => Spelling {
19470                conditions: &[
19471                    ("class K{ void f(){ if @ { x(); } } }", 1),
19472                    ("class K{ void f(){ while @ { x(); } } }", 1),
19473                    ("class K{ void f(){ do { x(); } while @; } }", 1),
19474                ],
19475                values: &[
19476                    "class K{ bool f(){ return @; } }",
19477                    "class K{ void f(){ var y = @; } }",
19478                    "class K{ void f(){ y = @; } }",
19479                    "class K{ void f(){ Func<bool> s = () => @; } }",
19480                ],
19481                ..C_FAMILY
19482            },
19483            LANG::Groovy => Spelling {
19484                conditions: &[
19485                    ("def f(){ if @ { x() } }", 1),
19486                    ("def f(){ while @ { x() } }", 1),
19487                    ("def f(){ do { x() } while @ }", 1),
19488                ],
19489                values: &[
19490                    "def f(){ return @ }",
19491                    "def f(){ def y = @ }",
19492                    "def f(){ y = @ }",
19493                ],
19494                ..C_FAMILY
19495            },
19496            LANG::Php => Spelling {
19497                comments: &["/*c*/ ", "//c\n ", "#c\n "],
19498                operand: "$b",
19499                conditions: &[
19500                    ("<?php function f(){ if @ { x(); } }", 1),
19501                    ("<?php function f(){ while @ { x(); } }", 1),
19502                    ("<?php function f(){ do { x(); } while @; }", 1),
19503                ],
19504                values: &["<?php function f(){ return @; }"],
19505                ..C_FAMILY
19506            },
19507            LANG::Kotlin => Spelling {
19508                conditions: &[
19509                    ("fun f(){ if @ { x() } }", 1),
19510                    ("fun f(){ while @ { x() } }", 1),
19511                    ("fun f(){ do { x() } while @ }", 1),
19512                ],
19513                values: &[],
19514                ..C_FAMILY
19515            },
19516            LANG::Perl => Spelling {
19517                comments: &["# c\n "],
19518                operand: "$b",
19519                conditions: &[
19520                    ("sub f { if @ { x(); } }", 1),
19521                    ("sub f { unless @ { x(); } }", 1),
19522                    ("sub f { while @ { x(); } }", 1),
19523                    ("sub f { until @ { x(); } }", 1),
19524                ],
19525                values: &["sub f { return @; }"],
19526                ..C_FAMILY
19527            },
19528            LANG::Rust => Spelling {
19529                parenthesised: false,
19530                syntactic_parens: false,
19531                conditions: &[
19532                    ("fn f(){ if @ { x(); } }", 1),
19533                    ("fn f(){ while @ { x(); } }", 1),
19534                ],
19535                values: &["fn f() -> bool { return @; }"],
19536                ..C_FAMILY
19537            },
19538            LANG::Go => Spelling {
19539                parenthesised: false,
19540                syntactic_parens: false,
19541                conditions: &[
19542                    ("package p\nfunc f(){ if @ { x() } }", 1),
19543                    ("package p\nfunc f(){ for @ { x() } }", 1),
19544                ],
19545                values: &["package p\nfunc f() bool { return @ }"],
19546                ..C_FAMILY
19547            },
19548            LANG::Lua => Spelling {
19549                comments: &["--[[c]] ", "--c\n "],
19550                negation: Some("not "),
19551                parenthesised: false,
19552                syntactic_parens: false,
19553                conditions: &[
19554                    ("function f() if @ then x() end end", 1),
19555                    ("function f() while @ do x() end end", 1),
19556                    ("function f() repeat x() until @ end", 1),
19557                ],
19558                values: &["function f() return @ end"],
19559                ..C_FAMILY
19560            },
19561            LANG::Python => Spelling {
19562                comments: &["# c\n "],
19563                negation: None,
19564                syntactic_parens: false,
19565                conditions: &[
19566                    ("def f():\n    if @:\n        x()\n", 1),
19567                    ("def f():\n    while @:\n        x()\n", 1),
19568                ],
19569                values: &[],
19570                ..C_FAMILY
19571            },
19572            LANG::Ruby => Spelling {
19573                comments: &["# c\n "],
19574                syntactic_parens: false,
19575                conditions: &[
19576                    ("def f\n  if @\n    x\n  end\nend\n", 1),
19577                    ("def f\n  while @\n    x\n  end\nend\n", 1),
19578                    ("def f\n  case\n  when @\n    x\n  end\nend\n", 1),
19579                ],
19580                values: &[],
19581                ..C_FAMILY
19582            },
19583            // Elixir's `if` slot scored its operand whatever the peel
19584            // returned, so the defect showed only where the peel decides:
19585            // an operand of a `&&` chain, which counts each operand.
19586            LANG::Elixir => Spelling {
19587                comments: &["# c\n "],
19588                syntactic_parens: false,
19589                conditions: &[
19590                    (
19591                        "defmodule M do\n def f do\n  if @ do\n   x()\n  end\n end\nend\n",
19592                        1,
19593                    ),
19594                    ("defmodule M do\n def f do\n  y = a && @\n end\nend\n", 2),
19595                ],
19596                values: &[],
19597                ..C_FAMILY
19598            },
19599            _ => return None,
19600        })
19601    }
19602
19603    /// `(without, with, expected)` for every form of every slot.
19604    fn cases(s: &Spelling) -> Vec<(String, String, u64)> {
19605        let slot = |x: String| {
19606            if s.parenthesised { format!("({x})") } else { x }
19607        };
19608        let b = s.operand;
19609        let mut out = Vec::new();
19610        for &(template, expected) in s.conditions {
19611            let fill = |x: String| template.replace('@', &x);
19612            for c in s.comments {
19613                let mut push = |plain: String, commented: String| {
19614                    out.push((fill(plain), fill(commented), expected));
19615                };
19616                push(slot(b.into()), slot(format!("{c}{b}")));
19617                push(slot(format!("({b})")), slot(format!("({c}{b})")));
19618                if let Some(n) = s.negation {
19619                    push(slot(format!("{n}{b}")), slot(format!("{c}{n}{b}")));
19620                    push(slot(format!("{n}{b}")), slot(format!("{n}{c}{b}")));
19621                }
19622                if s.syntactic_parens {
19623                    push(slot(b.into()), format!("{c}{}", slot(b.into())));
19624                }
19625            }
19626        }
19627        if let (Some(n), Some(c)) = (s.negation, s.comments.first()) {
19628            for template in s.values {
19629                let fill = |x: String| template.replace('@', &x);
19630                out.push((fill(format!("{n}{b}")), fill(format!("{c}{n}{b}")), 1));
19631                out.push((fill(format!("{n}{b}")), fill(format!("{n}{c}{b}")), 1));
19632            }
19633        }
19634        out
19635    }
19636
19637    #[test]
19638    fn a_comment_in_a_slot_changes_nothing() {
19639        let mut checked = 0;
19640        for lang in LANG::into_enum_iter().filter(LANG::is_enabled) {
19641            let Some(spelling) = spelling(lang) else {
19642                continue;
19643            };
19644            for (plain, commented, expected) in cases(&spelling) {
19645                let without = conditions(lang, &plain);
19646                assert_eq!(
19647                    conditions(lang, &commented),
19648                    without,
19649                    "{lang:?}: a comment changed the ABC conditions\n  \
19650                     without: {plain}\n  with:    {commented}"
19651                );
19652                assert_eq!(without, expected, "{lang:?}: {plain}");
19653                checked += 1;
19654            }
19655        }
19656        assert!(
19657            checked > 0,
19658            "no slot language enabled; this test asserted nothing"
19659        );
19660    }
19661}
19662
19663/// Slot occupants that no fixed index or first-named-child read finds
19664/// (#1455): a preamble before the slot, a comment *after* the operand
19665/// of a last-operand read, and the non-condition reads that were
19666/// positional too. Each row pairs a spelling with its plain twin.
19667#[cfg(test)]
19668#[cfg(any(
19669    feature = "cpp",
19670    feature = "elixir",
19671    feature = "mozcpp",
19672    feature = "perl",
19673    feature = "ruby",
19674    feature = "rust"
19675))]
19676mod slot_role_reads {
19677    use crate::test_support::metrics_verbatim;
19678    use crate::{LANG, MetricsOptions};
19679
19680    /// `(conditions, assignments)`.
19681    fn abc(lang: LANG, source: &str) -> (u64, u64) {
19682        let abc = metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default()).abc;
19683        (abc.conditions_sum(), abc.assignments_sum())
19684    }
19685
19686    /// `(language, plain, variant, (conditions, assignments))`.
19687    const ROWS: &[(LANG, &str, &str, (u64, u64))] = &[
19688        // A C++ `condition_clause` holds its condition in the `value`
19689        // field; an init-statement sits at child(1) before it.
19690        (
19691            LANG::Cpp,
19692            "int f(){ int y = g(); if (b) { x(); } }",
19693            "int f(){ if (int y = g(); b) { x(); } }",
19694            (1, 1),
19695        ),
19696        (
19697            LANG::Mozcpp,
19698            "int f(){ int y = g(); if (b) { x(); } }",
19699            "int f(){ if (int y = g(); b) { x(); } }",
19700            (1, 1),
19701        ),
19702        (
19703            LANG::Cpp,
19704            "int f(){ if constexpr (b) { x(); } }",
19705            "int f(){ if constexpr (/*c*/ b) { x(); } }",
19706            (1, 0),
19707        ),
19708        // A loop label is the `while` expression's first child.
19709        (
19710            LANG::Rust,
19711            "fn f(){ while b { x(); } }",
19712            "fn f(){ 'a: while b { x(); } }",
19713            (1, 0),
19714        ),
19715        // Perl's `(…)` evaluates to its last element, so the peel takes
19716        // the last operand — never a trailing comment.
19717        (
19718            LANG::Perl,
19719            "sub f { if ($b) { x(); } }",
19720            "sub f { if ($b # c\n) { x(); } }",
19721            (1, 0),
19722        ),
19723        (
19724            LANG::Perl,
19725            "sub f { x() if $b; }",
19726            "sub f { x() if $b # c\n; }",
19727            (1, 0),
19728        ),
19729        // `split`'s delimiter is its first *operand*; a comment before it
19730        // made the pattern read as an ordinary match.
19731        (
19732            LANG::Perl,
19733            "sub f { my @a = split(/,/, $s); }",
19734            "sub f { my @a = split( # sep\n /,/, $s); }",
19735            (0, 1),
19736        ),
19737        // A subject-less `when` scores its operand once; the comment
19738        // made the peel return it, so `x > 1` counted twice.
19739        (
19740            LANG::Ruby,
19741            "def f\n  case\n  when (x > 1)\n    y\n  end\nend\n",
19742            "def f\n  case\n  when ( # c\n x > 1)\n    y\n  end\nend\n",
19743            (1, 0),
19744        ),
19745        // Elixir's match operator, read at child(1), lost to a comment
19746        // before the `=` — an assignment rather than a condition.
19747        (
19748            LANG::Elixir,
19749            "defmodule M do\n def f do\n  a = 1\n end\nend\n",
19750            "defmodule M do\n def f do\n  a # c\n = 1\n end\nend\n",
19751            (0, 1),
19752        ),
19753    ];
19754
19755    #[test]
19756    fn a_slot_is_read_by_role() {
19757        let mut checked = 0;
19758        for &(lang, plain, variant, expected) in ROWS.iter().filter(|row| row.0.is_enabled()) {
19759            let without = abc(lang, plain);
19760            assert_eq!(
19761                abc(lang, variant),
19762                without,
19763                "{lang:?}: the variant scored differently\n  plain:   {plain}\n  variant: {variant}"
19764            );
19765            assert_eq!(without, expected, "{lang:?}: {plain}");
19766            checked += 1;
19767        }
19768        assert!(
19769            checked > 0,
19770            "no row language enabled; this test asserted nothing"
19771        );
19772    }
19773}