Skip to main content

big_code_analysis/metrics/
cognitive.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::match_same_arms,
10    clippy::needless_pass_by_value,
11    clippy::wildcard_imports
12)]
13// Metric counts (token, function, branch, argument, etc.) are stored as
14// `usize` and crossed with `f64` averages, ratios, and Halstead scores
15// across the cyclomatic / MI / Halstead computations. The `usize as f64`
16// and `f64 as usize` casts are intentional and snapshot-anchored — every
17// site is bounded by the count it came from. Allowing the lints at the
18// module level keeps the metric arithmetic legible.
19#![allow(
20    clippy::cast_precision_loss,
21    clippy::cast_possible_truncation,
22    clippy::cast_sign_loss
23)]
24
25use crate::spaces::{Nesting, NestingMap};
26
27use std::fmt;
28
29use crate::checker::Checker;
30use crate::macros::implement_metric_trait;
31use crate::*;
32
33// TODO: Find a way to increment the cognitive complexity value
34// for recursive code. For some kind of languages, such as C++, it is pretty
35// hard to detect, just parsing the code, if a determined function is recursive
36// because the call graph of a function is solved at runtime.
37// So a possible solution could be searching for a crate which implements
38// a light language interpreter, computing the call graph, and then detecting
39// if there are cycles. At this point, it is possible to figure out if a
40// function is recursive or not.
41
42/// The `Cognitive Complexity` metric.
43#[derive(Debug, Clone, PartialEq)]
44#[non_exhaustive]
45pub struct Stats {
46    structural: usize,
47    structural_sum: usize,
48    structural_min: usize,
49    structural_max: usize,
50    nesting: usize,
51    total_space_functions: usize,
52    boolean_seq: BoolSequence,
53}
54
55impl Default for Stats {
56    fn default() -> Self {
57        Self {
58            structural: 0,
59            structural_sum: 0,
60            structural_min: usize::MAX,
61            structural_max: 0,
62            nesting: 0,
63            total_space_functions: 1,
64            boolean_seq: BoolSequence::default(),
65        }
66    }
67}
68
69impl fmt::Display for Stats {
70    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
71        write!(
72            f,
73            "sum: {}, average: {}, min:{}, max: {}",
74            self.cognitive(),
75            self.cognitive_average(),
76            self.cognitive_min(),
77            self.cognitive_max()
78        )
79    }
80}
81
82impl Stats {
83    /// Merges a second `Cognitive Complexity` metric into the first one
84    pub fn merge(&mut self, other: &Stats) {
85        self.structural_min = self.structural_min.min(other.structural_min);
86        self.structural_max = self.structural_max.max(other.structural_max);
87        self.structural_sum += other.structural_sum;
88    }
89
90    /// Returns the `Cognitive Complexity` metric value
91    #[must_use]
92    pub fn cognitive(&self) -> u64 {
93        self.structural as u64
94    }
95    /// Returns the `Cognitive Complexity` sum metric value
96    #[must_use]
97    pub fn cognitive_sum(&self) -> u64 {
98        self.structural_sum as u64
99    }
100
101    /// Returns the `Cognitive Complexity` minimum metric value.
102    ///
103    /// Collapses the `usize::MAX` sentinel that `Stats::default()` plants
104    /// into `structural_min` to `0`, so a never-observed space
105    /// serializes to a meaningful number rather than `1.8446744e19`.
106    #[must_use]
107    pub fn cognitive_min(&self) -> u64 {
108        if self.structural_min == usize::MAX {
109            0
110        } else {
111            self.structural_min as u64
112        }
113    }
114    /// Returns the `Cognitive Complexity` maximum metric value
115    #[must_use]
116    pub fn cognitive_max(&self) -> u64 {
117        self.structural_max as u64
118    }
119
120    /// Returns the `Cognitive Complexity` metric average value
121    ///
122    /// This value is computed dividing the `Cognitive Complexity` value
123    /// for the total number of functions/closures in a space.
124    ///
125    /// The per-function divisor (shared with `cyclomatic`/`exit`/`nargs`,
126    /// #512) is guarded with `.max(1)` via the shared `average` helper, so
127    /// a space with no counted functions (or one where `Nom` was not
128    /// selected) degrades to `sum / 1` instead of producing `inf`/`NaN`
129    /// (#428).
130    #[must_use]
131    pub fn cognitive_average(&self) -> f64 {
132        crate::metrics::average(self.cognitive_sum() as f64, self.total_space_functions)
133    }
134    #[inline]
135    pub(crate) fn compute_sum(&mut self) {
136        self.structural_sum += self.structural;
137    }
138    #[inline]
139    pub(crate) fn compute_minmax(&mut self) {
140        self.structural_min = self.structural_min.min(self.structural);
141        self.structural_max = self.structural_max.max(self.structural);
142        self.compute_sum();
143    }
144
145    pub(crate) fn finalize(&mut self, total_space_functions: usize) {
146        self.total_space_functions = total_space_functions;
147    }
148}
149
150#[doc(hidden)]
151/// Per-language computation of the cognitive complexity metric.
152pub(crate) trait Cognitive
153where
154    Self: Checker,
155{
156    /// Whether [`compute`](Cognitive::compute) writes a `nesting_map`
157    /// slot for the node it was handed.
158    ///
159    /// Every real implementation does, on every path, which is what
160    /// lets the walker size the map to the node count up front. The
161    /// macro-generated no-op impls (`Preproc`, `Ccomment`) never write
162    /// one, so their map stays empty and must stay unallocated too —
163    /// they override this to `false`.
164    const SEEDS_NESTING: bool = true;
165
166    /// Walk `node` and update `stats` with this metric for the language
167    /// implementing the trait.
168    ///
169    /// `code` is the source bytes underlying the parsed tree. Most
170    /// languages ignore it: their control-flow constructs surface as
171    /// distinct grammar productions (`IfStatement`, `WhileStatement`,
172    /// …) and a `kind_id()` match is enough. Elixir is the exception
173    /// — `if` / `unless` / `case` / `cond` / `for` / `while` / `with`
174    /// all surface as `Call` nodes whose keyword target lives only in
175    /// the source text (the `target` field is an `Identifier`). This
176    /// matches the `Cyclomatic` / `Halstead` / `Exit` pattern of
177    /// taking `code` so the same source-text dispatch can run here.
178    ///
179    /// `ancestors` is the chain the walker descended through. The
180    /// grammars that spell `else if` as a nested `if` need an ancestor
181    /// to recognise the continuation, and Python needs one to find the
182    /// outermost operator of a boolean chain; resolving either from the
183    /// node alone costs `O(depth)` per node (#1084).
184    fn compute<'a>(
185        node: &Node<'a>,
186        code: &'a [u8],
187        ancestors: Ancestors<'a, '_>,
188        stats: &mut Stats,
189        nesting_map: &mut NestingMap,
190    );
191}
192
193/// Walks `node.children()` and folds each child whose `kind_id`
194/// satisfies `is_op` into the boolean-sequence counter. The predicate
195/// is the only thing that differs across the per-language short-
196/// circuit helpers (`compute_*_booleans`); inlining the predicate as
197/// a `Fn` closure lets each language declare its operator set with a
198/// `matches!` pattern at the call site without duplicating the walk.
199fn compute_booleans_with<F: Fn(u16) -> bool>(node: &Node, stats: &mut Stats, is_op: F) {
200    let enclosing_end = node.end_byte();
201    for child in node.children() {
202        let id = child.kind_id();
203        if is_op(id) {
204            stats.structural =
205                stats
206                    .boolean_seq
207                    .eval_based_on_prev(id, enclosing_end, stats.structural);
208        }
209    }
210}
211
212/// Two-operator specialization. Most call sites match exactly two
213/// enum variants (`&&` + `||`, or `and` + `or`); this signature
214/// keeps those call sites as plain `(node, stats, A, B)` rather than
215/// forcing a closure.
216fn compute_booleans<T: PartialEq + From<u16>>(node: &Node, stats: &mut Stats, typs1: T, typs2: T) {
217    compute_booleans_with(node, stats, |id| {
218        let converted: T = id.into();
219        typs1 == converted || typs2 == converted
220    });
221}
222
223#[derive(Debug, Default, Clone, PartialEq)]
224struct BoolSequence {
225    boolean_op: Option<(u16, usize)>,
226}
227
228impl BoolSequence {
229    fn reset(&mut self) {
230        // Structural boundaries (new branches, nesting increments) end the current sequence.
231        self.boolean_op = None;
232    }
233
234    fn eval_based_on_prev(
235        &mut self,
236        bool_id: u16,
237        enclosing_end: usize,
238        structural: usize,
239    ) -> usize {
240        match self.boolean_op {
241            // Same operator type and enclosing_end fits inside the previously seen
242            // binary_expression span (pre-order: parent visited before child) →
243            // continuation of the same sequence, no extra cost.
244            Some((prev_id, prev_end)) if prev_id == bool_id && enclosing_end <= prev_end => {
245                structural
246            }
247            _ => {
248                self.boolean_op = Some((bool_id, enclosing_end));
249                structural + 1
250            }
251        }
252    }
253}
254
255#[inline]
256fn increment(stats: &mut Stats) {
257    stats.structural += stats.nesting + 1;
258}
259
260#[inline]
261fn increment_by_one(stats: &mut Stats) {
262    stats.structural += 1;
263}
264
265#[inline]
266fn increment_branch_extension(stats: &mut Stats) {
267    stats.structural += 1;
268    stats.boolean_seq.reset();
269}
270
271/// Returns the [`Nesting`] `node` inherits from its parent.
272///
273/// The map is keyed so that a node's own slot holds what it *inherits*:
274/// the walker seeds each child's slot from its parent's slot after the
275/// parent's `compute` has run (see `propagate_nesting_to_children` in
276/// `spaces::compute`). Reading `node.parent()` here instead would cost
277/// `O(depth)` per node — `Node::parent` walks down from the root — which
278/// made this metric quadratic in nesting depth (#1062).
279fn get_nesting_from_map(node: &Node, nesting_map: &NestingMap) -> Nesting {
280    nesting_map.get(&node.id()).copied().unwrap_or_default()
281}
282
283/// Adds one to `depth` when `node` is lexically nested inside another
284/// function, where `stops` lists the grammar kinds that count as a
285/// function for this language.
286///
287/// The scan reads the walker's ancestor chain. Climbing with
288/// [`Node::parent`] instead costs `O(depth)` per step — tree-sitter
289/// stores no parent pointer — which made `Cognitive` `O(depth²)` on
290/// nested function definitions (#1062, deferred out of #1084). A nested
291/// function's enclosing function is a couple of levels up, so the scan
292/// stops immediately there; a function with *no* enclosing function
293/// scans its whole chain, but each step is a slice index rather than a
294/// descent from the root.
295fn increment_function_depth<'a, T: PartialEq + From<u16>>(
296    depth: &mut usize,
297    node: &Node<'a>,
298    ancestors: Ancestors<'a, '_>,
299    stops: &[T],
300) {
301    if ancestors
302        .iter(node)
303        .any(|(ancestor, _)| stops.contains(&T::from(ancestor.kind_id())))
304    {
305        *depth += 1;
306    }
307}
308
309/// Applies the function-boundary rule at `node`, which every language
310/// with a syntactic function-definition kind shares (#696).
311///
312/// It moves all three of [`Nesting`]'s channels. Structural nesting and
313/// the lambda surcharge restart at zero, so control flow written inside
314/// this function is charged against its own depth rather than against
315/// whatever enclosed the definition; and the function-depth surcharge
316/// rises when this definition is itself lexically nested in one of
317/// `stops`. Byte-equivalent constructs therefore score the same across
318/// languages, which is the property the book's per-language deviations
319/// list states.
320///
321/// The lambda reset was the JS macro's alone until #1187. Every other
322/// language carried the enclosing closure's surcharge into a function
323/// *declared inside* it, so the same body scored 3 or 2 depending on
324/// whether something two levels up happened to be a closure — measured
325/// in Rust, Java, C++, PHP and C#, where a `LocalFunctionStatement`
326/// inside a lambda is idiomatic. A function declaration is a new lexical
327/// scope whatever encloses it, so the reset belongs to every boundary,
328/// and living here is what stops a language opting out by accident —
329/// which is how the gap arose.
330///
331/// The two statements were spelled out longhand in eighteen modules
332/// before #1103. One caller still spells them out: `elixir.rs` takes the
333/// resets and deliberately skips the depth bump, and says why at its own
334/// site.
335fn enter_function_boundary<'a, T: PartialEq + From<u16>>(
336    nesting: &mut Nesting,
337    node: &Node<'a>,
338    ancestors: Ancestors<'a, '_>,
339    stops: &[T],
340) {
341    nesting.conditional = 0;
342    nesting.lambda = 0;
343    increment_function_depth(&mut nesting.function_depth, node, ancestors, stops);
344}
345
346/// Charges `node`'s construct at the current nesting level and opens a
347/// new structural level for its children.
348///
349/// Takes the whole [`Nesting`] rather than its three same-typed fields
350/// positionally: the previous signature was
351/// `(stats, &mut nesting, depth, lambda)` at 43 call sites, where any
352/// two of the trailing arguments could be transposed silently (#1086).
353#[inline]
354fn increase_nesting(stats: &mut Stats, nesting: &mut Nesting) {
355    stats.nesting = nesting.total();
356    increment(stats);
357    nesting.conditional += 1;
358    stats.boolean_seq.reset();
359}
360
361/// Whether `node` is a Python `lambda` expression, under either of the
362/// grammar's two aliased kind_ids: `Lambda` (196, the concrete
363/// production emitted today) and `Lambda2` (197, the currently-unseen
364/// hidden alias). `Lambda3` (73) is the `lambda` *keyword* token, not a
365/// closure node, and is intentionally excluded.
366///
367/// This is the single normalization chokepoint for the lambda-alias set
368/// — mirroring `npa::python_is_block` for the block aliases (#419). It
369/// is reused by the cognitive lambda-scope walks below and by
370/// [`PythonCode::is_closure`](crate::checker), so a future grammar bump
371/// that promotes `Lambda2` to a concrete node is handled in exactly one
372/// place rather than drifting across sites (#422). The
373/// `python_hidden_block_and_lambda_aliases_stay_unseen` drift guard in
374/// `checker.rs` trips on such a bump.
375pub(crate) fn python_is_lambda(node: &Node) -> bool {
376    matches!(node.kind_id().into(), Python::Lambda | Python::Lambda2)
377}
378
379macro_rules! js_cognitive {
380    ($lang:ident) => {
381        fn compute<'a>(
382            node: &Node<'a>,
383            _code: &'a [u8],
384            ancestors: Ancestors<'a, '_>,
385            stats: &mut Stats,
386            nesting_map: &mut NestingMap,
387        ) {
388            use $lang::*;
389            let mut nesting = get_nesting_from_map(node, nesting_map);
390
391            match node.kind_id().into() {
392                IfStatement if !Self::is_else_if(node, ancestors) => {
393                    increase_nesting(stats, &mut nesting);
394                }
395                ForStatement | ForInStatement | WhileStatement | DoStatement | SwitchStatement
396                | CatchClause | TernaryExpression => {
397                    increase_nesting(stats, &mut nesting);
398                }
399                // `Else` here is the `else` keyword token, which the
400                // grammar also emits for the `else` of an `else if` —
401                // so this arm covers both.
402                Else => {
403                    increment_by_one(stats);
404                }
405                // Per SonarSource Cognitive Complexity §B2, a labeled
406                // `break LABEL` / `continue LABEL` is an unstructured jump
407                // and adds +1. The JS-family grammar exposes the label as a
408                // `StatementIdentifier` child (not the plain `Identifier`
409                // Java uses), so gate on that kind; plain `break;` /
410                // `continue;` have no such child and add +0.
411                BreakStatement | ContinueStatement if node.is_child(StatementIdentifier as u16) => {
412                    increment_by_one(stats);
413                }
414                ExpressionStatement => {
415                    // Reset the boolean sequence
416                    stats.boolean_seq.reset();
417                }
418                BinaryExpression => {
419                    // `??` (`QMARKQMARK`) short-circuits like `&&` /
420                    // `||`, so a chain of `??` collapses to a single
421                    // boolean-sequence increment under Sonar B1.
422                    compute_booleans_with(node, stats, |id| {
423                        matches!(id.into(), AMPAMP | PIPEPIPE | QMARKQMARK)
424                    });
425                }
426                AugmentedAssignmentExpression => {
427                    // Compound short-circuit assignments `&&=`, `||=`,
428                    // `??=` are semantically `x = x op y` and each carries
429                    // one boolean-sequence decision, parallel to the
430                    // cyclomatic fix from #231. The operator token sits
431                    // inside the augmented-assignment node rather than a
432                    // `BinaryExpression`, so it needs its own arm (#236).
433                    compute_booleans_with(node, stats, |id| {
434                        matches!(id.into(), AMPAMPEQ | PIPEPIPEEQ | QMARKQMARKEQ)
435                    });
436                }
437                FunctionDeclaration
438                | MethodDefinition
439                | FunctionExpression
440                | GeneratorFunctionDeclaration
441                | GeneratorFunction
442                    if Self::is_func(node, ancestors) =>
443                {
444                    // The kind set is `is_js_func!` minus `ArrowFunction`,
445                    // and the `function_expression` half is re-derived by
446                    // asking `Self::is_func` rather than copied flat, because
447                    // `function_expression` covers both a function and a
448                    // closure. `check_if_func!` is what separates them: its
449                    // ancestor walk marks the expression a function when a
450                    // binding frame (`var x = …`, `x = …`, `label:`, object
451                    // `pair`) is reached before a positional one, and its
452                    // `$extra` disjunct additionally marks any expression
453                    // carrying its own `identifier` name child. So
454                    // `const f = function () {}` and `run(function f () {})`
455                    // are functions, while `run(function () {})` is a closure
456                    // and must keep falling through to `_`. That
457                    // classification is inherited from `is_func`, not
458                    // endorsed here — it is also what makes `nom` call the
459                    // same node a function or a closure, so cognitive
460                    // disagreeing with it would be the larger bug.
461                    // `ArrowFunction` stays out because it owns the lambda
462                    // channel in the arm below. Listing `FunctionDeclaration`
463                    // alone left a method or a bound function expression
464                    // inheriting the enclosing conditional nesting (#1159).
465                    //
466                    // `stops` takes bare kinds, so re-applying that gate to
467                    // an *ancestor* would mean changing
468                    // `increment_function_depth`'s signature. Leaving it
469                    // ungated is deliberate rather than a shortcut: an
470                    // anonymous IIFE is a lexical function scope —
471                    // `get_space_kind` maps every `function_expression` to
472                    // `SpaceKind::Function` — so a `function` declared inside
473                    // one really is nested in a function.
474                    //
475                    // `ArrowFunction` is in the list since #1187, which is
476                    // what makes `(function () { function g() {…} })()` and
477                    // `(() => { function g() {…} })()` both charge `g` a
478                    // depth of 1; the arrow form charged 0 while the kind
479                    // was absent. It cannot double-charge, because the sole
480                    // caller resets `nesting.lambda` first.
481                    //
482                    // Both generator kinds are in the arm and in `stops`
483                    // since #1186, which moved them from `is_js_closure!`
484                    // to `is_js_func!`. The two halves are independent
485                    // and had to move together: the arm decides whether
486                    // `function* g()` resets its own inherited nesting,
487                    // while `stops` decides whether a plain `function`
488                    // nested *inside* a generator gets a depth surcharge.
489                    // `GeneratorFunction` is gated by `Self::is_func` for
490                    // the same reason `FunctionExpression` is — it has an
491                    // optional name and covers both a function and a
492                    // closure — while `GeneratorFunctionDeclaration`,
493                    // like `FunctionDeclaration`, is unconditional.
494                    enter_function_boundary(
495                        &mut nesting,
496                        node,
497                        ancestors,
498                        &[
499                            FunctionDeclaration,
500                            MethodDefinition,
501                            FunctionExpression,
502                            GeneratorFunctionDeclaration,
503                            GeneratorFunction,
504                            ArrowFunction,
505                            ClassStaticBlock,
506                        ],
507                    );
508                }
509                // A class static block is a function boundary but is
510                // deliberately *not* in `is_func` (#1184), so it needs
511                // its own ungated arm rather than joining the gated one
512                // above — gated, it would never fire. It is in the
513                // `stops` list below for the same reason a
514                // `function_expression` is: a `function` declared inside
515                // a `static { … }` really is nested in one.
516                ClassStaticBlock => {
517                    enter_function_boundary(
518                        &mut nesting,
519                        node,
520                        ancestors,
521                        &[
522                            FunctionDeclaration,
523                            MethodDefinition,
524                            FunctionExpression,
525                            GeneratorFunctionDeclaration,
526                            GeneratorFunction,
527                            ArrowFunction,
528                            ClassStaticBlock,
529                        ],
530                    );
531                }
532                ArrowFunction => {
533                    nesting.lambda += 1;
534                }
535                _ => {}
536            }
537            nesting_map.insert(node.id(), nesting);
538        }
539    };
540}
541
542// Per-language `Cognitive` impls live in sibling modules. The `mod`
543// declarations sit after the local `macro_rules! js_cognitive!` so
544// textual macro scoping reaches the JS-family child files (mirrors
545// `getter.rs`, `metrics::npm`, `metrics::cyclomatic`).
546mod bash;
547mod c;
548mod cpp;
549mod csharp;
550mod elixir;
551mod go;
552mod groovy;
553mod irules;
554mod java;
555mod javascript;
556mod kotlin;
557mod lua;
558mod mozcpp;
559mod mozjs;
560mod objc;
561mod perl;
562mod php;
563mod python;
564mod ruby;
565mod rust;
566mod tcl;
567mod tsx;
568mod typescript;
569
570// Reads the text of the `target` field of an Elixir `Call` node.
571//
572// Most of Elixir's control-flow constructs (`if`, `unless`, `for`,
573// `while`, `case`, `cond`, `with`, `try`) and method-defining macros
574// (`def`, `defp`, `defmacro`, …) parse as `Call` nodes whose `target`
575// is an `Identifier` whose source text spells the keyword. The
576// `Cyclomatic` and `Exit` impls already follow this pattern; this
577// helper centralises the byte-text lookup so `Cognitive` and `Abc`
578// can share it.
579//
580// Returns `None` for Calls whose target is not a simple identifier
581// (e.g. `Module.func(…)` parses as `RemoteCallWithParentheses` with
582// the dotted name as target) or when the bytes are not valid UTF-8.
583pub(crate) fn elixir_call_keyword<'a>(node: &'a Node<'a>, code: &'a [u8]) -> Option<&'a str> {
584    if node.kind_id() != Elixir::Call as u16 {
585        return None;
586    }
587    let target = node.child_by_field_name("target")?;
588    if target.kind_id() != Elixir::Identifier as u16 {
589        return None;
590    }
591    target.utf8_text(code)
592}
593
594// Reads the leading word of a Tcl `command` node when it is a plain
595// `simple_word` (`switch`, `for`, `puts`, …). Returns `None` for any other
596// node kind, for commands whose leading word is computed (`$cmd`, `[cmd]`
597// parse it as `variable_substitution` / `command_substitution`, never
598// statically resolvable to a builtin), and for non-UTF-8 bytes. Shared by
599// the out-of-band control-flow detectors below (grammar-dispatch §10:
600// identity questions read the bytes).
601//
602// A *literal* name in a quoted or braced spelling is also unresolved, and
603// that is a deliberate limitation: `"for" {set i 0} {$i < 3} {incr i} {…}`
604// and `{for} …` are legal Tcl that still invoke the builtin, but the
605// grammar parses their name as `quoted_word` / `braced_word` rather than
606// `simple_word`, so they score as plain commands. The `simple_word` gate
607// is what keeps the computed forms out; matching the quoted spellings
608// would mean unquoting the bytes for a style no real Tcl uses.
609//
610// Callers dispatch on the returned name so each `command` node resolves it
611// exactly once per metric walk — the helpers below take the resolved
612// identity as a precondition rather than re-deriving it.
613pub(crate) fn tcl_command_name<'a>(node: &'a Node<'a>, code: &'a [u8]) -> Option<&'a str> {
614    if node.kind_id() != Tcl::Command as u16 {
615        return None;
616    }
617    let name = node.child_by_field_name("name")?;
618    if name.kind_id() != Tcl::SimpleWord as u16 {
619        return None;
620    }
621    name.utf8_text(code)
622}
623
624// Tcl's `switch` is a generic `command` (no dedicated kind_id, unlike
625// `if`/`while`/`foreach`/`catch`), so the kind-dispatch in the Cognitive
626// and Cyclomatic impls never sees it (issue #467, lesson 19). Both metrics
627// reach it through `tcl_command_name` and then through this pair: the arm
628// list locates the supported form, and `tcl_switch_decision_arms` counts
629// it. Cognitive needs only the former — it charges one structure whatever
630// the arm count is — so the count stays out of its path.
631//
632// Grammar shape (tree-sitter-tcl 0.x), canonical brace-list form:
633//
634//   (command name: (simple_word "switch")
635//     (word_list <options…> <value> (braced_word (command (simple_word PAT) …) …)))
636//
637// The arm list is the sole `braced_word` argument inside the command's
638// `word_list`, which makes the lookup robust to leading options (`-exact`,
639// `-glob`, `-regexp`, `-nocase`, `--`) and the matched value, all of which
640// precede it and never parse as `braced_word`. The rarer split form
641// (`switch $x a {b} c {d}` — arms as separate `word_list` arguments rather
642// than wrapped in one `braced_word`) produces *several* sibling
643// `braced_word`s, one per arm body, so requiring exactly one distinguishes
644// the brace-list form and excludes the split form, where the last
645// `braced_word` is merely a body rather than the full arm list. The split
646// form is intentionally NOT counted: its body braces are sibling
647// arguments, not nested commands, so there is no reliable arm node to
648// count. Idiomatic Tcl uses the brace-list form, so this scoping
649// under-counts only the uncommon style.
650//
651// **Precondition**: `node` is a `command` whose leading word resolved to
652// `"switch"` — the callers' `tcl_command_name` dispatch establishes that,
653// and re-checking it here would resolve the name a second time on every
654// switch.
655pub(crate) fn tcl_switch_arm_list<'a>(node: &Node<'a>) -> Option<Node<'a>> {
656    let word_list = node
657        .children()
658        .find(|child| child.kind_id() == Tcl::WordList as u16)?;
659    let mut braced_words = word_list
660        .children()
661        .filter(|child| child.kind_id() == Tcl::BracedWord as u16);
662    let arm_list = braced_words.next()?;
663    braced_words.next().is_none().then_some(arm_list)
664}
665
666// The number of *decision* arms of a `switch` — every arm but `default`,
667// which is the fallback and does not contribute a decision point, matching
668// the C-family `default:` convention (lesson 11). Each arm is a nested
669// `command` whose leading word is the pattern.
670//
671// Carries [`tcl_switch_arm_list`]'s precondition, and returns `None` for
672// the same unsupported split form, so callers leave it untouched exactly
673// as they leave a non-switch command.
674pub(crate) fn tcl_switch_decision_arms(node: &Node, code: &[u8]) -> Option<usize> {
675    let decision_arms = tcl_switch_arm_list(node)?
676        .children()
677        .filter(|arm| arm.kind_id() == Tcl::Command as u16)
678        .filter(|arm| {
679            arm.child_by_field_name("name")
680                .and_then(|pat| pat.utf8_text(code))
681                != Some("default")
682        })
683        .count();
684    Some(decision_arms)
685}
686
687// Tcl's `try` has a dedicated kind (unlike `switch`/`for`), but its error
688// handler is a flat run of sibling tokens rather than a wrapper node
689// (issue #1266):
690//
691//   (try "try" <body> ["on" "error" (arguments) <body>] [(finally)])
692//
693// The vendored grammar permits at most one `on error` handler and has no
694// `trap` rule at all — Tcl 8.6's multi-handler and `trap` forms degrade to
695// an `ERROR` node, a grammar limitation like the `for` condition in #1264.
696// The scan still tolerates repetition so a grammar bump that adds those
697// forms keeps counting without an edit here.
698//
699// Each handler body is located by structural position, never fixed index
700// (grammar-dispatch §3): it is the child directly following the handler's
701// `arguments` node. That makes the scan pairwise — every child paired with
702// its predecessor, yielding the successor of each `arguments` — rather
703// than a state machine over the `on` / `error` / `arguments` run. The
704// grammar's extras are whitespace-only, so nothing can interpose between
705// the `arguments` node and the body it introduces, and `arguments` appears
706// among a `try`'s direct children only in handler position. Repetition
707// costs nothing here: a grammar bump that admits several handlers (or
708// `trap`) is counted without an edit, because each `arguments` yields its
709// own successor. The `finally` clause is a wrapper kind (`Tcl::Finally`)
710// and is never yielded — unconditional cleanup costs nothing, matching the
711// cross-language `finally` convention (#416).
712//
713// Shared by Cognitive (which also seeds each body's nesting slot) and
714// Cyclomatic (which counts the bodies); both call it from their `Tcl::Try`
715// dispatch arm.
716pub(crate) fn tcl_try_handler_bodies<'a>(node: &Node<'a>) -> impl Iterator<Item = Node<'a>> {
717    node.children()
718        .zip(node.children().skip(1))
719        .filter(|(previous, _)| previous.kind_id() == Tcl::Arguments as u16)
720        .map(|(_, body)| body)
721}
722
723// iRules counterpart to [`tcl_switch_decision_arms`]. Unlike Tcl, the iRules
724// grammar models `switch` as a dedicated node with `switch_arm` children, so
725// the arms are read off the tree directly instead of re-parsing a generic
726// command. Returns the number of non-`default` arms (each a decision point in
727// standard CCN); `None` when `node` is not a `switch`. The `default` arm is the
728// fallback and does not contribute a branch (the Java/C-family wildcard
729// convention — lesson 11, #106).
730pub(crate) fn irules_switch_decision_arms(node: &Node, code: &[u8]) -> Option<usize> {
731    if node.kind_id() != Irules::Switch as u16 {
732        return None;
733    }
734    let decision_arms = node
735        .children()
736        .filter(|arm| arm.kind_id() == Irules::SwitchArm as u16)
737        .filter(|arm| {
738            arm.child_by_field_name("pattern")
739                .and_then(|pat| pat.utf8_text(code))
740                != Some("default")
741        })
742        .count();
743    Some(decision_arms)
744}
745
746// Method-defining macros (`def`, `defp`, `defmacro`, `defmacrop`). The set
747// is duplicated across checker, getter, and several metric impls
748// because each consults it from a different trait surface; centralising
749// the literal here keeps future additions (e.g. `defguard`) consistent.
750#[inline]
751pub(crate) fn elixir_is_method_macro(kw: &str) -> bool {
752    matches!(kw, "def" | "defp" | "defmacro" | "defmacrop")
753}
754
755// Class-defining macro (`defmodule`). Paired with [`elixir_is_method_macro`]
756// where a caller needs both ("any space-opening declaration").
757#[inline]
758pub(crate) fn elixir_is_class_macro(kw: &str) -> bool {
759    kw == "defmodule"
760}
761
762// Returns true when `node` is lexically nested inside the `do_block` of a
763// `quote do … end` Call (Elixir's metaprogramming template). A `def` /
764// `defp` / `defmacro` / `defmacrop` inside `quote` does not define a
765// method of any enclosing module — the syntax tree is a code template
766// emitted later, when the surrounding macro is invoked. Treating those
767// quoted Calls as methods inflates `Wmc` and disagrees with `Npm`'s
768// direct-children classification (#310).
769//
770// Walks the ancestor chain looking for a `quote` Call ancestor. Stops at
771// the first match (true) or at the root (false). Each step is a single
772// `child_by_field_name("target")` + identifier byte compare, so the cost
773// is O(steps) when `ancestors` is known — with `Ancestors::unknown` each
774// step additionally pays `Node::parent`'s O(depth) (#1084).
775pub(crate) fn elixir_is_inside_quote_block<'a>(
776    node: &Node<'a>,
777    code: &[u8],
778    ancestors: Ancestors<'a, '_>,
779) -> bool {
780    ancestors
781        .iter(node)
782        .any(|(n, _)| elixir_call_keyword(&n, code) == Some("quote"))
783}
784
785// Iterates the direct-child `Call` nodes inside the `do_block` of an
786// Elixir Call (typically a `defmodule`). Used by `Npm` / `Npa` to scan
787// a module body for method-defining macros / `defstruct` without
788// descending into nested modules. Yields no items when the Call has
789// no `do_block`.
790pub(crate) fn elixir_do_block_call_children<'a>(
791    node: &'a Node<'a>,
792) -> impl Iterator<Item = Node<'a>> + 'a {
793    node.children()
794        .filter(|child| child.kind_id() == Elixir::DoBlock as u16)
795        .flat_map(|do_block| do_block.children())
796        .filter(|stmt| stmt.kind_id() == Elixir::Call as u16)
797}
798
799implement_metric_trait!(Cognitive, PreprocCode, CcommentCode);
800
801#[cfg(test)]
802#[allow(
803    clippy::float_cmp,
804    clippy::cast_precision_loss,
805    clippy::cast_possible_truncation,
806    clippy::cast_sign_loss,
807    clippy::similar_names,
808    clippy::doc_markdown,
809    clippy::needless_raw_string_hashes,
810    clippy::too_many_lines
811)]
812mod tests {
813    use crate::test_support::{
814        check_func_space_only_shim, check_metrics_only_shim, child_space, function_space,
815    };
816
817    use super::*;
818
819    // Cognitive's dependency closure adds Nom, the divisor behind
820    // `cognitive_average`.
821    check_metrics_only_shim!(check_metrics, Cognitive);
822    check_func_space_only_shim!(check_func_space, Cognitive);
823    // The Python-comprehension tests (#417/#421) assert the cyclomatic
824    // count alongside the cognitive one, to show where the two metrics
825    // agree and where nesting makes them diverge. They are the only
826    // cross-metric assertions here, so they get their own shim rather
827    // than widening the module-wide selection.
828    check_metrics_only_shim!(check_cognitive_and_cyclomatic, Cognitive, Cyclomatic);
829
830    /// The walker must hand `is_else_if` the node's own parent at every
831    /// AST depth.
832    ///
833    /// A bare `{ … }` block carries no cognitive weight in either
834    /// language, so burying an `if / else if / else if` chain under more
835    /// of them cannot move the score — unless the chain of ancestors the
836    /// walker propagates (#1084) drifts. Then the inner `if`s stop
837    /// reading as continuations of the branch above, each pays a fresh
838    /// nesting penalty, and the total climbs with depth.
839    ///
840    /// Both `is_else_if` shapes are covered: C resolves the enclosing
841    /// `else_clause` through the parent, Java through the preceding
842    /// `else` token, which the chain answers by scanning the parent's
843    /// children.
844    #[test]
845    fn else_if_is_recognised_at_every_nesting_depth() {
846        use crate::test_support::metrics_verbatim;
847
848        // 1 for the `if`, plus 1 for each `else if` as a branch
849        // extension. No nesting penalty: an `else if` continues the
850        // chain rather than nesting inside it.
851        const CHAIN_COGNITIVE: u64 = 3;
852
853        let chain = "if (a) { } else if (b) { } else if (c) { }";
854        for depth in 0..=6 {
855            let (open, close) = ("{ ".repeat(depth), " }".repeat(depth));
856            for (lang, source) in [
857                (LANG::C, format!("void f() {{ {open}{chain}{close} }}\n")),
858                (
859                    LANG::Java,
860                    format!("class A {{ void m() {{ {open}{chain}{close} }} }}\n"),
861                ),
862            ] {
863                let metrics = metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default());
864                assert_eq!(
865                    metrics.cognitive.cognitive_sum(),
866                    CHAIN_COGNITIVE,
867                    "{lang:?}: `else if` chain under {depth} plain blocks scored \
868                     {} instead of {CHAIN_COGNITIVE}",
869                    metrics.cognitive.cognitive_sum(),
870                );
871            }
872        }
873    }
874
875    /// `SEEDS_NESTING` must say what `compute` actually does.
876    ///
877    /// The walker trusts the const twice over: `true` means the map is
878    /// worth sizing to the node count up front, `false` means it must be
879    /// left unallocated. Both are silent when wrong — a `false` on a
880    /// language that does seed only costs the rehashing back, and a
881    /// `true` on one that does not only wastes an allocation, so no
882    /// metric value moves either way. This pins each impl against what
883    /// it observably writes.
884    #[test]
885    fn seeds_nesting_matches_what_compute_writes() {
886        use std::path::Path;
887
888        fn writes_a_slot<T: ParserTrait>(source: &str, filename: &str) -> bool {
889            let parser = T::new(source.as_bytes().to_vec(), Path::new(filename), None);
890            let mut nesting_map = NestingMap::default();
891            T::Cognitive::compute(
892                &parser.root(),
893                parser.code(),
894                Ancestors::known(&[]),
895                &mut Stats::default(),
896                &mut nesting_map,
897            );
898            assert_eq!(
899                <T::Cognitive as Cognitive>::SEEDS_NESTING,
900                !nesting_map.is_empty(),
901                "{filename}: SEEDS_NESTING disagrees with what compute wrote"
902            );
903            !nesting_map.is_empty()
904        }
905
906        // One representative of each family that carries a real impl:
907        // the C-like macro, the JS-family macro, and the two languages
908        // with hand-written `compute` bodies.
909        assert!(writes_a_slot::<CppParser>(
910            "int main() { return 0; }",
911            "a.cpp"
912        ));
913        assert!(writes_a_slot::<JavascriptParser>(
914            "function f() { return 0; }",
915            "a.js"
916        ));
917        assert!(writes_a_slot::<PythonParser>(
918            "def f():\n    pass\n",
919            "a.py"
920        ));
921        assert!(writes_a_slot::<ElixirParser>(
922            "def f do\n  :ok\nend\n",
923            "a.ex"
924        ));
925
926        // The macro-generated no-ops: `SEEDS_NESTING` is what keeps the
927        // walker from reserving a map they never fill.
928        assert!(!writes_a_slot::<PreprocParser>("#define A 1\n", "a.h"));
929        assert!(!writes_a_slot::<CcommentParser>("/* c */ int x;", "a.c"));
930    }
931
932    /// A `Stats::default()` that never sees an
933    /// observation must not leak the `usize::MAX` sentinel for
934    /// `structural_min`. The getter collapses the sentinel to `0.0`
935    /// so JSON never emits `1.8446744e19`.
936    #[test]
937    fn cognitive_empty_file_min_is_zero() {
938        let stats = Stats::default();
939        assert_eq!(stats.cognitive_min(), 0);
940    }
941
942    #[test]
943    fn python_no_cognitive() {
944        check_metrics::<PythonParser>("a = 42", "foo.py", |metric| {
945            insta::assert_json_snapshot!(
946                metric.cognitive,
947                @r#"
948            {
949              "sum": 0,
950              "value": 0,
951              "average": 0.0,
952              "min": 0,
953              "max": 0
954            }
955            "#
956            );
957        });
958    }
959
960    #[test]
961    fn rust_no_cognitive() {
962        check_metrics::<RustParser>("let a = 42;", "foo.rs", |metric| {
963            insta::assert_json_snapshot!(
964                metric.cognitive,
965                @r#"
966            {
967              "sum": 0,
968              "value": 0,
969              "average": 0.0,
970              "min": 0,
971              "max": 0
972            }
973            "#
974            );
975        });
976    }
977
978    #[test]
979    fn c_no_cognitive() {
980        check_metrics::<CParser>("int a = 42;", "foo.c", |metric| {
981            insta::assert_json_snapshot!(
982                metric.cognitive,
983                @r#"
984            {
985              "sum": 0,
986              "value": 0,
987              "average": 0.0,
988              "min": 0,
989              "max": 0
990            }
991            "#
992            );
993        });
994    }
995
996    #[test]
997    fn mozjs_no_cognitive() {
998        check_metrics::<MozjsParser>("var a = 42;", "foo.js", |metric| {
999            insta::assert_json_snapshot!(
1000                metric.cognitive,
1001                @r#"
1002            {
1003              "sum": 0,
1004              "value": 0,
1005              "average": 0.0,
1006              "min": 0,
1007              "max": 0
1008            }
1009            "#
1010            );
1011        });
1012    }
1013
1014    #[test]
1015    fn javascript_no_cognitive() {
1016        check_metrics::<JavascriptParser>("var a = 42;", "foo.js", |metric| {
1017            insta::assert_json_snapshot!(
1018                metric.cognitive,
1019                @r#"
1020            {
1021              "sum": 0,
1022              "value": 0,
1023              "average": 0.0,
1024              "min": 0,
1025              "max": 0
1026            }
1027            "#
1028            );
1029        });
1030    }
1031
1032    #[test]
1033    fn python_simple_function() {
1034        check_metrics::<PythonParser>(
1035            "def f(a, b):
1036                if a and b:  # +2 (+1 and)
1037                   return 1
1038                if c and d: # +2 (+1 and)
1039                   return 1",
1040            "foo.py",
1041            |metric| {
1042                insta::assert_json_snapshot!(
1043                    metric.cognitive,
1044                    @r#"
1045                {
1046                  "sum": 4,
1047                  "value": 0,
1048                  "average": 4.0,
1049                  "min": 0,
1050                  "max": 4
1051                }
1052                "#
1053                );
1054            },
1055        );
1056    }
1057
1058    /// Python `match`/`case` (PEP 634, 3.10+) opens cognitive nesting
1059    /// the same way Rust's `match_expression` and the C-family
1060    /// `switch_statement` do. A 2-arm match with one explicit arm
1061    /// plus a wildcard contributes one cognitive decision point.
1062    /// Regression test for #212.
1063    #[test]
1064    fn python_match_two_arm_wildcard() {
1065        check_metrics::<PythonParser>(
1066            "def f(x):
1067    match x:
1068        case 1:
1069            return 'one'
1070        case _:
1071            return 'other'
1072",
1073            "foo.py",
1074            |metric| {
1075                // The `match_statement` contributes one decision point;
1076                // case arms inside add no extra nesting (mirrors Rust /
1077                // C-family switch). cognitive_max = 1.
1078                insta::assert_json_snapshot!(
1079                    metric.cognitive,
1080                    @r#"
1081                {
1082                  "sum": 1,
1083                  "value": 0,
1084                  "average": 1.0,
1085                  "min": 0,
1086                  "max": 1
1087                }
1088                "#
1089                );
1090            },
1091        );
1092    }
1093
1094    #[test]
1095    fn python_expression_statement() {
1096        // Boolean expressions containing `And` and `Or` operators were not
1097        // considered in assignments
1098        check_metrics::<PythonParser>(
1099            "def f(a, b):
1100                c = True and True",
1101            "foo.py",
1102            |metric| {
1103                insta::assert_json_snapshot!(
1104                    metric.cognitive,
1105                    @r#"
1106                {
1107                  "sum": 1,
1108                  "value": 0,
1109                  "average": 1.0,
1110                  "min": 0,
1111                  "max": 1
1112                }
1113                "#
1114                );
1115            },
1116        );
1117    }
1118
1119    #[test]
1120    fn python_tuple() {
1121        // Boolean expressions containing `And` and `Or` operators were not
1122        // considered inside tuples
1123        check_metrics::<PythonParser>(
1124            "def f(a, b):
1125                return \"%s%s\" % (a and \"Get\" or \"Set\", b)",
1126            "foo.py",
1127            |metric| {
1128                insta::assert_json_snapshot!(
1129                    metric.cognitive,
1130                    @r#"
1131                {
1132                  "sum": 2,
1133                  "value": 0,
1134                  "average": 2.0,
1135                  "min": 0,
1136                  "max": 2
1137                }
1138                "#
1139                );
1140            },
1141        );
1142    }
1143
1144    #[test]
1145    fn python_elif_function() {
1146        // Boolean expressions containing `And` and `Or` operators were not
1147        // considered in `elif` statements
1148        check_metrics::<PythonParser>(
1149            "def f(a, b):
1150                if a and b:  # +2 (+1 and)
1151                   return 1
1152                elif c and d: # +2 (+1 and)
1153                   return 1",
1154            "foo.py",
1155            |metric| {
1156                insta::assert_json_snapshot!(
1157                    metric.cognitive,
1158                    @r#"
1159                {
1160                  "sum": 4,
1161                  "value": 0,
1162                  "average": 4.0,
1163                  "min": 0,
1164                  "max": 4
1165                }
1166                "#
1167                );
1168            },
1169        );
1170    }
1171
1172    #[test]
1173    fn python_more_elifs_function() {
1174        // Boolean expressions containing `And` and `Or` operators were not
1175        // considered when there were more `elif` statements
1176        check_metrics::<PythonParser>(
1177            "def f(a, b):
1178                if a and b:  # +2 (+1 and)
1179                   return 1
1180                elif c and d: # +2 (+1 and)
1181                   return 1
1182                elif e and f: # +2 (+1 and)
1183                   return 1",
1184            "foo.py",
1185            |metric| {
1186                insta::assert_json_snapshot!(
1187                    metric.cognitive,
1188                    @r#"
1189                {
1190                  "sum": 6,
1191                  "value": 0,
1192                  "average": 6.0,
1193                  "min": 0,
1194                  "max": 6
1195                }
1196                "#
1197                );
1198            },
1199        );
1200    }
1201
1202    #[test]
1203    fn python_if_elif_elif_else_chain() {
1204        // Regression for #274: `if/elif/elif/else` must score as a flat
1205        // branch chain (each continuation contributes +1 with no extra
1206        // nesting). `ElifClause` is a dedicated node handled directly
1207        // by the cognitive dispatch as a branch extension, and the
1208        // generic `count_specific_ancestors` nesting walk does not
1209        // include `ElifClause` in its kind sets, so no ancestor-side
1210        // suppression via `is_else_if` is required.
1211        // expected: outer if +1, elif +1, elif +1, else +1 = 4.
1212        check_metrics::<PythonParser>(
1213            "def f(a, b, c, d):
1214                if a:
1215                   return 1
1216                elif b:
1217                   return 2
1218                elif c:
1219                   return 3
1220                else:
1221                   return 4",
1222            "foo.py",
1223            |metric| {
1224                assert_eq!(metric.cognitive.cognitive_sum(), 4);
1225                insta::assert_json_snapshot!(
1226                    metric.cognitive,
1227                    @r#"
1228                {
1229                  "sum": 4,
1230                  "value": 0,
1231                  "average": 4.0,
1232                  "min": 0,
1233                  "max": 4
1234                }
1235                "#
1236                );
1237            },
1238        );
1239    }
1240
1241    #[test]
1242    fn python_else_if_chain_matches_elif() {
1243        // Regression for #276: `else: if x:` (no `elif`) is semantically
1244        // an else-if chain and must score the same as the `elif`
1245        // equivalent. Before the fix, the inner `if_statement` was
1246        // double-counted (nesting +2 instead of +1), inflating the
1247        // cognitive score linearly with chain length.
1248        // expected: outer if +1, boolean `and` +1, else_clause +1,
1249        //   inner if suppressed by is_else_if, inner boolean `and` +1
1250        //   = 4 — matching the `elif` form above (python_elif_function).
1251        check_metrics::<PythonParser>(
1252            "def f(a, b, c, d):
1253                if a and b:
1254                   return 1
1255                else:
1256                   if c and d:
1257                      return 1",
1258            "foo.py",
1259            |metric| {
1260                assert_eq!(metric.cognitive.cognitive_sum(), 4);
1261                insta::assert_json_snapshot!(
1262                    metric.cognitive,
1263                    @r#"
1264                {
1265                  "sum": 4,
1266                  "value": 0,
1267                  "average": 4.0,
1268                  "min": 0,
1269                  "max": 4
1270                }
1271                "#
1272                );
1273            },
1274        );
1275    }
1276
1277    #[test]
1278    fn python_try_except_finally_finally_is_free() {
1279        // Regression for #416: a `finally` clause is structured cleanup that
1280        // always runs and must add 0 per the SonarSource Cognitive Complexity
1281        // spec. try/except/finally must score the same as try/except.
1282        // expected: except +1, finally +0 = 1.
1283        check_metrics::<PythonParser>(
1284            "def f():
1285                try:
1286                    x = risky()
1287                except ValueError:
1288                    x = 1
1289                finally:
1290                    cleanup()
1291                return x",
1292            "foo.py",
1293            |metric| {
1294                assert_eq!(metric.cognitive.cognitive_sum(), 1);
1295                insta::assert_json_snapshot!(
1296                    metric.cognitive,
1297                    @r#"
1298                {
1299                  "sum": 1,
1300                  "value": 0,
1301                  "average": 1.0,
1302                  "min": 0,
1303                  "max": 1
1304                }
1305                "#
1306                );
1307            },
1308        );
1309    }
1310
1311    #[test]
1312    fn python_try_except_matches_try_except_finally() {
1313        // Companion to #416: try/except (no finally) scores the same as the
1314        // try/except/finally form above, proving `finally` is free.
1315        // expected: except +1 = 1.
1316        check_metrics::<PythonParser>(
1317            "def f():
1318                try:
1319                    x = risky()
1320                except ValueError:
1321                    x = 1
1322                return x",
1323            "foo.py",
1324            |metric| {
1325                assert_eq!(metric.cognitive.cognitive_sum(), 1);
1326                insta::assert_json_snapshot!(
1327                    metric.cognitive,
1328                    @r#"
1329                {
1330                  "sum": 1,
1331                  "value": 0,
1332                  "average": 1.0,
1333                  "min": 0,
1334                  "max": 1
1335                }
1336                "#
1337                );
1338            },
1339        );
1340    }
1341
1342    #[test]
1343    fn python_comprehension_matches_explicit_loop() {
1344        // Regression for #417: a list comprehension's `for`/`if` clauses must
1345        // carry the same cognitive load as the explicit loop+condition they
1346        // desugar to. `[x for x in xs if x > 0]` was scoring 0 while the
1347        // equivalent explicit `for`/`if` scored 3.
1348        // expected: for_in_clause +1 (nesting 0), if_clause +2 (1 base +
1349        // 1 nesting under the for) = 3 — equal to the explicit form below.
1350        check_cognitive_and_cyclomatic::<PythonParser>(
1351            "def f(xs):
1352                return [x for x in xs if x > 0]",
1353            "foo.py",
1354            |metric| {
1355                // cyclomatic 4 = unit base 1 + for 1 + if 1 + function base 1.
1356                assert_eq!(metric.cognitive.cognitive_sum(), 3);
1357                assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
1358            },
1359        );
1360        check_cognitive_and_cyclomatic::<PythonParser>(
1361            "def g(xs):
1362                out = []
1363                for x in xs:
1364                    if x > 0:
1365                        out.append(x)
1366                return out",
1367            "foo.py",
1368            |metric| {
1369                // The explicit loop+if form the comprehension above desugars
1370                // to: for +1, nested if +2 = 3 (cognitive), matching f.
1371                // cyclomatic 4 matches f as well, confirming agreement.
1372                assert_eq!(metric.cognitive.cognitive_sum(), 3);
1373                assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
1374            },
1375        );
1376    }
1377
1378    #[test]
1379    fn python_comprehension_plain_no_filter() {
1380        // A comprehension with no `if` filter scores just the loop.
1381        // expected: for_in_clause +1 = 1.
1382        check_cognitive_and_cyclomatic::<PythonParser>(
1383            "def f(xs):
1384                return [x for x in xs]",
1385            "foo.py",
1386            |metric| {
1387                assert_eq!(metric.cognitive.cognitive_sum(), 1);
1388                // cyclomatic 3 = unit base 1 + for 1 + function base 1.
1389                assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
1390            },
1391        );
1392    }
1393
1394    #[test]
1395    fn python_comprehension_nested_for() {
1396        // Two `for` clauses are nested loops: the second nests under the
1397        // first, mirroring explicit nested `for` statements.
1398        // expected: for #1 +1 (nesting 0), for #2 +2 (1 base + 1 nesting) = 3.
1399        check_cognitive_and_cyclomatic::<PythonParser>(
1400            "def f(xs, ys):
1401                return [a for a in xs for b in ys]",
1402            "foo.py",
1403            |metric| {
1404                assert_eq!(metric.cognitive.cognitive_sum(), 3);
1405                // cyclomatic 4 = unit base 1 + for 1 + for 1 + function base 1.
1406                assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
1407            },
1408        );
1409    }
1410
1411    #[test]
1412    fn python_comprehension_multiple_filters() {
1413        // Each `if` filter is an independent condition nested under the for.
1414        // Cognitive penalizes the nesting, so it exceeds cyclomatic here; the
1415        // two metrics legitimately diverge once filters multiply.
1416        // expected cognitive: for +1, if #1 +2, if #2 +2 = 5.
1417        check_cognitive_and_cyclomatic::<PythonParser>(
1418            "def f(xs):
1419                return [x for x in xs if a if b]",
1420            "foo.py",
1421            |metric| {
1422                assert_eq!(metric.cognitive.cognitive_sum(), 5);
1423                // cyclomatic 5 = unit base 1 + for 1 + if 1 + if 1 + fn base 1.
1424                assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
1425            },
1426        );
1427    }
1428
1429    #[test]
1430    fn python_comprehension_variants_consistent() {
1431        // dict / set / generator comprehensions reuse the same for_in_clause /
1432        // if_clause node kinds as the list form, so all must score identically
1433        // to `[x for x in xs if x > 0]` (cognitive 3).
1434        // expected: for +1, if +2 = 3 for each variant.
1435        for body in [
1436            "{x: y for x, y in xs if x > 0}",
1437            "{x for x in xs if x > 0}",
1438            "(x for x in xs if x > 0)",
1439        ] {
1440            check_cognitive_and_cyclomatic::<PythonParser>(
1441                &format!("def f(xs):\n                return {body}"),
1442                "foo.py",
1443                |metric| {
1444                    assert_eq!(metric.cognitive.cognitive_sum(), 3);
1445                    // cyclomatic 4 = unit base 1 + for 1 + if 1 + fn base 1,
1446                    // identical to the list form, for every variant.
1447                    assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
1448                },
1449            );
1450        }
1451    }
1452
1453    #[test]
1454    fn python_comprehension_nested_in_element() {
1455        // Regression for #421: a comprehension in another comprehension's
1456        // element position must carry the full nesting of the outer loop+
1457        // filter, not the shallow depth the #417 sibling write-back left it
1458        // with (it under-counted at 6). The element is traversed before the
1459        // outer clauses, so the depth is established on the comprehension node
1460        // itself, independent of sibling traversal order.
1461        // expected cognitive: outer for +1 (nesting 0), outer if +2
1462        // (nesting 1), inner for +3 (nesting 2), inner if +4 (nesting 3) = 10.
1463        check_metrics::<PythonParser>(
1464            "def f(xs):
1465                return [[y for y in x if y] for x in xs if x]",
1466            "foo.py",
1467            |metric| {
1468                assert_eq!(metric.cognitive.cognitive_sum(), 10);
1469            },
1470        );
1471        // The explicit doubly-nested loop+if form it desugars to: for +1,
1472        // if +2, for +3, if +4 = 10, matching the comprehension above.
1473        check_metrics::<PythonParser>(
1474            "def g(xs):
1475                out = []
1476                for x in xs:
1477                    if x:
1478                        for y in x:
1479                            if y:
1480                                out.append(y)
1481                return out",
1482            "foo.py",
1483            |metric| {
1484                assert_eq!(metric.cognitive.cognitive_sum(), 10);
1485            },
1486        );
1487    }
1488
1489    #[test]
1490    fn python_comprehension_three_levels_nested() {
1491        // Three comprehensions nested through each other's element positions
1492        // must equal their explicit triply-nested loop+if form at every depth.
1493        // expected cognitive: for/if pairs at nesting 0..5 =
1494        // 1+2+3+4+5+6 = 21.
1495        check_metrics::<PythonParser>(
1496            "def f(xss):
1497                return [[[z for z in y if z] for y in x if y] for x in xss if x]",
1498            "foo.py",
1499            |metric| {
1500                assert_eq!(metric.cognitive.cognitive_sum(), 21);
1501            },
1502        );
1503        check_metrics::<PythonParser>(
1504            "def g(xss):
1505                out = []
1506                for x in xss:
1507                    if x:
1508                        for y in x:
1509                            if y:
1510                                for z in y:
1511                                    if z:
1512                                        out.append(z)
1513                return out",
1514            "foo.py",
1515            |metric| {
1516                assert_eq!(metric.cognitive.cognitive_sum(), 21);
1517            },
1518        );
1519    }
1520
1521    #[test]
1522    fn python_generator_in_comprehension_element() {
1523        // #421 edge case: a generator passed to a call (`sum(...)`) in a
1524        // comprehension's element still inherits the outer loop+filter depth
1525        // through the intervening call/argument_list nodes.
1526        // expected cognitive: outer for +1, outer if +2, inner for +3,
1527        // inner if +4 = 10.
1528        check_metrics::<PythonParser>(
1529            "def f(xs):
1530                return [sum(y for y in x if y) for x in xs if x]",
1531            "foo.py",
1532            |metric| {
1533                assert_eq!(metric.cognitive.cognitive_sum(), 10);
1534            },
1535        );
1536        check_metrics::<PythonParser>(
1537            "def g(xs):
1538                out = []
1539                for x in xs:
1540                    if x:
1541                        out.append(sum(y for y in x if y))
1542                return out",
1543            "foo.py",
1544            |metric| {
1545                assert_eq!(metric.cognitive.cognitive_sum(), 10);
1546            },
1547        );
1548    }
1549
1550    #[test]
1551    fn python_try_finally_no_except_is_free() {
1552        // #416: try/finally with no except clause scores 0 — neither the try
1553        // body nor the finally cleanup carries any cognitive cost on its own.
1554        // expected: 0.
1555        check_metrics::<PythonParser>(
1556            "def f():
1557                try:
1558                    x = risky()
1559                finally:
1560                    cleanup()
1561                return x",
1562            "foo.py",
1563            |metric| {
1564                assert_eq!(metric.cognitive.cognitive_sum(), 0);
1565                insta::assert_json_snapshot!(
1566                    metric.cognitive,
1567                    @r#"
1568                {
1569                  "sum": 0,
1570                  "value": 0,
1571                  "average": 0.0,
1572                  "min": 0,
1573                  "max": 0
1574                }
1575                "#
1576                );
1577            },
1578        );
1579    }
1580
1581    #[test]
1582    fn python_constructs_inside_finally_still_count() {
1583        // #416 guard: making `finally` free must not make its body invisible.
1584        // The finally clause itself carries no nesting increment (it never
1585        // called `increase_nesting`), so an `if` directly inside it is at
1586        // nesting depth 0 and contributes its +1 base cost.
1587        // expected: if inside finally = +1.
1588        check_metrics::<PythonParser>(
1589            "def f():
1590                try:
1591                    x = risky()
1592                finally:
1593                    if x:
1594                        cleanup()",
1595            "foo.py",
1596            |metric| {
1597                assert_eq!(metric.cognitive.cognitive_sum(), 1);
1598                insta::assert_json_snapshot!(
1599                    metric.cognitive,
1600                    @r#"
1601                {
1602                  "sum": 1,
1603                  "value": 0,
1604                  "average": 1.0,
1605                  "min": 0,
1606                  "max": 1
1607                }
1608                "#
1609                );
1610            },
1611        );
1612    }
1613
1614    #[test]
1615    fn rust_simple_function() {
1616        check_metrics::<RustParser>(
1617            "fn f() {
1618                 if a && b { // +2 (+1 &&)
1619                     println!(\"test\");
1620                 }
1621                 if c && d { // +2 (+1 &&)
1622                     println!(\"test\");
1623                 }
1624             }",
1625            "foo.rs",
1626            |metric| {
1627                insta::assert_json_snapshot!(
1628                    metric.cognitive,
1629                    @r#"
1630                {
1631                  "sum": 4,
1632                  "value": 0,
1633                  "average": 4.0,
1634                  "min": 0,
1635                  "max": 4
1636                }
1637                "#
1638                );
1639            },
1640        );
1641    }
1642
1643    #[test]
1644    fn c_simple_function() {
1645        check_metrics::<CParser>(
1646            "void f() {
1647                 if (a && b) { // +2 (+1 &&)
1648                     printf(\"test\");
1649                 }
1650                 if (c && d) { // +2 (+1 &&)
1651                     printf(\"test\");
1652                 }
1653             }",
1654            "foo.c",
1655            |metric| {
1656                insta::assert_json_snapshot!(
1657                    metric.cognitive,
1658                    @r#"
1659                {
1660                  "sum": 4,
1661                  "value": 0,
1662                  "average": 4.0,
1663                  "min": 0,
1664                  "max": 4
1665                }
1666                "#
1667                );
1668            },
1669        );
1670    }
1671
1672    #[test]
1673    fn mozjs_simple_function() {
1674        check_metrics::<MozjsParser>(
1675            "function f() {
1676                 if (a && b) { // +2 (+1 &&)
1677                     window.print(\"test\");
1678                 }
1679                 if (c && d) { // +2 (+1 &&)
1680                     window.print(\"test\");
1681                 }
1682             }",
1683            "foo.js",
1684            |metric| {
1685                insta::assert_json_snapshot!(
1686                    metric.cognitive,
1687                    @r#"
1688                {
1689                  "sum": 4,
1690                  "value": 0,
1691                  "average": 4.0,
1692                  "min": 0,
1693                  "max": 4
1694                }
1695                "#
1696                );
1697            },
1698        );
1699    }
1700
1701    #[test]
1702    fn javascript_simple_function() {
1703        check_metrics::<JavascriptParser>(
1704            "function f() {
1705                 if (a && b) { // +2 (+1 &&)
1706                     console.log(\"test\");
1707                 }
1708                 if (c || d) { // +2 (+1 ||)
1709                     console.log(\"test\");
1710                 }
1711             }",
1712            "foo.js",
1713            |metric| {
1714                insta::assert_json_snapshot!(
1715                    metric.cognitive,
1716                    @r#"
1717                {
1718                  "sum": 4,
1719                  "value": 0,
1720                  "average": 4.0,
1721                  "min": 0,
1722                  "max": 4
1723                }
1724                "#
1725                );
1726            },
1727        );
1728    }
1729
1730    #[test]
1731    fn python_sequence_same_booleans() {
1732        check_metrics::<PythonParser>(
1733            "def f(a, b):
1734                if a and b and True:  # +2 (+1 sequence of and)
1735                   return 1",
1736            "foo.py",
1737            |metric| {
1738                insta::assert_json_snapshot!(
1739                    metric.cognitive,
1740                    @r#"
1741                {
1742                  "sum": 2,
1743                  "value": 0,
1744                  "average": 2.0,
1745                  "min": 0,
1746                  "max": 2
1747                }
1748                "#
1749                );
1750            },
1751        );
1752    }
1753
1754    #[test]
1755    fn rust_sequence_same_booleans() {
1756        check_metrics::<RustParser>(
1757            "fn f() {
1758                 if a && b && true { // +2 (+1 sequence of &&)
1759                     println!(\"test\");
1760                 }
1761             }",
1762            "foo.rs",
1763            |metric| {
1764                insta::assert_json_snapshot!(
1765                    metric.cognitive,
1766                    @r#"
1767                {
1768                  "sum": 2,
1769                  "value": 0,
1770                  "average": 2.0,
1771                  "min": 0,
1772                  "max": 2
1773                }
1774                "#
1775                );
1776            },
1777        );
1778
1779        check_metrics::<RustParser>(
1780            "fn f() {
1781                 if a || b || c || d { // +2 (+1 sequence of ||)
1782                     println!(\"test\");
1783                 }
1784             }",
1785            "foo.rs",
1786            |metric| {
1787                insta::assert_json_snapshot!(
1788                    metric.cognitive,
1789                    @r#"
1790                {
1791                  "sum": 2,
1792                  "value": 0,
1793                  "average": 2.0,
1794                  "min": 0,
1795                  "max": 2
1796                }
1797                "#
1798                );
1799            },
1800        );
1801    }
1802
1803    // Regression for issue #396: in Rust 2024 let-chains, the `&&`
1804    // tokens are direct children of the `_let_chain` / `let_chain`
1805    // node rather than a `BinaryExpression`. Before #396 these
1806    // tokens were invisible to the cognitive boolean-sequence
1807    // counter (cyclomatic already counted them via AMPAMP).
1808    #[test]
1809    fn rust_let_chain_sequence_booleans() {
1810        // expected: +1 for the `if`, +1 for the chain of two `&&`
1811        // tokens (sequence of same operator collapses to one).
1812        // Equivalent shape to `if a && b && true { ... }` above,
1813        // which scores 2.0.
1814        check_metrics::<RustParser>(
1815            "fn f(a: Option<i32>, b: Option<i32>) {
1816                 if let Some(x) = a && let Some(y) = b && x > y { // +2 (+1 sequence of &&)
1817                     println!(\"both\");
1818                 }
1819             }",
1820            "foo.rs",
1821            |metric| {
1822                assert_eq!(metric.cognitive.cognitive_sum() as u32, 2);
1823                insta::assert_json_snapshot!(
1824                    metric.cognitive,
1825                    @r#"
1826                {
1827                  "sum": 2,
1828                  "value": 0,
1829                  "average": 2.0,
1830                  "min": 0,
1831                  "max": 2
1832                }
1833                "#
1834                );
1835            },
1836        );
1837    }
1838
1839    #[test]
1840    fn rust_let_chain_vs_nested_if_let() {
1841        // Companion to `rust_let_chain_sequence_booleans`. The nested
1842        // `if let` form has no `&&` and so is unaffected by the #396
1843        // LetChain dispatch; this test pins that the pre-existing
1844        // nesting scoring (+1 outer `if`, +2 nested `if` at nesting=1)
1845        // still yields 3 and that the LetChain arm did not alter it.
1846        check_metrics::<RustParser>(
1847            "fn f(a: Option<i32>, b: Option<i32>) {
1848                 if let Some(x) = a { // +1
1849                     if let Some(y) = b { // +2 (nesting=1)
1850                         println!(\"{} {}\", x, y);
1851                     }
1852                 }
1853             }",
1854            "foo.rs",
1855            |metric| {
1856                assert_eq!(metric.cognitive.cognitive_sum() as u32, 3);
1857                insta::assert_json_snapshot!(
1858                    metric.cognitive,
1859                    @r#"
1860                {
1861                  "sum": 3,
1862                  "value": 0,
1863                  "average": 3.0,
1864                  "min": 0,
1865                  "max": 3
1866                }
1867                "#
1868                );
1869            },
1870        );
1871    }
1872
1873    #[test]
1874    fn c_sequence_same_booleans() {
1875        check_metrics::<CParser>(
1876            "void f() {
1877                 if (a && b && 1 == 1) { // +2 (+1 sequence of &&)
1878                     printf(\"test\");
1879                 }
1880             }",
1881            "foo.c",
1882            |metric| {
1883                insta::assert_json_snapshot!(
1884                    metric.cognitive,
1885                    @r#"
1886                {
1887                  "sum": 2,
1888                  "value": 0,
1889                  "average": 2.0,
1890                  "min": 0,
1891                  "max": 2
1892                }
1893                "#
1894                );
1895            },
1896        );
1897
1898        check_metrics::<CppParser>(
1899            "void f() {
1900                 if (a || b || c || d) { // +2 (+1 sequence of ||)
1901                     printf(\"test\");
1902                 }
1903             }",
1904            "foo.c",
1905            |metric| {
1906                insta::assert_json_snapshot!(
1907                    metric.cognitive,
1908                    @r#"
1909                {
1910                  "sum": 2,
1911                  "value": 0,
1912                  "average": 2.0,
1913                  "min": 0,
1914                  "max": 2
1915                }
1916                "#
1917                );
1918            },
1919        );
1920    }
1921
1922    #[test]
1923    fn mozjs_sequence_same_booleans() {
1924        check_metrics::<MozjsParser>(
1925            "function f() {
1926                 if (a && b && 1 == 1) { // +2 (+1 sequence of &&)
1927                     window.print(\"test\");
1928                 }
1929             }",
1930            "foo.js",
1931            |metric| {
1932                insta::assert_json_snapshot!(
1933                    metric.cognitive,
1934                    @r#"
1935                {
1936                  "sum": 2,
1937                  "value": 0,
1938                  "average": 2.0,
1939                  "min": 0,
1940                  "max": 2
1941                }
1942                "#
1943                );
1944            },
1945        );
1946
1947        check_metrics::<MozjsParser>(
1948            "function f() {
1949                 if (a || b || c || d) { // +2 (+1 sequence of ||)
1950                     window.print(\"test\");
1951                 }
1952             }",
1953            "foo.js",
1954            |metric| {
1955                insta::assert_json_snapshot!(
1956                    metric.cognitive,
1957                    @r#"
1958                {
1959                  "sum": 2,
1960                  "value": 0,
1961                  "average": 2.0,
1962                  "min": 0,
1963                  "max": 2
1964                }
1965                "#
1966                );
1967            },
1968        );
1969    }
1970
1971    #[test]
1972    fn rust_not_booleans() {
1973        check_metrics::<RustParser>(
1974            "fn f() {
1975                 if !a && !b { // +2 (+1 &&)
1976                     println!(\"test\");
1977                 }
1978             }",
1979            "foo.rs",
1980            |metric| {
1981                insta::assert_json_snapshot!(
1982                    metric.cognitive,
1983                    @r#"
1984                {
1985                  "sum": 2,
1986                  "value": 0,
1987                  "average": 2.0,
1988                  "min": 0,
1989                  "max": 2
1990                }
1991                "#
1992                );
1993            },
1994        );
1995
1996        check_metrics::<RustParser>(
1997            // `!` does not break boolean sequences (issue #392): the
1998            // outer and inner `&&`s are folded into a single sequence
1999            // because pre-order visits the outer BinaryExpression first
2000            // (recording `&&` at its end_byte) and the inner `&&` lies
2001            // within that span. The `!` arm was dead anyway — it fired
2002            // after both BinaryExpressions had already been counted.
2003            "fn f() {
2004                 if a && !(b && c) { // +2 (+1 if, +1 outer &&; inner && continues)
2005                     println!(\"test\");
2006                 }
2007             }",
2008            "foo.rs",
2009            |metric| {
2010                insta::assert_json_snapshot!(
2011                    metric.cognitive,
2012                    @r#"
2013                {
2014                  "sum": 2,
2015                  "value": 0,
2016                  "average": 2.0,
2017                  "min": 0,
2018                  "max": 2
2019                }
2020                "#
2021                );
2022            },
2023        );
2024
2025        check_metrics::<RustParser>(
2026            "fn f() {
2027                 if !(a || b) && !(c || d) { // +4 (+1 ||, +1 &&, +1 ||)
2028                     println!(\"test\");
2029                 }
2030             }",
2031            "foo.rs",
2032            |metric| {
2033                insta::assert_json_snapshot!(
2034                    metric.cognitive,
2035                    @r#"
2036                {
2037                  "sum": 4,
2038                  "value": 0,
2039                  "average": 4.0,
2040                  "min": 0,
2041                  "max": 4
2042                }
2043                "#
2044                );
2045            },
2046        );
2047    }
2048
2049    #[test]
2050    fn rust_not_does_not_affect_boolean_sequence_392() {
2051        // Regression test for issue #392: `!` does not affect cognitive
2052        // scoring for a same-operator boolean sequence. `!a && !b && !c`
2053        // must score identically to `a && b && c` — both are a single
2054        // `&&` chain under SonarSource's rule B1 (only operator switches
2055        // start a new sequence). The previously dead `UnaryExpression`
2056        // arm could not have affected this case either way (pre-order
2057        // visits the BinaryExpressions before the UnaryExpressions), so
2058        // this asserts the new and old behaviour agree where it matters.
2059        // if(+1) + && sequence(+1) = 2; the two trailing `&&`s are
2060        // continuations because all three share the outer pre-order
2061        // parent's end_byte.
2062        check_metrics::<RustParser>(
2063            "fn f() {
2064                 if !a && !b && !c {
2065                     println!(\"test\");
2066                 }
2067             }",
2068            "foo.rs",
2069            |metric| {
2070                assert_eq!(metric.cognitive.cognitive_sum(), 2);
2071                insta::assert_json_snapshot!(
2072                    metric.cognitive,
2073                    @r#"
2074                {
2075                  "sum": 2,
2076                  "value": 0,
2077                  "average": 2.0,
2078                  "min": 0,
2079                  "max": 2
2080                }
2081                "#
2082                );
2083            },
2084        );
2085        check_metrics::<RustParser>(
2086            "fn f() {
2087                 if a && b && c {
2088                     println!(\"test\");
2089                 }
2090             }",
2091            "foo.rs",
2092            |metric| {
2093                // Same sum as the negated form above: `!` is not a
2094                // boolean-sequence boundary.
2095                assert_eq!(metric.cognitive.cognitive_sum(), 2);
2096                insta::assert_json_snapshot!(
2097                    metric.cognitive,
2098                    @r#"
2099                {
2100                  "sum": 2,
2101                  "value": 0,
2102                  "average": 2.0,
2103                  "min": 0,
2104                  "max": 2
2105                }
2106                "#
2107                );
2108            },
2109        );
2110    }
2111
2112    #[test]
2113    fn c_not_booleans() {
2114        // `!` does not break boolean sequences (issue #392): the inner
2115        // `&&` is folded into the outer `&&`'s span because pre-order
2116        // visits the outer `binary_expression` first.
2117        check_metrics::<CParser>(
2118            "void f() {
2119                 if (a && !(b && c)) { // +2 (+1 if, +1 outer &&; inner && continues)
2120                     printf(\"test\");
2121                 }
2122             }",
2123            "foo.c",
2124            |metric| {
2125                insta::assert_json_snapshot!(
2126                    metric.cognitive,
2127                    @r#"
2128                {
2129                  "sum": 2,
2130                  "value": 0,
2131                  "average": 2.0,
2132                  "min": 0,
2133                  "max": 2
2134                }
2135                "#
2136                );
2137            },
2138        );
2139
2140        check_metrics::<CppParser>(
2141            "void f() {
2142                 if (!(a || b) && !(c || d)) { // +4 (+1 ||, +1 &&, +1 ||)
2143                     printf(\"test\");
2144                 }
2145             }",
2146            "foo.c",
2147            |metric| {
2148                insta::assert_json_snapshot!(
2149                    metric.cognitive,
2150                    @r#"
2151                {
2152                  "sum": 4,
2153                  "value": 0,
2154                  "average": 4.0,
2155                  "min": 0,
2156                  "max": 4
2157                }
2158                "#
2159                );
2160            },
2161        );
2162    }
2163
2164    #[test]
2165    fn mozjs_not_booleans() {
2166        // `!` does not break boolean sequences (issue #392): inner `&&`
2167        // continues the outer `&&` sequence (pre-order visits the outer
2168        // BinaryExpression first, so its end_byte already covers the
2169        // inner one).
2170        check_metrics::<MozjsParser>(
2171            "function f() {
2172                 if (a && !(b && c)) { // +2 (+1 if, +1 outer &&; inner && continues)
2173                     window.print(\"test\");
2174                 }
2175             }",
2176            "foo.js",
2177            |metric| {
2178                insta::assert_json_snapshot!(
2179                    metric.cognitive,
2180                    @r#"
2181                {
2182                  "sum": 2,
2183                  "value": 0,
2184                  "average": 2.0,
2185                  "min": 0,
2186                  "max": 2
2187                }
2188                "#
2189                );
2190            },
2191        );
2192
2193        check_metrics::<MozjsParser>(
2194            "function f() {
2195                 if (!(a || b) && !(c || d)) { // +4 (+1 ||, +1 &&, +1 ||)
2196                     window.print(\"test\");
2197                 }
2198             }",
2199            "foo.js",
2200            |metric| {
2201                insta::assert_json_snapshot!(
2202                    metric.cognitive,
2203                    @r#"
2204                {
2205                  "sum": 4,
2206                  "value": 0,
2207                  "average": 4.0,
2208                  "min": 0,
2209                  "max": 4
2210                }
2211                "#
2212                );
2213            },
2214        );
2215    }
2216
2217    #[test]
2218    fn python_sequence_different_booleans() {
2219        check_metrics::<PythonParser>(
2220            "def f(a, b):
2221                if a and b or True:  # +3 (+1 and, +1 or)
2222                   return 1",
2223            "foo.py",
2224            |metric| {
2225                insta::assert_json_snapshot!(
2226                    metric.cognitive,
2227                    @r#"
2228                {
2229                  "sum": 3,
2230                  "value": 0,
2231                  "average": 3.0,
2232                  "min": 0,
2233                  "max": 3
2234                }
2235                "#
2236                );
2237            },
2238        );
2239    }
2240
2241    #[test]
2242    fn rust_sequence_different_booleans() {
2243        check_metrics::<RustParser>(
2244            "fn f() {
2245                 if a && b || true { // +3 (+1 &&, +1 ||)
2246                     println!(\"test\");
2247                 }
2248             }",
2249            "foo.rs",
2250            |metric| {
2251                insta::assert_json_snapshot!(
2252                    metric.cognitive,
2253                    @r#"
2254                {
2255                  "sum": 3,
2256                  "value": 0,
2257                  "average": 3.0,
2258                  "min": 0,
2259                  "max": 3
2260                }
2261                "#
2262                );
2263            },
2264        );
2265    }
2266
2267    #[test]
2268    fn c_sequence_different_booleans() {
2269        check_metrics::<CParser>(
2270            "void f() {
2271                 if (a && b || 1 == 1) { // +3 (+1 &&, +1 ||)
2272                     printf(\"test\");
2273                 }
2274             }",
2275            "foo.c",
2276            |metric| {
2277                insta::assert_json_snapshot!(
2278                    metric.cognitive,
2279                    @r#"
2280                {
2281                  "sum": 3,
2282                  "value": 0,
2283                  "average": 3.0,
2284                  "min": 0,
2285                  "max": 3
2286                }
2287                "#
2288                );
2289            },
2290        );
2291    }
2292
2293    #[test]
2294    fn mozjs_sequence_different_booleans() {
2295        check_metrics::<MozjsParser>(
2296            "function f() {
2297                 if (a && b || 1 == 1) { // +3 (+1 &&, +1 ||)
2298                     window.print(\"test\");
2299                 }
2300             }",
2301            "foo.js",
2302            |metric| {
2303                insta::assert_json_snapshot!(
2304                    metric.cognitive,
2305                    @r#"
2306                {
2307                  "sum": 3,
2308                  "value": 0,
2309                  "average": 3.0,
2310                  "min": 0,
2311                  "max": 3
2312                }
2313                "#
2314                );
2315            },
2316        );
2317    }
2318
2319    #[test]
2320    fn python_formatted_sequence_different_booleans() {
2321        check_metrics::<PythonParser>(
2322            "def f(a, b):
2323                if (  # +1
2324                    a and b and  # +1
2325                    (c or d)  # +1
2326                ):
2327                   return 1",
2328            "foo.py",
2329            |metric| {
2330                insta::assert_json_snapshot!(
2331                    metric.cognitive,
2332                    @r#"
2333                {
2334                  "sum": 3,
2335                  "value": 0,
2336                  "average": 3.0,
2337                  "min": 0,
2338                  "max": 3
2339                }
2340                "#
2341                );
2342            },
2343        );
2344    }
2345
2346    #[test]
2347    fn python_1_level_nesting() {
2348        check_metrics::<PythonParser>(
2349            "def f(a, b):
2350                if a:  # +1
2351                    for i in range(b):  # +2
2352                        return 1",
2353            "foo.py",
2354            |metric| {
2355                insta::assert_json_snapshot!(
2356                    metric.cognitive,
2357                    @r#"
2358                {
2359                  "sum": 3,
2360                  "value": 0,
2361                  "average": 3.0,
2362                  "min": 0,
2363                  "max": 3
2364                }
2365                "#
2366                );
2367            },
2368        );
2369    }
2370
2371    #[test]
2372    fn rust_1_level_nesting() {
2373        check_metrics::<RustParser>(
2374            "fn f() {
2375                 if true { // +1
2376                     if true { // +2 (nesting = 1)
2377                         println!(\"test\");
2378                     } else if 1 == 1 { // +1
2379                         if true { // +3 (nesting = 2)
2380                             println!(\"test\");
2381                         }
2382                     } else { // +1
2383                         if true { // +3 (nesting = 2)
2384                             println!(\"test\");
2385                         }
2386                     }
2387                 }
2388             }",
2389            "foo.rs",
2390            |metric| {
2391                insta::assert_json_snapshot!(
2392                    metric.cognitive,
2393                    @r#"
2394                {
2395                  "sum": 11,
2396                  "value": 0,
2397                  "average": 11.0,
2398                  "min": 0,
2399                  "max": 11
2400                }
2401                "#
2402                );
2403            },
2404        );
2405
2406        check_metrics::<RustParser>(
2407            "fn f() {
2408                 if true { // +1
2409                     match true { // +2 (nesting = 1)
2410                         true => println!(\"test\"),
2411                         false => println!(\"test\"),
2412                     }
2413                 }
2414             }",
2415            "foo.rs",
2416            |metric| {
2417                insta::assert_json_snapshot!(
2418                    metric.cognitive,
2419                    @r#"
2420                {
2421                  "sum": 3,
2422                  "value": 0,
2423                  "average": 3.0,
2424                  "min": 0,
2425                  "max": 3
2426                }
2427                "#
2428                );
2429            },
2430        );
2431    }
2432
2433    #[test]
2434    fn c_1_level_nesting() {
2435        check_metrics::<CParser>(
2436            "void f() {
2437                 if (1 == 1) { // +1
2438                     if (1 == 1) { // +2 (nesting = 1)
2439                         printf(\"test\");
2440                     } else if (1 == 1) { // +1
2441                         if (1 == 1) { // +3 (nesting = 2)
2442                             printf(\"test\");
2443                         }
2444                     } else { // +1
2445                         if (1 == 1) { // +3 (nesting = 2)
2446                             printf(\"test\");
2447                         }
2448                     }
2449                 }
2450             }",
2451            "foo.c",
2452            |metric| {
2453                insta::assert_json_snapshot!(
2454                    metric.cognitive,
2455                    @r#"
2456                {
2457                  "sum": 11,
2458                  "value": 0,
2459                  "average": 11.0,
2460                  "min": 0,
2461                  "max": 11
2462                }
2463                "#
2464                );
2465            },
2466        );
2467    }
2468
2469    #[test]
2470    fn mozjs_1_level_nesting() {
2471        check_metrics::<MozjsParser>(
2472            "function f() {
2473                 if (1 == 1) { // +1
2474                     if (1 == 1) { // +2 (nesting = 1)
2475                         window.print(\"test\");
2476                     } else if (1 == 1) { // +1
2477                         if (1 == 1) { // +3 (nesting = 2)
2478                             window.print(\"test\");
2479                         }
2480                     } else { // +1
2481                         if (1 == 1) { // +3 (nesting = 2)
2482                             window.print(\"test\");
2483                         }
2484                     }
2485                 }
2486             }",
2487            "foo.js",
2488            |metric| {
2489                insta::assert_json_snapshot!(
2490                    metric.cognitive,
2491                    @r#"
2492                {
2493                  "sum": 11,
2494                  "value": 0,
2495                  "average": 11.0,
2496                  "min": 0,
2497                  "max": 11
2498                }
2499                "#
2500                );
2501            },
2502        );
2503    }
2504
2505    #[test]
2506    fn javascript_nesting() {
2507        check_metrics::<JavascriptParser>(
2508            "function f() {
2509                 if (a) { // +1
2510                     for (let i = 0; i < 10; i++) { // +2 (nesting = 1)
2511                         while (b) { // +3 (nesting = 2)
2512                             console.log(\"test\");
2513                         }
2514                     }
2515                 }
2516             }",
2517            "foo.js",
2518            |metric| {
2519                insta::assert_json_snapshot!(
2520                    metric.cognitive,
2521                    @r#"
2522                {
2523                  "sum": 6,
2524                  "value": 0,
2525                  "average": 6.0,
2526                  "min": 0,
2527                  "max": 6
2528                }
2529                "#
2530                );
2531            },
2532        );
2533    }
2534
2535    #[test]
2536    fn python_2_level_nesting() {
2537        check_metrics::<PythonParser>(
2538            "def f(a, b):
2539                if a:  # +1
2540                    for i in range(b):  # +2
2541                        if b:  # +3
2542                            return 1",
2543            "foo.py",
2544            |metric| {
2545                insta::assert_json_snapshot!(
2546                    metric.cognitive,
2547                    @r#"
2548                {
2549                  "sum": 6,
2550                  "value": 0,
2551                  "average": 6.0,
2552                  "min": 0,
2553                  "max": 6
2554                }
2555                "#
2556                );
2557            },
2558        );
2559    }
2560
2561    #[test]
2562    fn rust_2_level_nesting() {
2563        check_metrics::<RustParser>(
2564            "fn f() {
2565                 if true { // +1
2566                     for i in 0..4 { // +2 (nesting = 1)
2567                         match true { // +3 (nesting = 2)
2568                             true => println!(\"test\"),
2569                             false => println!(\"test\"),
2570                         }
2571                     }
2572                 }
2573             }",
2574            "foo.rs",
2575            |metric| {
2576                insta::assert_json_snapshot!(
2577                    metric.cognitive,
2578                    @r#"
2579                {
2580                  "sum": 6,
2581                  "value": 0,
2582                  "average": 6.0,
2583                  "min": 0,
2584                  "max": 6
2585                }
2586                "#
2587                );
2588            },
2589        );
2590    }
2591
2592    #[test]
2593    fn python_try_construct() {
2594        check_metrics::<PythonParser>(
2595            "def f(a, b):
2596                try:
2597                    for foo in bar:  # +1
2598                        return a
2599                except Exception:  # +1
2600                    if a < 0:  # +2
2601                        return a",
2602            "foo.py",
2603            |metric| {
2604                insta::assert_json_snapshot!(
2605                    metric.cognitive,
2606                    @r#"
2607                {
2608                  "sum": 4,
2609                  "value": 0,
2610                  "average": 4.0,
2611                  "min": 0,
2612                  "max": 4
2613                }
2614                "#
2615                );
2616            },
2617        );
2618    }
2619
2620    #[test]
2621    fn python_flat_try_except() {
2622        // Regression for #242: flat try/except at function top level
2623        // must still score +1 for the except clause (no enclosing
2624        // control-flow nesting). Before the fix this happened to be
2625        // correct because `stats.nesting` was zero; after the fix the
2626        // value is the same — `increase_nesting` records nesting=0 and
2627        // bumps structural by 0+1.
2628        check_metrics::<PythonParser>(
2629            "def f():
2630                try:
2631                    pass
2632                except Exception:  # +1
2633                    pass",
2634            "foo.py",
2635            |metric| {
2636                // expected: only the except clause contributes (+1).
2637                assert_eq!(metric.cognitive.cognitive_sum() as u32, 1);
2638                insta::assert_json_snapshot!(
2639                    metric.cognitive,
2640                    @r#"
2641                {
2642                  "sum": 1,
2643                  "value": 0,
2644                  "average": 1.0,
2645                  "min": 0,
2646                  "max": 1
2647                }
2648                "#
2649                );
2650            },
2651        );
2652    }
2653
2654    #[test]
2655    fn python_except_inside_if() {
2656        // Regression for #242: try/except nested inside an `if` must
2657        // apply a nesting penalty to the except clause. Before the
2658        // fix, the except contributed +1 because `stats.nesting` was
2659        // stale (0 from the previous `increase_nesting` call on the
2660        // if). After the fix the except sees nesting=1 and contributes
2661        // +2.
2662        check_metrics::<PythonParser>(
2663            "def f(x):
2664                if x:  # +1
2665                    try:
2666                        pass
2667                    except Exception:  # +2 (nesting = 1)
2668                        pass",
2669            "foo.py",
2670            |metric| {
2671                // expected: if (+1) + except inside if (+2) = 3
2672                assert_eq!(metric.cognitive.cognitive_sum() as u32, 3);
2673                insta::assert_json_snapshot!(
2674                    metric.cognitive,
2675                    @r#"
2676                {
2677                  "sum": 3,
2678                  "value": 0,
2679                  "average": 3.0,
2680                  "min": 0,
2681                  "max": 3
2682                }
2683                "#
2684                );
2685            },
2686        );
2687    }
2688
2689    #[test]
2690    fn python_except_inside_for() {
2691        // Regression for #242: try/except nested inside a `for` must
2692        // apply the for's nesting penalty to the except clause.
2693        check_metrics::<PythonParser>(
2694            "def f(xs):
2695                for x in xs:  # +1
2696                    try:
2697                        pass
2698                    except Exception:  # +2 (nesting = 1)
2699                        pass",
2700            "foo.py",
2701            |metric| {
2702                // expected: for (+1) + except inside for (+2) = 3
2703                assert_eq!(metric.cognitive.cognitive_sum() as u32, 3);
2704                insta::assert_json_snapshot!(
2705                    metric.cognitive,
2706                    @r#"
2707                {
2708                  "sum": 3,
2709                  "value": 0,
2710                  "average": 3.0,
2711                  "min": 0,
2712                  "max": 3
2713                }
2714                "#
2715                );
2716            },
2717        );
2718    }
2719
2720    #[test]
2721    fn python_multi_except_inside_if() {
2722        // Regression for #242: every clause in a multi-except chain
2723        // nested inside an `if` must reflect the nesting penalty.
2724        // Before the fix, all three except clauses contributed +1;
2725        // after the fix each contributes +2 (nesting = 1 from the
2726        // enclosing if).
2727        check_metrics::<PythonParser>(
2728            "def f(x):
2729                if x:  # +1
2730                    try:
2731                        pass
2732                    except ValueError:    # +2
2733                        pass
2734                    except TypeError:     # +2
2735                        pass
2736                    except Exception:     # +2
2737                        pass",
2738            "foo.py",
2739            |metric| {
2740                // expected: if (+1) + 3 * except inside if (+2 each) = 7
2741                assert_eq!(metric.cognitive.cognitive_sum() as u32, 7);
2742                insta::assert_json_snapshot!(
2743                    metric.cognitive,
2744                    @r#"
2745                {
2746                  "sum": 7,
2747                  "value": 0,
2748                  "average": 7.0,
2749                  "min": 0,
2750                  "max": 7
2751                }
2752                "#
2753                );
2754            },
2755        );
2756    }
2757
2758    #[test]
2759    fn mozjs_try_construct() {
2760        check_metrics::<MozjsParser>(
2761            "function asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
2762                 for (const collector of this.collectors) {
2763                     try {
2764                         collector._onChannelRedirect(oldChannel, newChannel, flags);
2765                     } catch (ex) {
2766                         console.error(
2767                             \"StackTraceCollector.onChannelRedirect threw an exception\",
2768                              ex
2769                         );
2770                     }
2771                 }
2772                 callback.onRedirectVerifyCallback(Cr.NS_OK);
2773             }",
2774            "foo.js",
2775            |metric| {
2776                insta::assert_json_snapshot!(
2777                    metric.cognitive,
2778                    @r#"
2779                {
2780                  "sum": 3,
2781                  "value": 0,
2782                  "average": 3.0,
2783                  "min": 0,
2784                  "max": 3
2785                }
2786                "#
2787                );
2788            },
2789        );
2790    }
2791
2792    #[test]
2793    fn javascript_try_construct() {
2794        check_metrics::<JavascriptParser>(
2795            "function f() {
2796                 for (let i = 0; i < 10; i++) { // +1
2797                     try {
2798                         doSomething(i);
2799                     } catch (ex) { // +2 (nesting = 1)
2800                         if (ex instanceof TypeError) { // +3 (nesting = 2)
2801                             console.error(\"type error\");
2802                         }
2803                     } finally {
2804                         cleanup();
2805                     }
2806                 }
2807             }",
2808            "foo.js",
2809            |metric| {
2810                insta::assert_json_snapshot!(
2811                    metric.cognitive,
2812                    @r#"
2813                {
2814                  "sum": 6,
2815                  "value": 0,
2816                  "average": 6.0,
2817                  "min": 0,
2818                  "max": 6
2819                }
2820                "#
2821                );
2822            },
2823        );
2824    }
2825
2826    // The tree-sitter-javascript / -typescript grammars fold both
2827    // `for...in` and `for...of` into the same `for_in_statement` node
2828    // (only the keyword token differs). The four regression tests below
2829    // lock that in across every JS-family parser, so any future grammar
2830    // bump that splits `for...of` into its own node kind would surface
2831    // here rather than silently scoring `for...of` loops as 0 cognitive.
2832
2833    #[test]
2834    fn javascript_for_of_loop() {
2835        check_metrics::<JavascriptParser>(
2836            "function f(xs) {
2837                 let s = 0;
2838                 for (const x of xs) { // +1
2839                     s += x;
2840                 }
2841                 return s;
2842             }",
2843            "foo.js",
2844            |metric| {
2845                assert_eq!(metric.cognitive.cognitive_sum(), 1);
2846                assert_eq!(metric.cognitive.cognitive_max(), 1);
2847                insta::assert_json_snapshot!(
2848                    metric.cognitive,
2849                    @r#"
2850                {
2851                  "sum": 1,
2852                  "value": 0,
2853                  "average": 1.0,
2854                  "min": 0,
2855                  "max": 1
2856                }
2857                "#
2858                );
2859            },
2860        );
2861    }
2862
2863    #[test]
2864    fn mozjs_for_of_loop() {
2865        check_metrics::<MozjsParser>(
2866            "function f(xs) {
2867                 let s = 0;
2868                 for (const x of xs) { // +1
2869                     s += x;
2870                 }
2871                 return s;
2872             }",
2873            "foo.js",
2874            |metric| {
2875                assert_eq!(metric.cognitive.cognitive_sum(), 1);
2876                assert_eq!(metric.cognitive.cognitive_max(), 1);
2877                insta::assert_json_snapshot!(
2878                    metric.cognitive,
2879                    @r#"
2880                {
2881                  "sum": 1,
2882                  "value": 0,
2883                  "average": 1.0,
2884                  "min": 0,
2885                  "max": 1
2886                }
2887                "#
2888                );
2889            },
2890        );
2891    }
2892
2893    #[test]
2894    fn typescript_for_of_loop() {
2895        check_metrics::<TypescriptParser>(
2896            "function f(xs: number[]): number {
2897                 let s = 0;
2898                 for (const x of xs) { // +1
2899                     s += x;
2900                 }
2901                 return s;
2902             }",
2903            "foo.ts",
2904            |metric| {
2905                assert_eq!(metric.cognitive.cognitive_sum(), 1);
2906                assert_eq!(metric.cognitive.cognitive_max(), 1);
2907                insta::assert_json_snapshot!(
2908                    metric.cognitive,
2909                    @r#"
2910                {
2911                  "sum": 1,
2912                  "value": 0,
2913                  "average": 1.0,
2914                  "min": 0,
2915                  "max": 1
2916                }
2917                "#
2918                );
2919            },
2920        );
2921    }
2922
2923    #[test]
2924    fn tsx_for_of_loop() {
2925        check_metrics::<TsxParser>(
2926            "function f(xs: number[]): number {
2927                 let s = 0;
2928                 for (const x of xs) { // +1
2929                     s += x;
2930                 }
2931                 return s;
2932             }",
2933            "foo.tsx",
2934            |metric| {
2935                assert_eq!(metric.cognitive.cognitive_sum(), 1);
2936                assert_eq!(metric.cognitive.cognitive_max(), 1);
2937                insta::assert_json_snapshot!(
2938                    metric.cognitive,
2939                    @r#"
2940                {
2941                  "sum": 1,
2942                  "value": 0,
2943                  "average": 1.0,
2944                  "min": 0,
2945                  "max": 1
2946                }
2947                "#
2948                );
2949            },
2950        );
2951    }
2952
2953    #[test]
2954    fn rust_break_continue() {
2955        // Only labeled break and continue statements are considered
2956        check_metrics::<RustParser>(
2957            "fn f() {
2958                 'tens: for ten in 0..3 { // +1
2959                     '_units: for unit in 0..=9 { // +2 (nesting = 1)
2960                         if unit % 2 == 0 { // +3 (nesting = 2)
2961                             continue;
2962                         } else if unit == 5 { // +1
2963                             continue 'tens; // +1
2964                         } else if unit == 6 { // +1
2965                             break;
2966                         } else { // +1
2967                             break 'tens; // +1
2968                         }
2969                     }
2970                 }
2971             }",
2972            "foo.rs",
2973            |metric| {
2974                insta::assert_json_snapshot!(
2975                    metric.cognitive,
2976                    @r#"
2977                {
2978                  "sum": 11,
2979                  "value": 0,
2980                  "average": 11.0,
2981                  "min": 0,
2982                  "max": 11
2983                }
2984                "#
2985                );
2986            },
2987        );
2988    }
2989
2990    // Regression for #389: Rust's `loop {}` has a dedicated grammar node
2991    // (LoopExpression) distinct from WhileExpression. The cognitive nesting
2992    // arm previously matched only For/While/Match, so `loop {}` silently
2993    // contributed neither a structural +1 nor a nesting bump.
2994    #[test]
2995    fn rust_loop_single() {
2996        check_metrics::<RustParser>(
2997            "fn f() {
2998                 loop { // +1
2999                     if true { // +2 (nesting = 1)
3000                         break;
3001                     }
3002                 }
3003             }",
3004            "foo.rs",
3005            |metric| {
3006                // expected: loop=+1, nested if=+2 (1 + nesting depth 1) = 3
3007                assert_eq!(metric.cognitive.cognitive_sum() as u32, 3);
3008                insta::assert_json_snapshot!(
3009                    metric.cognitive,
3010                    @r#"
3011                {
3012                  "sum": 3,
3013                  "value": 0,
3014                  "average": 3.0,
3015                  "min": 0,
3016                  "max": 3
3017                }
3018                "#
3019                );
3020            },
3021        );
3022    }
3023
3024    // Regression for #389: nested `loop` blocks must accrue nesting just
3025    // like nested `while`/`for` would.
3026    #[test]
3027    fn rust_loop_nested() {
3028        check_metrics::<RustParser>(
3029            "fn f() {
3030                 loop { // +1
3031                     loop { // +2 (nesting = 1)
3032                         if true { // +3 (nesting = 2)
3033                             break;
3034                         }
3035                     }
3036                 }
3037             }",
3038            "foo.rs",
3039            |metric| {
3040                // expected: outer loop=+1, inner loop=+2, inner if=+3 = 6
3041                assert_eq!(metric.cognitive.cognitive_sum() as u32, 6);
3042                insta::assert_json_snapshot!(
3043                    metric.cognitive,
3044                    @r#"
3045                {
3046                  "sum": 6,
3047                  "value": 0,
3048                  "average": 6.0,
3049                  "min": 0,
3050                  "max": 6
3051                }
3052                "#
3053                );
3054            },
3055        );
3056    }
3057
3058    #[test]
3059    fn cpp_nested_function_resets_nesting_and_adds_depth() {
3060        // Regression for #696: a method defined on a local struct declared
3061        // two `if`s deep inside an outer method must reset nesting to 0 and
3062        // gain a function-depth surcharge — not inherit the enclosing
3063        // nesting.
3064        //
3065        // expected: outer `if` (+1, nesting=0) + inner `if` (+2, nesting=1)
3066        // + Inner::g's `if` (+1 base + 1 depth = +2, nesting=0, depth=1) = 5.
3067        // Before the fix, `g` inherited nesting=2 from the two enclosing
3068        // `if`s, scoring its inner `if` at nesting 2 (+3) for a sum of 6.
3069        // The two-deep nesting is load-bearing: one level deep, the
3070        // inherited nesting (1) coincidentally equals the depth bump (1).
3071        check_metrics::<CppParser>(
3072            "struct S {
3073                void outer(bool a) {
3074                    if (a) {
3075                        if (a) {
3076                            struct Inner {
3077                                void g(bool b) {
3078                                    if (b) { h(); }
3079                                }
3080                            };
3081                        }
3082                    }
3083                }
3084            };",
3085            "foo.cpp",
3086            |metric| {
3087                assert_eq!(metric.cognitive.cognitive_sum(), 5);
3088                assert_eq!(metric.cognitive.cognitive_max(), 3);
3089            },
3090        );
3091    }
3092
3093    #[test]
3094    fn c_goto() {
3095        check_metrics::<CParser>(
3096            "void f() {
3097             OUT: for (int i = 1; i <= max; ++i) { // +1
3098                      for (int j = 2; j < i; ++j) { // +2 (nesting = 1)
3099                          if (i % j == 0) { // +3 (nesting = 2)
3100                              goto OUT; // +1
3101                          }
3102                      }
3103                  }
3104             }",
3105            "foo.c",
3106            |metric| {
3107                insta::assert_json_snapshot!(
3108                    metric.cognitive,
3109                    @r#"
3110                {
3111                  "sum": 7,
3112                  "value": 0,
3113                  "average": 7.0,
3114                  "min": 0,
3115                  "max": 7
3116                }
3117                "#
3118                );
3119            },
3120        );
3121    }
3122
3123    #[test]
3124    fn c_switch() {
3125        check_metrics::<CParser>(
3126            "void f() {
3127                 switch (1) { // +1
3128                     case 1:
3129                         printf(\"one\");
3130                         break;
3131                     case 2:
3132                         printf(\"two\");
3133                         break;
3134                     case 3:
3135                         printf(\"three\");
3136                         break;
3137                     default:
3138                         printf(\"all\");
3139                         break;
3140                 }
3141             }",
3142            "foo.c",
3143            |metric| {
3144                insta::assert_json_snapshot!(
3145                    metric.cognitive,
3146                    @r#"
3147                {
3148                  "sum": 1,
3149                  "value": 0,
3150                  "average": 1.0,
3151                  "min": 0,
3152                  "max": 1
3153                }
3154                "#
3155                );
3156            },
3157        );
3158    }
3159
3160    #[test]
3161    fn c_ternary() {
3162        // Sonar's rule scores the ternary `?:` as +1 (and +nesting), matching
3163        // the JS/Java/Python/Rust families. The cognitive walker matches the
3164        // `conditional_expression` node, so the operator participates in nesting
3165        // like any other conditional construct.
3166        check_metrics::<CParser>(
3167            "int f(int a) {
3168                 if (a) { // +1
3169                     return a > 0 ? 1 : -1; // +2 (1 + nesting 1)
3170                 }
3171                 return a > 0 ? 0 : -1; // +1
3172             }",
3173            "foo.c",
3174            // expected: 1 (if) + 2 (nested ternary, nesting=1) + 1 (top-level
3175            // ternary) = 4. max is 4 for the only function.
3176            |metric| {
3177                assert_eq!(metric.cognitive.cognitive_sum(), 4);
3178                assert_eq!(metric.cognitive.cognitive_max(), 4);
3179                insta::assert_json_snapshot!(
3180                    metric.cognitive,
3181                    @r#"
3182                {
3183                  "sum": 4,
3184                  "value": 0,
3185                  "average": 4.0,
3186                  "min": 0,
3187                  "max": 4
3188                }
3189                "#
3190                );
3191            },
3192        );
3193    }
3194
3195    #[test]
3196    fn cpp_try_catch_single() {
3197        check_metrics::<CppParser>(
3198            "void f() {
3199                 try {
3200                     g();
3201                 } catch (const std::exception& e) { // +1
3202                     h();
3203                 }
3204             }",
3205            "foo.cpp",
3206            |metric| {
3207                // Single catch clause +1.
3208                assert_eq!(metric.cognitive.cognitive_sum(), 1);
3209                assert_eq!(metric.cognitive.cognitive_max(), 1);
3210                insta::assert_json_snapshot!(
3211                    metric.cognitive,
3212                    @r#"
3213                {
3214                  "sum": 1,
3215                  "value": 0,
3216                  "average": 1.0,
3217                  "min": 0,
3218                  "max": 1
3219                }
3220                "#
3221                );
3222            },
3223        );
3224    }
3225
3226    #[test]
3227    fn cpp_try_multiple_catches() {
3228        check_metrics::<CppParser>(
3229            "void f() {
3230                 try {
3231                     g();
3232                 } catch (const std::runtime_error& e) { // +1
3233                     h();
3234                 } catch (const std::logic_error& e) { // +1
3235                     i();
3236                 } catch (...) { // +1
3237                     j();
3238                 }
3239             }",
3240            "foo.cpp",
3241            |metric| {
3242                // Three catch clauses, each +1 at nesting 0 → 3.
3243                assert_eq!(metric.cognitive.cognitive_sum(), 3);
3244                assert_eq!(metric.cognitive.cognitive_max(), 3);
3245                insta::assert_json_snapshot!(
3246                    metric.cognitive,
3247                    @r#"
3248                {
3249                  "sum": 3,
3250                  "value": 0,
3251                  "average": 3.0,
3252                  "min": 0,
3253                  "max": 3
3254                }
3255                "#
3256                );
3257            },
3258        );
3259    }
3260
3261    #[test]
3262    fn cpp_try_catch_in_loop() {
3263        check_metrics::<CppParser>(
3264            "void f() {
3265                 for (int i = 0; i < 10; ++i) { // +1
3266                     try {
3267                         g();
3268                     } catch (const std::exception& e) { // +2 (nesting = 1)
3269                         h();
3270                     }
3271                 }
3272             }",
3273            "foo.cpp",
3274            |metric| {
3275                // for +1, catch +2 (nesting = 1) → 3.
3276                assert_eq!(metric.cognitive.cognitive_sum(), 3);
3277                assert_eq!(metric.cognitive.cognitive_max(), 3);
3278                insta::assert_json_snapshot!(
3279                    metric.cognitive,
3280                    @r#"
3281                {
3282                  "sum": 3,
3283                  "value": 0,
3284                  "average": 3.0,
3285                  "min": 0,
3286                  "max": 3
3287                }
3288                "#
3289                );
3290            },
3291        );
3292    }
3293
3294    #[test]
3295    fn cpp_range_based_for() {
3296        check_metrics::<CppParser>(
3297            "int sum(const std::vector<int>& v) {
3298                 int s = 0;
3299                 for (int x : v) { // +1
3300                     s += x;
3301                 }
3302                 return s;
3303             }",
3304            "foo.cpp",
3305            |metric| {
3306                // C++11 range-based `for (auto x : v)` parses as
3307                // `for_range_loop`; it is a control-flow construct and
3308                // counts the same as a classic `for_statement` → +1.
3309                assert_eq!(metric.cognitive.cognitive_sum(), 1);
3310                assert_eq!(metric.cognitive.cognitive_max(), 1);
3311                insta::assert_json_snapshot!(
3312                    metric.cognitive,
3313                    @r#"
3314                {
3315                  "sum": 1,
3316                  "value": 0,
3317                  "average": 1.0,
3318                  "min": 0,
3319                  "max": 1
3320                }
3321                "#
3322                );
3323            },
3324        );
3325    }
3326
3327    #[test]
3328    fn cpp_nested_range_based_for() {
3329        check_metrics::<CppParser>(
3330            "void f(const std::vector<std::vector<int>>& vv) {
3331                 for (const auto& row : vv) { // +1
3332                     for (int x : row) { // +2 (nesting = 1)
3333                         g(x);
3334                     }
3335                 }
3336             }",
3337            "foo.cpp",
3338            |metric| {
3339                // Nested range-fors compound by nesting, matching the
3340                // behaviour of nested classic `for` loops: 1 + 2 = 3.
3341                assert_eq!(metric.cognitive.cognitive_sum(), 3);
3342                assert_eq!(metric.cognitive.cognitive_max(), 3);
3343                insta::assert_json_snapshot!(
3344                    metric.cognitive,
3345                    @r#"
3346                {
3347                  "sum": 3,
3348                  "value": 0,
3349                  "average": 3.0,
3350                  "min": 0,
3351                  "max": 3
3352                }
3353                "#
3354                );
3355            },
3356        );
3357    }
3358
3359    #[test]
3360    fn c_nested_for() {
3361        check_metrics::<CParser>(
3362            "void f(int n, int m) {
3363                 for (int i = 0; i < n; ++i) { // +1
3364                     for (int j = 0; j < m; ++j) { // +2 (nesting = 1)
3365                         for (int k = 0; k < 4; ++k) { // +3 (nesting = 2)
3366                             g(i, j, k);
3367                         }
3368                     }
3369                 }
3370             }",
3371            "foo.c",
3372            |metric| {
3373                // Three nested `for` loops → 1 + 2 + 3 = 6.
3374                assert_eq!(metric.cognitive.cognitive_sum(), 6);
3375                assert_eq!(metric.cognitive.cognitive_max(), 6);
3376                insta::assert_json_snapshot!(
3377                    metric.cognitive,
3378                    @r#"
3379                {
3380                  "sum": 6,
3381                  "value": 0,
3382                  "average": 6.0,
3383                  "min": 0,
3384                  "max": 6
3385                }
3386                "#
3387                );
3388            },
3389        );
3390    }
3391
3392    #[test]
3393    fn c_nested_while() {
3394        check_metrics::<CParser>(
3395            "void f(int n) {
3396                 while (n > 0) { // +1
3397                     while (n % 2 == 0) { // +2 (nesting = 1)
3398                         n /= 2;
3399                     }
3400                     n -= 1;
3401                 }
3402             }",
3403            "foo.c",
3404            |metric| {
3405                // Two nested `while` loops → 1 + 2 = 3.
3406                assert_eq!(metric.cognitive.cognitive_sum(), 3);
3407                assert_eq!(metric.cognitive.cognitive_max(), 3);
3408                insta::assert_json_snapshot!(
3409                    metric.cognitive,
3410                    @r#"
3411                {
3412                  "sum": 3,
3413                  "value": 0,
3414                  "average": 3.0,
3415                  "min": 0,
3416                  "max": 3
3417                }
3418                "#
3419                );
3420            },
3421        );
3422    }
3423
3424    #[test]
3425    fn c_recursion() {
3426        // Sonar's rule scores each recursive call to the enclosing function
3427        // as +1, but the file-level comment in `cognitive.rs` documents that
3428        // recursion is not tracked for C/C++ because the call graph is only
3429        // resolvable at run time. The body of `fact` therefore costs only
3430        // the explicit `if`.
3431        check_metrics::<CParser>(
3432            "int fact(int n) {
3433                 if (n <= 1) { // +1
3434                     return 1;
3435                 }
3436                 return n * fact(n - 1); // recursion: currently not counted
3437             }",
3438            "foo.c",
3439            |metric| {
3440                // Only the `if` contributes; recursion is a documented gap.
3441                assert_eq!(metric.cognitive.cognitive_sum(), 1);
3442                assert_eq!(metric.cognitive.cognitive_max(), 1);
3443                insta::assert_json_snapshot!(
3444                    metric.cognitive,
3445                    @r#"
3446                {
3447                  "sum": 1,
3448                  "value": 0,
3449                  "average": 1.0,
3450                  "min": 0,
3451                  "max": 1
3452                }
3453                "#
3454                );
3455            },
3456        );
3457    }
3458
3459    #[test]
3460    fn c_goto_sibling_jump() {
3461        check_metrics::<CParser>(
3462            "void f(int n) {
3463                 if (n < 0) { // +1
3464                     goto err; // +1
3465                 }
3466                 if (n > 100) { // +1
3467                     goto err; // +1
3468                 }
3469                 return;
3470             err:
3471                 abort();
3472             }",
3473            "foo.c",
3474            |metric| {
3475                // Two `if` (+1 each) and two `goto` (+1 each) at nesting 0
3476                // (the `goto` cost is flat, not multiplied by nesting) → 4.
3477                assert_eq!(metric.cognitive.cognitive_sum(), 4);
3478                assert_eq!(metric.cognitive.cognitive_max(), 4);
3479                insta::assert_json_snapshot!(
3480                    metric.cognitive,
3481                    @r#"
3482                {
3483                  "sum": 4,
3484                  "value": 0,
3485                  "average": 4.0,
3486                  "min": 0,
3487                  "max": 4
3488                }
3489                "#
3490                );
3491            },
3492        );
3493    }
3494
3495    #[test]
3496    fn cpp_lambda_inside_function() {
3497        // Per `increase_nesting`, entering a lambda bumps the effective nesting
3498        // by one — so an `if` directly inside a top-level lambda is +2 charged
3499        // to the enclosing function (Cpp lambdas are not split into a separate
3500        // FuncSpace by `getter.rs`, so the `if` is not double-counted).
3501        // The lambda *is* counted as a closure by NoM, so the cognitive
3502        // average is sum / (1 function + 1 closure) = 2 / 2 = 1.0.
3503        check_metrics::<CppParser>(
3504            "int f(const std::vector<int>& v) {
3505                 auto pred = [](int x) {
3506                     if (x > 0) { // +2 (lambda nesting = 1)
3507                         return true;
3508                     }
3509                     return false;
3510                 };
3511                 return std::count_if(v.begin(), v.end(), pred);
3512             }",
3513            "foo.cpp",
3514            |metric| {
3515                // Single `if` inside lambda at lambda-nesting 1 → +2.
3516                assert_eq!(metric.cognitive.cognitive_sum(), 2);
3517                assert_eq!(metric.cognitive.cognitive_max(), 2);
3518                insta::assert_json_snapshot!(
3519                    metric.cognitive,
3520                    @r#"
3521                {
3522                  "sum": 2,
3523                  "value": 0,
3524                  "average": 1.0,
3525                  "min": 0,
3526                  "max": 2
3527                }
3528                "#
3529                );
3530            },
3531        );
3532    }
3533
3534    /// The `mozcpp` fork must charge a lambda the same nesting surcharge
3535    /// as upstream C++ — the same source through `CppParser`
3536    /// (`cpp_lambda_inside_function` above) scores identically.
3537    ///
3538    /// `mozcpp`'s `LambdaExpression` arm had no cognitive test before
3539    /// this, so the whole arm measured zero-coverage even though the
3540    /// fork is expected to stay metric-equivalent to `cpp`.
3541    #[test]
3542    fn mozcpp_lambda_inside_function() {
3543        check_metrics::<MozcppParser>(
3544            "int f(const std::vector<int>& v) {
3545                 auto pred = [](int x) {
3546                     if (x > 0) { // +2 (lambda nesting = 1)
3547                         return true;
3548                     }
3549                     return false;
3550                 };
3551                 return std::count_if(v.begin(), v.end(), pred);
3552             }",
3553            "foo.cpp",
3554            |metric| {
3555                assert_eq!(metric.cognitive.cognitive_sum(), 2);
3556                assert_eq!(metric.cognitive.cognitive_max(), 2);
3557            },
3558        );
3559    }
3560
3561    #[test]
3562    fn c_switch_fall_through() {
3563        // A `case` without `break` (fall-through) does not add cognitive cost
3564        // beyond the enclosing `switch` itself: only `switch` is in the match
3565        // arm. Same accounting as `c_switch` above — switch +1 only.
3566        check_metrics::<CParser>(
3567            "void f(int n) {
3568                 switch (n) { // +1
3569                     case 1:
3570                     case 2:
3571                         g();
3572                         // fall-through
3573                     case 3:
3574                         h();
3575                         break;
3576                     default:
3577                         i();
3578                         break;
3579                 }
3580             }",
3581            "foo.c",
3582            |metric| {
3583                assert_eq!(metric.cognitive.cognitive_sum(), 1);
3584                assert_eq!(metric.cognitive.cognitive_max(), 1);
3585                insta::assert_json_snapshot!(
3586                    metric.cognitive,
3587                    @r#"
3588                {
3589                  "sum": 1,
3590                  "value": 0,
3591                  "average": 1.0,
3592                  "min": 0,
3593                  "max": 1
3594                }
3595                "#
3596                );
3597            },
3598        );
3599    }
3600
3601    #[test]
3602    fn c_switch_in_loop() {
3603        check_metrics::<CParser>(
3604            "void f(int n) {
3605                 for (int i = 0; i < n; ++i) { // +1
3606                     switch (i % 3) { // +2 (nesting = 1)
3607                         case 0:
3608                             a();
3609                             break;
3610                         case 1:
3611                             b();
3612                             break;
3613                         default:
3614                             c();
3615                             break;
3616                     }
3617                 }
3618             }",
3619            "foo.c",
3620            |metric| {
3621                // for +1, switch +2 (nesting = 1) → 3.
3622                assert_eq!(metric.cognitive.cognitive_sum(), 3);
3623                assert_eq!(metric.cognitive.cognitive_max(), 3);
3624                insta::assert_json_snapshot!(
3625                    metric.cognitive,
3626                    @r#"
3627                {
3628                  "sum": 3,
3629                  "value": 0,
3630                  "average": 3.0,
3631                  "min": 0,
3632                  "max": 3
3633                }
3634                "#
3635                );
3636            },
3637        );
3638    }
3639
3640    #[test]
3641    fn c_macro_expanded_control_flow() {
3642        // Per the file-level comment in `cognitive.rs`, macro expansion is not
3643        // tracked for C/C++ — macros are treated as opaque tokens. This is the
3644        // defensive case: a control-flow-bearing macro contributes nothing on
3645        // its own; only the explicit `if` in the function body is counted.
3646        check_metrics::<CParser>(
3647            "#define CHECK(x) do { if (!(x)) return; } while (0)
3648             void f(int a, int b) {
3649                 CHECK(a);              // expansion is opaque: 0
3650                 if (b < 0) {           // +1
3651                     return;
3652                 }
3653             }",
3654            "foo.c",
3655            |metric| {
3656                // Only the explicit `if` contributes.
3657                assert_eq!(metric.cognitive.cognitive_sum(), 1);
3658                assert_eq!(metric.cognitive.cognitive_max(), 1);
3659                insta::assert_json_snapshot!(
3660                    metric.cognitive,
3661                    @r#"
3662                {
3663                  "sum": 1,
3664                  "value": 0,
3665                  "average": 1.0,
3666                  "min": 0,
3667                  "max": 1
3668                }
3669                "#
3670                );
3671            },
3672        );
3673    }
3674
3675    #[test]
3676    fn mozjs_switch() {
3677        check_metrics::<MozjsParser>(
3678            "function f() {
3679                 switch (1) { // +1
3680                     case 1:
3681                         window.print(\"one\");
3682                         break;
3683                     case 2:
3684                         window.print(\"two\");
3685                         break;
3686                     case 3:
3687                         window.print(\"three\");
3688                         break;
3689                     default:
3690                         window.print(\"all\");
3691                         break;
3692                 }
3693             }",
3694            "foo.js",
3695            |metric| {
3696                insta::assert_json_snapshot!(
3697                    metric.cognitive,
3698                    @r#"
3699                {
3700                  "sum": 1,
3701                  "value": 0,
3702                  "average": 1.0,
3703                  "min": 0,
3704                  "max": 1
3705                }
3706                "#
3707                );
3708            },
3709        );
3710    }
3711
3712    #[test]
3713    fn javascript_switch() {
3714        check_metrics::<JavascriptParser>(
3715            "function f() {
3716                 switch (x) { // +1
3717                     case 1:
3718                         console.log(\"one\");
3719                         break;
3720                     case 2:
3721                         console.log(\"two\");
3722                         break;
3723                     default:
3724                         console.log(\"other\");
3725                         break;
3726                 }
3727             }",
3728            "foo.js",
3729            |metric| {
3730                insta::assert_json_snapshot!(
3731                    metric.cognitive,
3732                    @r#"
3733                {
3734                  "sum": 1,
3735                  "value": 0,
3736                  "average": 1.0,
3737                  "min": 0,
3738                  "max": 1
3739                }
3740                "#
3741                );
3742            },
3743        );
3744    }
3745
3746    #[test]
3747    fn python_ternary_operator() {
3748        check_metrics::<PythonParser>(
3749            "def f(a, b):
3750                 if a % 2:  # +1
3751                     return 'c' if a else 'd'  # +2
3752                 return 'a' if a else 'b'  # +1",
3753            "foo.py",
3754            |metric| {
3755                insta::assert_json_snapshot!(
3756                    metric.cognitive,
3757                    @r#"
3758                {
3759                  "sum": 4,
3760                  "value": 0,
3761                  "average": 4.0,
3762                  "min": 0,
3763                  "max": 4
3764                }
3765                "#
3766                );
3767            },
3768        );
3769    }
3770
3771    /// Cognitive cost of a boolean sequence inside a `lambda`, under
3772    /// each statement kind that can enclose one.
3773    ///
3774    /// This pins the scores, not the stop set.
3775    /// `python_apply_boolean_operator`'s enclosing-lambda walk stops at
3776    /// `ExpressionList | IfStatement | ForStatement | WhileStatement`,
3777    /// and no fixture below discriminates any of those arms: none has a
3778    /// `lambda` above the stop node, so halting there and running to the
3779    /// module root give the same count. Do not "strengthen" this test by
3780    /// asserting on the arms — the one arm that can differ,
3781    /// `ExpressionList`, is discriminated by
3782    /// `python_boolean_in_expression_list_under_lambda` (#1090).
3783    #[test]
3784    fn python_boolean_in_lambda_scores_under_each_enclosing_statement() {
3785        use crate::test_support::metrics_verbatim;
3786
3787        let cognitive_sum = |source: &str| {
3788            metrics_verbatim(
3789                crate::LANG::Python,
3790                source.as_bytes(),
3791                MetricsOptions::default(),
3792            )
3793            .cognitive
3794            .cognitive_sum()
3795        };
3796
3797        // No enclosing branch statement: +1 boolean sequence, +1 for the
3798        // one enclosing lambda = 2.
3799        assert_eq!(cognitive_sum("y = (lambda x: x and x)(1)\n"), 2);
3800
3801        // Each branch statement adds its own +1 nesting on top of that
3802        // same 2.
3803        for (label, source) in [
3804            ("if", "if (lambda x: x and x)(1):\n    pass\n"),
3805            ("for", "for i in (lambda x: x and x)(1):\n    pass\n"),
3806            ("while", "while (lambda x: x and x)(1):\n    break\n"),
3807            (
3808                "for over a comma list",
3809                "for i in (lambda x: x and x)(1), 2:\n    pass\n",
3810            ),
3811        ] {
3812            assert_eq!(
3813                cognitive_sum(source),
3814                3,
3815                "{label}: +1 statement nesting, +1 lambda, +1 boolean sequence"
3816            );
3817        }
3818
3819        // A second enclosing lambda adds one more, which is what the
3820        // enclosing-lambda walk is actually for.
3821        assert_eq!(
3822            cognitive_sum("f = lambda a: ((lambda x: x and x)(1), 2)\n"),
3823            3
3824        );
3825    }
3826
3827    /// The `ExpressionList` arm of `python_apply_boolean_operator`'s
3828    /// stop set — the only one of its four arms that can change a score.
3829    ///
3830    /// Two grammar productions can put an `expression_list` under a
3831    /// `lambda`: a parenthesised `yield`, and an f-string interpolation
3832    /// (`_f_expression`). Every other site tree-sitter-python spells
3833    /// `expression_list` at is either a statement (`return`, `del`,
3834    /// `raise`, `for … in`) or an assignment right-hand side, and a
3835    /// lambda body is a single expression, so it can contain none of
3836    /// them. In both fixtures the `expression_list` sits directly above
3837    /// the `boolean_operator` and stops the enclosing-lambda walk before
3838    /// the `lambda` is counted, leaving the +1 boolean sequence alone.
3839    ///
3840    /// Measured, not derived: deleting only the `ExpressionList` arm
3841    /// takes both fixtures from 1 to 2, while the doubly-nested lambda
3842    /// in `python_boolean_in_lambda_scores_under_each_enclosing_statement`
3843    /// stays at 3 (#1090). Whether 1 or 2 is the *right* score is a
3844    /// separate question — this pins current behaviour, and the
3845    /// per-lambda surcharge itself is under review in #1150.
3846    #[test]
3847    fn python_boolean_in_expression_list_under_lambda() {
3848        use crate::test_support::metrics_verbatim;
3849
3850        for (route, source) in [
3851            ("parenthesised yield", "k = lambda q: (yield a and b, c)\n"),
3852            (
3853                "f-string interpolation",
3854                "m = lambda q: f\"{a and b, c}\"\n",
3855            ),
3856        ] {
3857            let metrics = metrics_verbatim(
3858                crate::LANG::Python,
3859                source.as_bytes(),
3860                MetricsOptions::default(),
3861            );
3862
3863            assert_eq!(
3864                metrics.cognitive.cognitive_sum(),
3865                1,
3866                "{route}: +1 boolean sequence only — the `expression_list` \
3867                 stops the enclosing-lambda walk before it reaches the `lambda`"
3868            );
3869        }
3870    }
3871
3872    #[test]
3873    fn python_nested_functions_lambdas() {
3874        check_metrics::<PythonParser>(
3875            "def f(a, b):
3876                 def foo(a):
3877                     if a:  # +2 (+1 nesting)
3878                         return 1
3879                 # +3 (+1 for boolean sequence +2 for lambda nesting)
3880                 bar = lambda a: lambda b: b or True or True
3881                 return bar(foo(a))(a)",
3882            "foo.py",
3883            |metric| {
3884                // 2 functions + 2 lambdas = 4
3885                insta::assert_json_snapshot!(
3886                    metric.cognitive,
3887                    @r#"
3888                {
3889                  "sum": 5,
3890                  "value": 0,
3891                  "average": 1.25,
3892                  "min": 0,
3893                  "max": 3
3894                }
3895                "#
3896                );
3897            },
3898        );
3899    }
3900
3901    /// #1149: a `def` nested inside a conditional is scored against its
3902    /// own depth, not the enclosing function's.
3903    ///
3904    /// Python was the only language with a syntactic function-definition
3905    /// node that never reset `nesting.conditional` at the boundary, so
3906    /// `inner` charged base(1) + inherited-conditional(1) +
3907    /// function-depth(1) = 3 where every sibling charges base(1) +
3908    /// function-depth(1) = 2. `python_nested_functions_lambdas` missed it
3909    /// because its nested `def` sits at function top level, where
3910    /// `conditional` is already 0.
3911    ///
3912    /// The Java companion is the byte-equivalent construct — Java has no
3913    /// local function, so a method reaches the inside of an `if` only
3914    /// through a class body declared there — and pins the book's
3915    /// "byte-equivalent constructs therefore score identically across
3916    /// languages" claim with a test rather than prose.
3917    ///
3918    /// Both fixtures nest the definition **two** conditionals deep, not
3919    /// one. At one level the Java assertion cannot discriminate: reset +
3920    /// depth-surcharge and no-reset + no-surcharge both yield 2, so
3921    /// deleting both lines from `cognitive/java.rs` leaves it green. At
3922    /// two levels the correct answer stays 2 while an unreset
3923    /// implementation gives 4 (Python, which also bumps depth) or 3
3924    /// (depth dropped as well).
3925    #[test]
3926    fn python_nested_def_inside_conditional_scores_like_java() {
3927        fn cognitive_of(space: &FuncSpace, name: &str) -> u64 {
3928            function_space(space, name).metrics.cognitive.cognitive()
3929        }
3930
3931        check_func_space::<PythonParser, _>(
3932            "def outer(a, b, c):
3933                 if a:  # +1
3934                     if b:  # +2 (+1 nesting)
3935                         def inner(c):
3936                             if c:  # +1 base, +1 function depth, +0 inherited
3937                                 return 1
3938                         return inner",
3939            "nested.py",
3940            |space| {
3941                assert_eq!(cognitive_of(&space, "outer"), 3, "python outer");
3942                assert_eq!(cognitive_of(&space, "inner"), 2, "python inner");
3943            },
3944        );
3945
3946        check_func_space::<JavaParser, _>(
3947            "class N {
3948                 int outer(boolean a, boolean b, boolean c) {
3949                     if (a) {  // +1
3950                         if (b) {  // +2 (+1 nesting)
3951                             class I {
3952                                 int inner(boolean c) {
3953                                     if (c) {  // +1 base, +1 function depth
3954                                         return 1;
3955                                     }
3956                                     return 0;
3957                                 }
3958                             }
3959                         }
3960                     }
3961                     return 0;
3962                 }
3963             }",
3964            "N.java",
3965            |space| {
3966                assert_eq!(cognitive_of(&space, "outer"), 3, "java outer");
3967                assert_eq!(cognitive_of(&space, "inner"), 2, "java inner");
3968            },
3969        );
3970    }
3971
3972    #[test]
3973    fn python_real_function() {
3974        check_metrics::<PythonParser>(
3975            "def process_raw_constant(constant, min_word_length):
3976                 processed_words = []
3977                 raw_camelcase_words = []
3978                 for raw_word in re.findall(r'[a-z]+', constant):  # +1
3979                     word = raw_word.strip()
3980                         if (  # +2 (+1 if and +1 nesting)
3981                             len(word) >= min_word_length
3982                             and not (word.startswith('-') or word.endswith('-')) # +2 operators
3983                         ):
3984                             if is_camel_case_word(word):  # +3 (+1 if and +2 nesting)
3985                                 raw_camelcase_words.append(word)
3986                             else: # +1 else
3987                                 processed_words.append(word.lower())
3988                 return processed_words, raw_camelcase_words",
3989            "foo.py",
3990            |metric| {
3991                insta::assert_json_snapshot!(
3992                    metric.cognitive,
3993                    @r#"
3994                {
3995                  "sum": 9,
3996                  "value": 0,
3997                  "average": 9.0,
3998                  "min": 0,
3999                  "max": 9
4000                }
4001                "#
4002                );
4003            },
4004        );
4005    }
4006
4007    #[test]
4008    fn rust_if_let_else_if_else() {
4009        check_metrics::<RustParser>(
4010            "pub fn create_usage_no_title(p: &Parser, used: &[&str]) -> String {
4011                 debugln!(\"usage::create_usage_no_title;\");
4012                 if let Some(u) = p.meta.usage_str { // +1
4013                     String::from(&*u)
4014                 } else if used.is_empty() { // +1
4015                     create_help_usage(p, true)
4016                 } else { // +1
4017                     create_smart_usage(p, used)
4018                }
4019            }",
4020            "foo.rs",
4021            |metric| {
4022                insta::assert_json_snapshot!(
4023                    metric.cognitive,
4024                    @r#"
4025                {
4026                  "sum": 3,
4027                  "value": 0,
4028                  "average": 3.0,
4029                  "min": 0,
4030                  "max": 3
4031                }
4032                "#
4033                );
4034            },
4035        );
4036    }
4037
4038    #[test]
4039    fn typescript_if_else_if_else() {
4040        check_metrics::<TypescriptParser>(
4041            "function foo() {
4042                 if (this._closed) return Promise.resolve(); // +1
4043                 if (this._tempDirectory) { // +1
4044                     this.kill();
4045                 } else if (this.connection) { // +1
4046                     this.kill();
4047                 } else { // +1
4048                     throw new Error(`Error`);
4049                }
4050                helper.removeEventListeners(this._listeners);
4051                return this._processClosing;
4052            }",
4053            "foo.ts",
4054            |metric| {
4055                insta::assert_json_snapshot!(
4056                    metric.cognitive,
4057                    @r#"
4058                {
4059                  "sum": 4,
4060                  "value": 0,
4061                  "average": 4.0,
4062                  "min": 0,
4063                  "max": 4
4064                }
4065                "#
4066                );
4067            },
4068        );
4069    }
4070
4071    #[test]
4072    fn java_no_cognitive() {
4073        check_metrics::<JavaParser>("int a = 42;", "foo.java", |metric| {
4074            insta::assert_json_snapshot!(
4075                metric.cognitive,
4076                @r#"
4077            {
4078              "sum": 0,
4079              "value": 0,
4080              "average": 0.0,
4081              "min": 0,
4082              "max": 0
4083            }
4084            "#
4085            );
4086        });
4087    }
4088
4089    #[test]
4090    fn java_single_branch_function() {
4091        check_metrics::<JavaParser>(
4092            "class X {
4093                public static void print(boolean a){  
4094                if(a){ // +1
4095                  System.out.println(\"test1\");
4096                }
4097              }
4098            }",
4099            "foo.java",
4100            |metric| {
4101                insta::assert_json_snapshot!(
4102                    metric.cognitive,
4103                    @r#"
4104                {
4105                  "sum": 1,
4106                  "value": 0,
4107                  "average": 1.0,
4108                  "min": 0,
4109                  "max": 1
4110                }
4111                "#
4112                );
4113            },
4114        );
4115    }
4116
4117    #[test]
4118    fn java_multiple_branch_function() {
4119        check_metrics::<JavaParser>(
4120            "class X {
4121              public static void print(boolean a, boolean b){  
4122                if(a){ // +1
4123                  System.out.println(\"test1\");
4124                }
4125                if(b){ // +1
4126                  System.out.println(\"test2\");
4127                }
4128                else { // +1
4129                  System.out.println(\"test3\");
4130                }
4131              }
4132            }",
4133            "foo.java",
4134            |metric| {
4135                insta::assert_json_snapshot!(
4136                    metric.cognitive,
4137                    @r#"
4138                {
4139                  "sum": 3,
4140                  "value": 0,
4141                  "average": 3.0,
4142                  "min": 0,
4143                  "max": 3
4144                }
4145                "#
4146                );
4147            },
4148        );
4149    }
4150
4151    #[test]
4152    fn java_compound_conditions() {
4153        check_metrics::<JavaParser>(
4154            "class X {
4155              public static void print(boolean a, boolean b, boolean c, boolean d){  
4156                if(a && b){ // +2 (+1 &&)
4157                  System.out.println(\"test1\");
4158                }
4159                if(c && d){ // +2 (+1 &&)
4160                  System.out.println(\"test2\");
4161                }
4162              }
4163            }",
4164            "foo.java",
4165            |metric| {
4166                insta::assert_json_snapshot!(
4167                    metric.cognitive,
4168                    @r#"
4169                {
4170                  "sum": 4,
4171                  "value": 0,
4172                  "average": 4.0,
4173                  "min": 0,
4174                  "max": 4
4175                }
4176                "#
4177                );
4178            },
4179        );
4180    }
4181
4182    #[test]
4183    fn java_switch_statement() {
4184        check_metrics::<JavaParser>(
4185            "class X {
4186              public static void print(boolean a, boolean b, boolean c, boolean d){
4187                switch(expr){ //+1
4188                  case 1:
4189                    System.out.println(\"test1\");
4190                    break;
4191                  case 2:
4192                    System.out.println(\"test2\");
4193                    break;
4194                  default:
4195                    System.out.println(\"test\");
4196                }
4197              }
4198            }",
4199            "foo.java",
4200            |metric| {
4201                insta::assert_json_snapshot!(
4202                    metric.cognitive,
4203                    @r#"
4204                {
4205                  "sum": 1,
4206                  "value": 0,
4207                  "average": 1.0,
4208                  "min": 0,
4209                  "max": 1
4210                }
4211                "#
4212                );
4213            },
4214        );
4215    }
4216
4217    #[test]
4218    fn java_switch_expression() {
4219        check_metrics::<JavaParser>(
4220            "class X {
4221              public static void print(boolean a, boolean b, boolean c, boolean d){
4222                switch(expr){ // +1
4223                  case 1 -> System.out.println(\"test1\");
4224                  case 2 -> System.out.println(\"test2\");
4225                  default -> System.out.println(\"test\");
4226                }
4227              }
4228            }",
4229            "foo.java",
4230            |metric| {
4231                insta::assert_json_snapshot!(
4232                    metric.cognitive,
4233                    @r#"
4234                {
4235                  "sum": 1,
4236                  "value": 0,
4237                  "average": 1.0,
4238                  "min": 0,
4239                  "max": 1
4240                }
4241                "#
4242                );
4243            },
4244        );
4245    }
4246
4247    #[test]
4248    fn java_not_booleans() {
4249        // `!` does not break boolean sequences (issue #392): pre-order
4250        // visits the outer `&&` BinaryExpression first; the inner `&&`
4251        // lies within that span and is a continuation, not a new
4252        // sequence.
4253        check_metrics::<JavaParser>(
4254            "class X {
4255              public static void print(boolean a, boolean b, boolean c, boolean d){
4256                if (a && !(b && c)) { // +2 (+1 if, +1 outer &&; inner && continues)
4257                  printf(\"test\");
4258                }
4259              }
4260            }",
4261            "foo.java",
4262            |metric| {
4263                insta::assert_json_snapshot!(
4264                    metric.cognitive,
4265                    @r#"
4266                {
4267                  "sum": 2,
4268                  "value": 0,
4269                  "average": 2.0,
4270                  "min": 0,
4271                  "max": 2
4272                }
4273                "#
4274                );
4275            },
4276        );
4277    }
4278
4279    #[test]
4280    fn java_enhanced_for_statement() {
4281        check_metrics::<JavaParser>(
4282            "class X {
4283              public static int sum(int[] xs) {
4284                int s = 0;
4285                for (int x : xs) { // +1
4286                  s += x;
4287                }
4288                return s;
4289              }
4290            }",
4291            "foo.java",
4292            |metric| {
4293                // Java's enhanced-for `for (T x : c)` parses as
4294                // `enhanced_for_statement`; it is a control-flow construct
4295                // and counts the same as a classic `for_statement` → +1.
4296                assert_eq!(metric.cognitive.cognitive_sum(), 1);
4297                assert_eq!(metric.cognitive.cognitive_max(), 1);
4298                insta::assert_json_snapshot!(
4299                    metric.cognitive,
4300                    @r#"
4301                {
4302                  "sum": 1,
4303                  "value": 0,
4304                  "average": 1.0,
4305                  "min": 0,
4306                  "max": 1
4307                }
4308                "#
4309                );
4310            },
4311        );
4312    }
4313
4314    #[test]
4315    fn java_nested_enhanced_for_statement() {
4316        check_metrics::<JavaParser>(
4317            "class X {
4318              public static void f(int[][] xss) {
4319                for (int[] xs : xss) { // +1
4320                  for (int x : xs) { // +2 (nesting = 1)
4321                    g(x);
4322                  }
4323                }
4324              }
4325            }",
4326            "foo.java",
4327            |metric| {
4328                // Nested enhanced-fors compound by nesting, matching the
4329                // behaviour of nested classic `for` loops: 1 + 2 = 3.
4330                assert_eq!(metric.cognitive.cognitive_sum(), 3);
4331                assert_eq!(metric.cognitive.cognitive_max(), 3);
4332                insta::assert_json_snapshot!(
4333                    metric.cognitive,
4334                    @r#"
4335                {
4336                  "sum": 3,
4337                  "value": 0,
4338                  "average": 3.0,
4339                  "min": 0,
4340                  "max": 3
4341                }
4342                "#
4343                );
4344            },
4345        );
4346    }
4347
4348    #[test]
4349    fn java_ternary() {
4350        // Java's ternary `?:` (grammar `ternary_expression`) is a
4351        // conditional construct: +1 base + nesting, matching the
4352        // SonarSource Cognitive Complexity §2 rule and the C++/JS
4353        // siblings.
4354        check_metrics::<JavaParser>(
4355            "class X {
4356              public static boolean check(int a) {
4357                  return a > 0 ? true : false; // +1
4358              }
4359            }",
4360            "foo.java",
4361            |metric| {
4362                assert_eq!(metric.cognitive.cognitive_sum(), 1);
4363                assert_eq!(metric.cognitive.cognitive_max(), 1);
4364                insta::assert_json_snapshot!(
4365                    metric.cognitive,
4366                    @r#"
4367                {
4368                  "sum": 1,
4369                  "value": 0,
4370                  "average": 1.0,
4371                  "min": 0,
4372                  "max": 1
4373                }
4374                "#
4375                );
4376            },
4377        );
4378    }
4379
4380    #[test]
4381    fn java_nested_ternary() {
4382        // Nested ternaries inside an `if` block compound by nesting,
4383        // matching the C++ regression test for issue #172.
4384        // expected: if (+1, nesting=0) + outer ternary (+1+1=+2,
4385        // nesting=1) + inner ternary (+1+2=+3, nesting=2) = 6.
4386        check_metrics::<JavaParser>(
4387            "class X {
4388              public static String classify(int a, int b) {
4389                  if (a > 0) { // +1
4390                      return b > 0 ? (b > 10 ? \"big\" : \"small\") : \"neg\"; // +2, +3
4391                  }
4392                  return \"zero\";
4393              }
4394            }",
4395            "foo.java",
4396            |metric| {
4397                assert_eq!(metric.cognitive.cognitive_sum(), 6);
4398                assert_eq!(metric.cognitive.cognitive_max(), 6);
4399                insta::assert_json_snapshot!(
4400                    metric.cognitive,
4401                    @r#"
4402                {
4403                  "sum": 6,
4404                  "value": 0,
4405                  "average": 6.0,
4406                  "min": 0,
4407                  "max": 6
4408                }
4409                "#
4410                );
4411            },
4412        );
4413    }
4414
4415    #[test]
4416    fn java_nested_method_resets_nesting_and_adds_depth() {
4417        // Regression for #696: a local-class method declared two `if`s deep
4418        // inside an outer method must NOT inherit the enclosing nesting. The
4419        // method-declaration boundary resets nesting to 0 and bumps the
4420        // function-depth surcharge by 1 (it is nested inside `outer`).
4421        //
4422        // expected: outer `if` (+1, nesting=0) + inner `if` (+2, nesting=1)
4423        // + Local.f's `if` (+1 base + 1 depth = +2, nesting=0, depth=1) = 5.
4424        // Before the fix, `f` inherited nesting=2 from the two enclosing
4425        // `if`s, scoring its inner `if` at nesting 2 (+3) for a sum of 6.
4426        // The two-deep nesting is load-bearing: at one level deep the
4427        // inherited nesting (1) coincidentally equals the depth bump (1) so
4428        // the bug is invisible.
4429        check_metrics::<JavaParser>(
4430            "class Outer {
4431                void outer(boolean a) {
4432                    if (a) {
4433                        if (a) {
4434                            class Local {
4435                                void f(boolean b) {
4436                                    if (b) { g(); }
4437                                }
4438                            }
4439                        }
4440                    }
4441                }
4442            }",
4443            "foo.java",
4444            |metric| {
4445                assert_eq!(metric.cognitive.cognitive_sum(), 5);
4446                assert_eq!(metric.cognitive.cognitive_max(), 3);
4447            },
4448        );
4449    }
4450
4451    /// Regression for #1160: a record's compact constructor is its own
4452    /// grammar kind (`compact_constructor_declaration`), which was absent
4453    /// from `is_func`, `get_space_kind`, and the boundary arm in
4454    /// `cognitive/java.rs`. It therefore opened no function space and its
4455    /// control flow was charged to the enclosing class, so `bca check`
4456    /// could never flag one however complex it got.
4457    ///
4458    /// expected: each `if` is +1 at nesting 0, so `function R` scores 2
4459    /// while `class R` scores 0 of its own. Both halves are asserted:
4460    /// checking only the new space would still pass if the class kept a
4461    /// duplicate count of the same two branches.
4462    #[test]
4463    fn java_record_compact_constructor_opens_function_space() {
4464        check_func_space::<JavaParser, _>(
4465            "record R(int a, int b) {
4466                 R {
4467                     if (a < 0) { throw new IllegalArgumentException(); }
4468                     if (b < 0) { throw new IllegalArgumentException(); }
4469                 }
4470                 int sum() { return a + b; }
4471             }",
4472            "R.java",
4473            |space| {
4474                let class = child_space(&space, "R");
4475                assert_eq!(class.kind, SpaceKind::Class, "record opens a class space");
4476                assert_eq!(class.metrics.cognitive.cognitive(), 0, "class R own score");
4477                // Also pins the space name: the compact form carries a
4478                // `name` field holding the record's simple name, so the
4479                // default `get_func_space_name` reports `R` rather than
4480                // `<anonymous>`.
4481                assert_eq!(
4482                    function_space(&space, "R").metrics.cognitive.cognitive(),
4483                    2,
4484                    "compact constructor own score",
4485                );
4486            },
4487        );
4488    }
4489
4490    /// The compact constructor is a *function boundary*, not merely a
4491    /// space: #1160 added it to the arm that resets structural nesting and
4492    /// to the `stops` set behind the function-depth surcharge. The
4493    /// reproducer above cannot see either — at nesting 0 with no enclosing
4494    /// function both lines are no-ops — so each gets a fixture that only
4495    /// it can satisfy.
4496    ///
4497    /// The first nests a *local record* (Java 16+) two conditionals deep,
4498    /// per the two-level rule in
4499    /// `java_nested_method_resets_nesting_and_adds_depth`: at one level
4500    /// the reset and the surcharge cancel out.
4501    /// expected: outer `if` +1, inner `if` +2, the compact constructor's
4502    /// `if` +1 base +1 depth (it is lexically inside `m`) = 5. Without the
4503    /// reset the last `if` inherits nesting 2 and scores +3, for 6.
4504    ///
4505    /// The second inverts the nesting: a local class method inside a
4506    /// compact constructor. Only the `stops` entry makes the constructor
4507    /// count as `f`'s enclosing function.
4508    /// expected: `f`'s `if` is +1 base +1 depth = 2. Without the `stops`
4509    /// entry the surcharge is 0 and it scores 1.
4510    #[test]
4511    fn java_record_compact_constructor_is_a_function_boundary() {
4512        check_func_space::<JavaParser, _>(
4513            "class C {
4514                 void m(boolean f) {
4515                     if (f) {
4516                         if (f) {
4517                             record R(int a) {
4518                                 R {
4519                                     if (a < 0) { throw new IllegalArgumentException(); }
4520                                 }
4521                             }
4522                         }
4523                     }
4524                 }
4525             }",
4526            "C.java",
4527            |space| {
4528                assert_eq!(
4529                    space.metrics.cognitive.cognitive_sum(),
4530                    5,
4531                    "local record's compact constructor restarts nesting",
4532                );
4533                assert_eq!(
4534                    function_space(&space, "R").metrics.cognitive.cognitive(),
4535                    2,
4536                    "compact constructor: +1 base, +1 function depth",
4537                );
4538            },
4539        );
4540
4541        check_func_space::<JavaParser, _>(
4542            "record R(int a) {
4543                 R {
4544                     class L {
4545                         void f(boolean b) {
4546                             if (b) { g(); }
4547                         }
4548                     }
4549                 }
4550             }",
4551            "R.java",
4552            |space| {
4553                assert_eq!(
4554                    function_space(&space, "f").metrics.cognitive.cognitive(),
4555                    2,
4556                    "a compact constructor is `f`'s enclosing function",
4557                );
4558            },
4559        );
4560    }
4561
4562    #[test]
4563    fn java_labeled_break_continue() {
4564        // Per SonarSource Cognitive Complexity §B2 (issue #225), labeled
4565        // `break LABEL` and `continue LABEL` each add +1 because they break
4566        // structured control flow. Mirrors `go_labeled_break_continue` and
4567        // `rust_break_continue_labeled`.
4568        // expected: outer for (+1, nesting=0) + inner for (+2, nesting=1)
4569        // + if (+3, nesting=2) + continue outer (+1)
4570        // + if (+3, nesting=2) + break outer (+1) = 11.
4571        check_metrics::<JavaParser>(
4572            "class X {
4573                void scan(int[][] m) {
4574                    outer:
4575                    for (int i = 0; i < m.length; i++) {        // +1
4576                        for (int j = 0; j < m[i].length; j++) {  // +2
4577                            if (m[i][j] < 0) continue outer;     // +3, +1
4578                            if (m[i][j] > 100) break outer;      // +3, +1
4579                        }
4580                    }
4581                }
4582            }",
4583            "foo.java",
4584            |metric| {
4585                assert_eq!(metric.cognitive.cognitive_sum(), 11);
4586                assert_eq!(metric.cognitive.cognitive_max(), 11);
4587                insta::assert_json_snapshot!(
4588                    metric.cognitive,
4589                    @r#"
4590                {
4591                  "sum": 11,
4592                  "value": 0,
4593                  "average": 11.0,
4594                  "min": 0,
4595                  "max": 11
4596                }
4597                "#
4598                );
4599            },
4600        );
4601    }
4602
4603    #[test]
4604    fn java_unlabeled_break_continue_not_counted() {
4605        // Negative test for issue #225: plain `break;` / `continue;` are
4606        // *not* unstructured jumps under SonarSource Cognitive Complexity
4607        // §B2 and must add 0. Only the surrounding `for` + `if` contribute.
4608        // expected: for (+1) + if (+2) + if (+2) = 5.
4609        check_metrics::<JavaParser>(
4610            "class X {
4611                void scan(int[] m) {
4612                    for (int i = 0; i < m.length; i++) {  // +1
4613                        if (m[i] < 0) continue;            // +2, +0
4614                        if (m[i] > 100) break;             // +2, +0
4615                    }
4616                }
4617            }",
4618            "foo.java",
4619            |metric| {
4620                assert_eq!(metric.cognitive.cognitive_sum(), 5);
4621                assert_eq!(metric.cognitive.cognitive_max(), 5);
4622                insta::assert_json_snapshot!(
4623                    metric.cognitive,
4624                    @r#"
4625                {
4626                  "sum": 5,
4627                  "value": 0,
4628                  "average": 5.0,
4629                  "min": 0,
4630                  "max": 5
4631                }
4632                "#
4633                );
4634            },
4635        );
4636    }
4637
4638    #[test]
4639    fn csharp_no_cognitive() {
4640        check_metrics::<CsharpParser>("int a = 42;", "foo.cs", |metric| {
4641            insta::assert_json_snapshot!(
4642                metric.cognitive,
4643                @r#"
4644            {
4645              "sum": 0,
4646              "value": 0,
4647              "average": 0.0,
4648              "min": 0,
4649              "max": 0
4650            }
4651            "#
4652            );
4653        });
4654    }
4655
4656    #[test]
4657    fn csharp_single_branch_function() {
4658        check_metrics::<CsharpParser>(
4659            "class X {
4660                public static void Print(bool a) {
4661                    if (a) {
4662                        System.Console.WriteLine(\"test1\");
4663                    }
4664                }
4665            }",
4666            "foo.cs",
4667            |metric| {
4668                // Single `if` at nesting 0 → +1.
4669                assert_eq!(metric.cognitive.cognitive_sum(), 1);
4670                assert_eq!(metric.cognitive.cognitive_max(), 1);
4671                insta::assert_json_snapshot!(metric.cognitive);
4672            },
4673        );
4674    }
4675
4676    #[test]
4677    fn csharp_multiple_branch_function() {
4678        check_metrics::<CsharpParser>(
4679            "class X {
4680                public static void Print(bool a, bool b) {
4681                    if (a) {
4682                        System.Console.WriteLine(\"test1\");
4683                    }
4684                    if (b) {
4685                        System.Console.WriteLine(\"test2\");
4686                    } else {
4687                        System.Console.WriteLine(\"test3\");
4688                    }
4689                }
4690            }",
4691            "foo.cs",
4692            |metric| {
4693                // First `if` +1, second `if` +1, `else` +1 → 3.
4694                assert_eq!(metric.cognitive.cognitive_sum(), 3);
4695                assert_eq!(metric.cognitive.cognitive_max(), 3);
4696                insta::assert_json_snapshot!(metric.cognitive);
4697            },
4698        );
4699    }
4700
4701    #[test]
4702    fn csharp_compound_conditions() {
4703        check_metrics::<CsharpParser>(
4704            "class X {
4705                public static void Print(bool a, bool b, bool c, bool d) {
4706                    if (a && b) {
4707                        System.Console.WriteLine(\"test1\");
4708                    }
4709                    if (c && d) {
4710                        System.Console.WriteLine(\"test2\");
4711                    }
4712                }
4713            }",
4714            "foo.cs",
4715            |metric| {
4716                // Two ifs (+1 each) + two `&&` (+1 each, fresh chain per if) = 4.
4717                assert_eq!(metric.cognitive.cognitive_sum(), 4);
4718                assert_eq!(metric.cognitive.cognitive_max(), 4);
4719                insta::assert_json_snapshot!(metric.cognitive);
4720            },
4721        );
4722    }
4723
4724    #[test]
4725    fn csharp_switch_statement() {
4726        check_metrics::<CsharpParser>(
4727            "class X {
4728                public static void Print(int expr) {
4729                    switch (expr) {
4730                        case 1:
4731                            System.Console.WriteLine(\"test1\");
4732                            break;
4733                        case 2:
4734                            System.Console.WriteLine(\"test2\");
4735                            break;
4736                        default:
4737                            System.Console.WriteLine(\"test\");
4738                            break;
4739                    }
4740                }
4741            }",
4742            "foo.cs",
4743            |metric| {
4744                // Single `switch` +1; cases / default do not increment.
4745                assert_eq!(metric.cognitive.cognitive_sum(), 1);
4746                assert_eq!(metric.cognitive.cognitive_max(), 1);
4747                insta::assert_json_snapshot!(metric.cognitive);
4748            },
4749        );
4750    }
4751
4752    #[test]
4753    fn csharp_switch_expression() {
4754        check_metrics::<CsharpParser>(
4755            "class X {
4756                public static string Name(int expr) =>
4757                    expr switch {
4758                        1 => \"one\",
4759                        2 => \"two\",
4760                        _ => \"other\"
4761                    };
4762            }",
4763            "foo.cs",
4764            |metric| {
4765                // `switch` expression +1; arms do not increment.
4766                assert_eq!(metric.cognitive.cognitive_sum(), 1);
4767                assert_eq!(metric.cognitive.cognitive_max(), 1);
4768                insta::assert_json_snapshot!(metric.cognitive);
4769            },
4770        );
4771    }
4772
4773    #[test]
4774    fn csharp_not_booleans() {
4775        // `!` does not break boolean sequences (issue #392): pre-order
4776        // visits the outer `&&` BinaryExpression first, so the inner
4777        // `&&` lies within its span and is a continuation.
4778        check_metrics::<CsharpParser>(
4779            "class X {
4780                public static void Print(bool a, bool b, bool c) {
4781                    if (a && !(b && c)) {
4782                        System.Console.WriteLine(\"test\");
4783                    }
4784                }
4785            }",
4786            "foo.cs",
4787            |metric| {
4788                // `if` +1, outer `&&` +1, inner `&&` continues outer span → 2.
4789                assert_eq!(metric.cognitive.cognitive_sum(), 2);
4790                assert_eq!(metric.cognitive.cognitive_max(), 2);
4791                insta::assert_json_snapshot!(metric.cognitive);
4792            },
4793        );
4794    }
4795
4796    #[test]
4797    fn csharp_ternary() {
4798        // C#'s ternary `?:` (grammar `conditional_expression`) is a
4799        // conditional construct: +1 base + nesting. Regression test for
4800        // issue #224.
4801        check_metrics::<CsharpParser>(
4802            "class X {
4803                public static bool Check(int a) {
4804                    return a > 0 ? true : false; // +1
4805                }
4806            }",
4807            "foo.cs",
4808            |metric| {
4809                assert_eq!(metric.cognitive.cognitive_sum(), 1);
4810                assert_eq!(metric.cognitive.cognitive_max(), 1);
4811                insta::assert_json_snapshot!(
4812                    metric.cognitive,
4813                    @r#"
4814                {
4815                  "sum": 1,
4816                  "value": 0,
4817                  "average": 1.0,
4818                  "min": 0,
4819                  "max": 1
4820                }
4821                "#
4822                );
4823            },
4824        );
4825    }
4826
4827    #[test]
4828    fn csharp_nested_ternary() {
4829        // Nested ternaries inside an `if` compound by nesting (mirrors
4830        // the C++ regression test for #172).
4831        // expected: if (+1) + outer ternary (+2, nesting=1) + inner
4832        // ternary (+3, nesting=2) = 6.
4833        check_metrics::<CsharpParser>(
4834            "class X {
4835                public static string Classify(int a, int b) {
4836                    if (a > 0) { // +1
4837                        return b > 0 ? (b > 10 ? \"big\" : \"small\") : \"neg\"; // +2, +3
4838                    }
4839                    return \"zero\";
4840                }
4841            }",
4842            "foo.cs",
4843            |metric| {
4844                assert_eq!(metric.cognitive.cognitive_sum(), 6);
4845                assert_eq!(metric.cognitive.cognitive_max(), 6);
4846                insta::assert_json_snapshot!(
4847                    metric.cognitive,
4848                    @r#"
4849                {
4850                  "sum": 6,
4851                  "value": 0,
4852                  "average": 6.0,
4853                  "min": 0,
4854                  "max": 6
4855                }
4856                "#
4857                );
4858            },
4859        );
4860    }
4861
4862    #[test]
4863    fn csharp_local_function_in_if_does_not_inherit_nesting() {
4864        // Regression for #696 (the acute C# case): a `local_function_statement`
4865        // declared two `if`s deep must reset nesting to 0 and gain a
4866        // function-depth surcharge — not inherit `nesting = 2` from the
4867        // enclosing `if`s. C# has dedicated `LocalFunctionStatement(342)` /
4868        // `LocalFunctionDeclaration(343)` nodes that previously went
4869        // unhandled by the cognitive walker.
4870        //
4871        // expected: outer `if` (+1, nesting=0) + inner `if` (+2, nesting=1)
4872        // + Local's `if` (+1 base + 1 depth = +2, nesting=0, depth=1) = 5.
4873        // Before the fix, `Local` inherited nesting=2, scoring its inner
4874        // `if` at nesting 2 (+3) for a sum of 6. The two-deep nesting is
4875        // load-bearing: one level deep, the inherited nesting (1)
4876        // coincidentally equals the depth bump (1) and the bug is invisible.
4877        check_metrics::<CsharpParser>(
4878            "class C {
4879                void Outer(bool flag) {
4880                    if (flag) {
4881                        if (flag) {
4882                            void Local() {
4883                                if (flag) {
4884                                    System.Console.WriteLine(\"x\");
4885                                }
4886                            }
4887                            Local();
4888                        }
4889                    }
4890                }
4891            }",
4892            "foo.cs",
4893            |metric| {
4894                assert_eq!(metric.cognitive.cognitive_sum(), 5);
4895                assert_eq!(metric.cognitive.cognitive_max(), 3);
4896            },
4897        );
4898    }
4899
4900    #[test]
4901    fn csharp_goto_statement() {
4902        // Per SonarSource Cognitive Complexity §B2 (issue #225), any `goto`
4903        // is an unstructured jump and adds +1. Mirrors C++'s `GotoStatement`
4904        // and Go's `GotoStatement` handling.
4905        // expected: if (+1, nesting=0) + goto neg (+1) = 2.
4906        check_metrics::<CsharpParser>(
4907            "class X {
4908                int Classify(int x) {
4909                    if (x < 0) goto neg;  // +1, +1
4910                    return x;
4911                    neg:
4912                    return -x;
4913                }
4914            }",
4915            "foo.cs",
4916            |metric| {
4917                assert_eq!(metric.cognitive.cognitive_sum(), 2);
4918                assert_eq!(metric.cognitive.cognitive_max(), 2);
4919                insta::assert_json_snapshot!(
4920                    metric.cognitive,
4921                    @r#"
4922                {
4923                  "sum": 2,
4924                  "value": 0,
4925                  "average": 2.0,
4926                  "min": 0,
4927                  "max": 2
4928                }
4929                "#
4930                );
4931            },
4932        );
4933    }
4934
4935    #[test]
4936    fn csharp_goto_case_and_default() {
4937        // `goto case` and `goto default` inside a `switch` are also
4938        // unstructured jumps (+1 each) per SonarSource §B2.
4939        // expected: switch (+1, nesting=0) + goto case 2 (+1)
4940        // + goto default (+1) = 3.
4941        check_metrics::<CsharpParser>(
4942            "class X {
4943                int Walk(int x) {
4944                    switch (x) {  // +1
4945                        case 1: goto case 2;     // +1
4946                        case 2: return 2;
4947                        case 3: goto default;    // +1
4948                        default: return 0;
4949                    }
4950                }
4951            }",
4952            "foo.cs",
4953            |metric| {
4954                assert_eq!(metric.cognitive.cognitive_sum(), 3);
4955                assert_eq!(metric.cognitive.cognitive_max(), 3);
4956                insta::assert_json_snapshot!(
4957                    metric.cognitive,
4958                    @r#"
4959                {
4960                  "sum": 3,
4961                  "value": 0,
4962                  "average": 3.0,
4963                  "min": 0,
4964                  "max": 3
4965                }
4966                "#
4967                );
4968            },
4969        );
4970    }
4971
4972    #[test]
4973    fn csharp_unlabeled_break_not_counted() {
4974        // Negative test for issue #225: C#'s grammar does not allow
4975        // labeled `break`/`continue` (those are syntactically rejected),
4976        // and plain `break;` / `continue;` are not unstructured jumps under
4977        // SonarSource §B2 — they must add 0. Only the `for` + `if`
4978        // contribute.
4979        // expected: for (+1) + if (+2) = 3.
4980        check_metrics::<CsharpParser>(
4981            "class X {
4982                void Scan(int[] m) {
4983                    for (int i = 0; i < m.Length; i++) {  // +1
4984                        if (m[i] < 0) break;               // +2, +0
4985                    }
4986                }
4987            }",
4988            "foo.cs",
4989            |metric| {
4990                assert_eq!(metric.cognitive.cognitive_sum(), 3);
4991                assert_eq!(metric.cognitive.cognitive_max(), 3);
4992                insta::assert_json_snapshot!(
4993                    metric.cognitive,
4994                    @r#"
4995                {
4996                  "sum": 3,
4997                  "value": 0,
4998                  "average": 3.0,
4999                  "min": 0,
5000                  "max": 3
5001                }
5002                "#
5003                );
5004            },
5005        );
5006    }
5007
5008    #[test]
5009    fn perl_no_cognitive() {
5010        check_metrics::<PerlParser>("my $a = 42;", "foo.pl", |metric| {
5011            insta::assert_json_snapshot!(metric.cognitive, @r#"
5012            {
5013              "sum": 0,
5014              "value": 0,
5015              "average": 0.0,
5016              "min": 0,
5017              "max": 0
5018            }
5019            "#);
5020        });
5021    }
5022
5023    #[test]
5024    fn perl_simple_function() {
5025        check_metrics::<PerlParser>(
5026            "sub f {
5027                return 1;
5028            }",
5029            "foo.pl",
5030            |metric| {
5031                insta::assert_json_snapshot!(metric.cognitive, @r#"
5032                {
5033                  "sum": 0,
5034                  "value": 0,
5035                  "average": 0.0,
5036                  "min": 0,
5037                  "max": 0
5038                }
5039                "#);
5040            },
5041        );
5042    }
5043
5044    #[test]
5045    fn perl_sequence_same_booleans() {
5046        check_metrics::<PerlParser>(
5047            "sub f {
5048                if ($a && $b && $c) { # +1 if, +1 first &&-chain
5049                    print 'x';
5050                }
5051            }",
5052            "foo.pl",
5053            |metric| {
5054                insta::assert_json_snapshot!(metric.cognitive, @r#"
5055                {
5056                  "sum": 2,
5057                  "value": 0,
5058                  "average": 2.0,
5059                  "min": 0,
5060                  "max": 2
5061                }
5062                "#);
5063            },
5064        );
5065    }
5066
5067    #[test]
5068    fn perl_sequence_different_booleans() {
5069        check_metrics::<PerlParser>(
5070            "sub f {
5071                if ($a && $b || $c) { # +1 if, +1 &&, +1 ||
5072                    print 'x';
5073                }
5074            }",
5075            "foo.pl",
5076            |metric| {
5077                insta::assert_json_snapshot!(metric.cognitive, @r#"
5078                {
5079                  "sum": 3,
5080                  "value": 0,
5081                  "average": 3.0,
5082                  "min": 0,
5083                  "max": 3
5084                }
5085                "#);
5086            },
5087        );
5088    }
5089
5090    #[test]
5091    fn perl_compound_short_circuit_assignment_249() {
5092        // Regression for issue #249: `&&=`, `||=`, `//=` are compound
5093        // short-circuit assignments (e.g. `$x //= 1` ≡ `$x = $x // 1`)
5094        // and each carries one boolean-sequence decision. The grammar
5095        // exposes the operator token inside `binary_expression`, so the
5096        // existing arm picks them up once `compute_perl_booleans`
5097        // recognises the three `*EQ` tokens.
5098        check_metrics::<PerlParser>(
5099            "sub f {
5100                 my ($x, $y, $z) = @_;
5101                 $x ||= 1; # +1 (||=)
5102                 $y &&= 2; # +1 (&&=)
5103                 $z //= 3; # +1 (//=)
5104                 return $x;
5105             }",
5106            "foo.pl",
5107            |metric| {
5108                assert_eq!(metric.cognitive.cognitive_sum(), 3);
5109                assert_eq!(metric.cognitive.cognitive_max(), 3);
5110                insta::assert_json_snapshot!(
5111                    metric.cognitive,
5112                    @r#"
5113                {
5114                  "sum": 3,
5115                  "value": 0,
5116                  "average": 3.0,
5117                  "min": 0,
5118                  "max": 3
5119                }
5120                "#
5121                );
5122            },
5123        );
5124    }
5125
5126    #[test]
5127    fn perl_not_booleans() {
5128        // `!` does not break boolean sequences (issue #392): pre-order
5129        // visits the outer `&&` BinaryExpression first, so the inner
5130        // `&&` lies within its span and is a continuation.
5131        check_metrics::<PerlParser>(
5132            "sub f {
5133                if ($a && !($b && $c)) { # +1 if, +1 outer &&; inner && continues
5134                    print 'x';
5135                }
5136            }",
5137            "foo.pl",
5138            |metric| {
5139                insta::assert_json_snapshot!(metric.cognitive, @r#"
5140                {
5141                  "sum": 2,
5142                  "value": 0,
5143                  "average": 2.0,
5144                  "min": 0,
5145                  "max": 2
5146                }
5147                "#);
5148            },
5149        );
5150    }
5151
5152    #[test]
5153    fn perl_1_level_nesting() {
5154        check_metrics::<PerlParser>(
5155            "sub f {
5156                for my $i (1..3) { # +1 for
5157                    if ($i % 2) { # +2 if (nested 1)
5158                        print $i;
5159                    }
5160                }
5161            }",
5162            "foo.pl",
5163            |metric| {
5164                insta::assert_json_snapshot!(metric.cognitive, @r#"
5165                {
5166                  "sum": 3,
5167                  "value": 0,
5168                  "average": 3.0,
5169                  "min": 0,
5170                  "max": 3
5171                }
5172                "#);
5173            },
5174        );
5175    }
5176
5177    #[test]
5178    fn perl_2_level_nesting() {
5179        check_metrics::<PerlParser>(
5180            "sub f {
5181                for my $i (1..3) { # +1 for
5182                    while ($n > 0) { # +2 while (nested 1)
5183                        if ($n % 2) { # +3 if (nested 2)
5184                            $n--;
5185                        }
5186                    }
5187                }
5188            }",
5189            "foo.pl",
5190            |metric| {
5191                insta::assert_json_snapshot!(metric.cognitive, @r#"
5192                {
5193                  "sum": 6,
5194                  "value": 0,
5195                  "average": 6.0,
5196                  "min": 0,
5197                  "max": 6
5198                }
5199                "#);
5200            },
5201        );
5202    }
5203
5204    #[test]
5205    fn perl_break_continue() {
5206        // Perl's `last`/`next` are loop-control statements; per Sonar's
5207        // cognitive rule, they do not add complexity in their bare form
5208        // (the surrounding loop already contributes +1).
5209        check_metrics::<PerlParser>(
5210            "sub f {
5211                while (1) { # +1 while (nesting becomes 1)
5212                    last if $done; # +2 postfix-if at nesting=1
5213                    next; # +0 bare loop control
5214                }
5215            }",
5216            "foo.pl",
5217            |metric| {
5218                insta::assert_json_snapshot!(metric.cognitive, @r#"
5219                {
5220                  "sum": 3,
5221                  "value": 0,
5222                  "average": 3.0,
5223                  "min": 0,
5224                  "max": 3
5225                }
5226                "#);
5227            },
5228        );
5229    }
5230
5231    #[test]
5232    fn perl_if_elsif_else() {
5233        check_metrics::<PerlParser>(
5234            "sub f {
5235                if ($x) { # +1 if
5236                    print 'a';
5237                } elsif ($y) { # +1 elsif
5238                    print 'b';
5239                } else { # +1 else
5240                    print 'c';
5241                }
5242            }",
5243            "foo.pl",
5244            |metric| {
5245                insta::assert_json_snapshot!(metric.cognitive, @r#"
5246                {
5247                  "sum": 3,
5248                  "value": 0,
5249                  "average": 3.0,
5250                  "min": 0,
5251                  "max": 3
5252                }
5253                "#);
5254            },
5255        );
5256    }
5257
5258    #[test]
5259    fn perl_function_definition_without_sub_depth() {
5260        // Regression: FunctionDefinitionWithoutSub must be a stop in
5261        // increment_function_depth so that a `sub` nested inside a `method`
5262        // block gets depth=1, making its structural elements cost +2 instead
5263        // of +1.  `method name { }` (Method::Signatures style) is what
5264        // tree-sitter-perl parses as function_definition_without_sub.
5265        check_metrics::<PerlParser>(
5266            "method outer {
5267                sub inner {
5268                    if (1) { } # +2 (depth=1)
5269                }
5270            }",
5271            "foo.pl",
5272            |metric| {
5273                insta::assert_json_snapshot!(metric.cognitive, @r#"
5274                {
5275                  "sum": 2,
5276                  "value": 0,
5277                  "average": 1.0,
5278                  "min": 0,
5279                  "max": 2
5280                }
5281                "#);
5282            },
5283        );
5284    }
5285
5286    #[test]
5287    fn perl_goto_single_increment() {
5288        // Regression (#450): `goto LABEL;` parses as `goto_expression`
5289        // wrapping the anonymous `goto` keyword token. The walker visits
5290        // both, so matching `Goto | GotoExpression` counted the jump twice
5291        // (cognitive 2). Matching only `GotoExpression` scores the correct
5292        // +1.
5293        check_metrics::<PerlParser>("sub f { goto LABEL; LABEL: return; }", "foo.pl", |metric| {
5294            // expected: one `goto` jump (§B2) = +1
5295            assert_eq!(metric.cognitive.cognitive_sum(), 1);
5296            insta::assert_json_snapshot!(metric.cognitive, @r#"
5297            {
5298              "sum": 1,
5299              "value": 0,
5300              "average": 1.0,
5301              "min": 0,
5302              "max": 1
5303            }
5304            "#);
5305        });
5306    }
5307
5308    #[test]
5309    fn perl_labeled_loop_control() {
5310        // Regression (#450): the jump target of `last/next/redo LABEL` is
5311        // carried as an `Identifier` child of `loop_control_statement`
5312        // (`Label` is the loop-*definition* node `OUTER:`). Gating on
5313        // `Label` was a dead arm — labeled jumps scored +0. Each labeled
5314        // form is now +1 (§B2). The bare forms below stay +0.
5315        check_metrics::<PerlParser>(
5316            "OUTER: for my $i (@a) { # +1 for
5317                 last OUTER;  # +1 labeled
5318                 next OUTER;  # +1 labeled
5319                 redo OUTER;  # +1 labeled
5320             }",
5321            "foo.pl",
5322            |metric| {
5323                // expected: +1 for-loop, +1 each labeled last/next/redo = 4
5324                assert_eq!(metric.cognitive.cognitive_sum(), 4);
5325                insta::assert_json_snapshot!(metric.cognitive, @r#"
5326                {
5327                  "sum": 4,
5328                  "value": 4,
5329                  "average": 4.0,
5330                  "min": 4,
5331                  "max": 4
5332                }
5333                "#);
5334            },
5335        );
5336    }
5337
5338    #[test]
5339    fn perl_bare_loop_control_zero() {
5340        // Bare `last;` / `next;` / `redo;` have no `Identifier` jump-target
5341        // child and must stay +0 — only the surrounding loop counts (§B2).
5342        check_metrics::<PerlParser>(
5343            "for my $i (@a) { # +1 for
5344                 last;  # +0
5345                 next;  # +0
5346                 redo;  # +0
5347             }",
5348            "foo.pl",
5349            |metric| {
5350                // expected: only the +1 for-loop; bare jumps add nothing
5351                assert_eq!(metric.cognitive.cognitive_sum(), 1);
5352                insta::assert_json_snapshot!(metric.cognitive, @r#"
5353                {
5354                  "sum": 1,
5355                  "value": 1,
5356                  "average": 1.0,
5357                  "min": 1,
5358                  "max": 1
5359                }
5360                "#);
5361            },
5362        );
5363    }
5364
5365    #[test]
5366    fn tsx_nested_if_for_with_booleans() {
5367        check_metrics::<TsxParser>(
5368            "function process(items: number[]) {
5369                 if (items.length > 0) { // +1
5370                     for (let i = 0; i < items.length; i++) { // +2 (nesting=1)
5371                         if (items[i] > 0 && items[i] < 100) { // +3 (nesting=2) +1 (&&)
5372                             console.log(items[i]);
5373                         }
5374                     }
5375                 }
5376             }",
5377            "foo.tsx",
5378            |metric| {
5379                insta::assert_json_snapshot!(
5380                    metric.cognitive,
5381                    @r#"
5382                {
5383                  "sum": 7,
5384                  "value": 0,
5385                  "average": 7.0,
5386                  "min": 0,
5387                  "max": 7
5388                }
5389                "#
5390                );
5391            },
5392        );
5393    }
5394
5395    #[test]
5396    fn typescript_nested_if_with_boolean_sequence() {
5397        check_metrics::<TypescriptParser>(
5398            "function validate(input: string, strict: boolean): boolean {
5399                 if (input.length > 0) { // +1
5400                     if (strict && input.trim() === input) { // +2 (nesting=1) +1 (&&)
5401                         return true;
5402                     }
5403                 }
5404                 return false;
5405             }",
5406            "foo.ts",
5407            |metric| {
5408                insta::assert_json_snapshot!(
5409                    metric.cognitive,
5410                    @r#"
5411                {
5412                  "sum": 4,
5413                  "value": 0,
5414                  "average": 4.0,
5415                  "min": 0,
5416                  "max": 4
5417                }
5418                "#
5419                );
5420            },
5421        );
5422    }
5423
5424    #[test]
5425    fn typescript_try_catch_with_nesting() {
5426        check_metrics::<TypescriptParser>(
5427            "function fetchData(url: string): string {
5428                 try {
5429                     if (url.length === 0) { // +1
5430                         throw new Error('empty url');
5431                     }
5432                     return url;
5433                 } catch (e) { // +1
5434                     if (e instanceof Error) { // +2 (nesting=1)
5435                         return e.message;
5436                     }
5437                     return 'unknown error';
5438                 }
5439             }",
5440            "foo.ts",
5441            |metric| {
5442                insta::assert_json_snapshot!(
5443                    metric.cognitive,
5444                    @r#"
5445                {
5446                  "sum": 4,
5447                  "value": 0,
5448                  "average": 4.0,
5449                  "min": 0,
5450                  "max": 4
5451                }
5452                "#
5453                );
5454            },
5455        );
5456    }
5457
5458    #[test]
5459    fn kotlin_cognitive_control_flow() {
5460        check_metrics::<KotlinParser>(
5461            "fun process(x: Int, y: Int): String {
5462                if (x > 0) {                // +1
5463                    for (i in 1..x) {       // +2 (nesting=1)
5464                        if (i % 2 == 0) {   // +3 (nesting=2)
5465                            println(i)
5466                        }
5467                    }
5468                } else if (x < 0) {        // +1 (else-if: flat +1 for else, if not counted as else-if)
5469                    when (y) {              // +2 (nesting=1)
5470                        1 -> println(\"one\")
5471                        2 -> println(\"two\")
5472                        else -> println(\"other\")
5473                    }
5474                } else {                    // +1
5475                    while (y > 0) {         // +2
5476                        println(y)
5477                    }
5478                }
5479                return if (x > y) \"big\" else \"small\"
5480            }",
5481            "foo.kt",
5482            |metric| {
5483                insta::assert_json_snapshot!(
5484                    metric.cognitive,
5485                    @r#"
5486                {
5487                  "sum": 14,
5488                  "value": 0,
5489                  "average": 14.0,
5490                  "min": 0,
5491                  "max": 14
5492                }
5493                "#
5494                );
5495            },
5496        );
5497    }
5498
5499    #[test]
5500    fn kotlin_no_cognitive() {
5501        check_metrics::<KotlinParser>("fun main() { val x = 42 }", "foo.kt", |metric| {
5502            insta::assert_json_snapshot!(metric.cognitive, @r#"
5503            {
5504              "sum": 0,
5505              "value": 0,
5506              "average": 0.0,
5507              "min": 0,
5508              "max": 0
5509            }
5510            "#);
5511        });
5512    }
5513
5514    #[test]
5515    fn kotlin_simple_if_with_boolean() {
5516        check_metrics::<KotlinParser>(
5517            "fun test(a: Boolean, b: Boolean) { if (a && b) { val x = 1 } }",
5518            "foo.kt",
5519            |metric| {
5520                insta::assert_json_snapshot!(metric.cognitive, @r#"
5521                {
5522                  "sum": 2,
5523                  "value": 0,
5524                  "average": 2.0,
5525                  "min": 0,
5526                  "max": 2
5527                }
5528                "#);
5529            },
5530        );
5531    }
5532
5533    #[test]
5534    fn kotlin_nesting() {
5535        check_metrics::<KotlinParser>(
5536            "fun test(items: List<Int>) {
5537                if (items.isNotEmpty()) {
5538                    for (i in items) {
5539                        if (i > 0) {
5540                            println(i)
5541                        }
5542                    }
5543                }
5544            }",
5545            "foo.kt",
5546            |metric| {
5547                insta::assert_json_snapshot!(metric.cognitive, @r#"
5548                {
5549                  "sum": 6,
5550                  "value": 0,
5551                  "average": 6.0,
5552                  "min": 0,
5553                  "max": 6
5554                }
5555                "#);
5556            },
5557        );
5558    }
5559
5560    #[test]
5561    fn kotlin_when_expression() {
5562        check_metrics::<KotlinParser>(
5563            "fun test(x: Int) { when { x > 10 -> val a = 1; x > 5 -> val b = 2; else -> val c = 3 } }",
5564            "foo.kt",
5565            |metric| {
5566                insta::assert_json_snapshot!(metric.cognitive, @r#"
5567                {
5568                  "sum": 1,
5569                  "value": 0,
5570                  "average": 1.0,
5571                  "min": 0,
5572                  "max": 1
5573                }
5574                "#);
5575            },
5576        );
5577    }
5578
5579    #[test]
5580    fn kotlin_when_else_no_increment() {
5581        check_metrics::<KotlinParser>(
5582            "fun test(x: Int) {
5583                when (x) {
5584                    1 -> println(\"one\")
5585                    2 -> println(\"two\")
5586                    else -> println(\"other\")
5587                }
5588            }",
5589            "foo.kt",
5590            |metric| {
5591                insta::assert_json_snapshot!(metric.cognitive, @r#"
5592                {
5593                  "sum": 1,
5594                  "value": 0,
5595                  "average": 1.0,
5596                  "min": 0,
5597                  "max": 1
5598                }
5599                "#);
5600            },
5601        );
5602    }
5603
5604    #[test]
5605    fn kotlin_labeled_break_continue() {
5606        // Regression (#450): tree-sitter-kotlin-ng has no break/continue
5607        // jump-statement kind — `break@outer` / `continue@outer` are
5608        // `labeled_expression` nodes. The Kotlin impl had no arm for them,
5609        // so labeled jumps scored +0. Each labeled jump is now +1 (§B2);
5610        // the bare `break` below (a plain identifier) stays +0.
5611        check_metrics::<KotlinParser>(
5612            "fun f() {
5613                 outer@ for (i in 1..10) { // +1 for
5614                     break@outer     // +1 labeled
5615                     continue@outer  // +1 labeled
5616                     break           // +0 bare
5617                 }
5618             }",
5619            "foo.kt",
5620            |metric| {
5621                // expected: +1 for-loop, +1 each labeled break/continue = 3
5622                assert_eq!(metric.cognitive.cognitive_sum(), 3);
5623                insta::assert_json_snapshot!(metric.cognitive, @r#"
5624                {
5625                  "sum": 3,
5626                  "value": 0,
5627                  "average": 3.0,
5628                  "min": 0,
5629                  "max": 3
5630                }
5631                "#);
5632            },
5633        );
5634    }
5635
5636    #[test]
5637    fn kotlin_labeled_nonjump_expression_not_counted() {
5638        // Regression (#450 follow-up): tree-sitter-kotlin-ng models ANY
5639        // labeled expression as `labeled_expression`, not only labeled
5640        // jumps. The original #450 arm was unconditional, so a labeled
5641        // non-jump (`lbl@ run { … }`) wrongly scored +1. The arm now gates
5642        // on the `label` token being the fused jump keyword `break@` /
5643        // `continue@`; an ordinary `name@` label must contribute +0.
5644        // Pre-fix this scored 1.0; verified via test-via-revert.
5645        check_metrics::<KotlinParser>("fun f() { lbl@ run { println(1) } }", "foo.kt", |metric| {
5646            // expected: labeled non-jump is not a structured-control-flow
5647            // break, so it adds nothing.
5648            assert_eq!(metric.cognitive.cognitive_sum(), 0);
5649        });
5650    }
5651
5652    #[test]
5653    fn kotlin_else_in_if_still_increments() {
5654        check_metrics::<KotlinParser>(
5655            "fun test(x: Int) {
5656                if (x > 0) {
5657                    println(\"positive\")
5658                } else {
5659                    println(\"non-positive\")
5660                }
5661            }",
5662            "foo.kt",
5663            |metric| {
5664                insta::assert_json_snapshot!(metric.cognitive, @r#"
5665                {
5666                  "sum": 2,
5667                  "value": 0,
5668                  "average": 2.0,
5669                  "min": 0,
5670                  "max": 2
5671                }
5672                "#);
5673            },
5674        );
5675    }
5676
5677    #[test]
5678    fn kotlin_else_if_chain() {
5679        check_metrics::<KotlinParser>(
5680            "fun test(x: Int) {
5681                if (x > 10) {
5682                } else if (x > 5) {
5683                } else if (x > 0) {
5684                } else {
5685                }
5686            }",
5687            "foo.kt",
5688            |metric| {
5689                insta::assert_json_snapshot!(metric.cognitive, @r#"
5690                {
5691                  "sum": 4,
5692                  "value": 0,
5693                  "average": 4.0,
5694                  "min": 0,
5695                  "max": 4
5696                }
5697                "#);
5698            },
5699        );
5700    }
5701
5702    #[test]
5703    fn kotlin_lambda_nesting() {
5704        check_metrics::<KotlinParser>(
5705            "fun test() { val f = { if (true) { } } }",
5706            "foo.kt",
5707            |metric| {
5708                insta::assert_json_snapshot!(metric.cognitive, @r#"
5709                {
5710                  "sum": 2,
5711                  "value": 0,
5712                  "average": 1.0,
5713                  "min": 0,
5714                  "max": 2
5715                }
5716                "#);
5717            },
5718        );
5719    }
5720
5721    #[test]
5722    fn kotlin_secondary_constructor_depth() {
5723        // Regression: SecondaryConstructor must be a stop in increment_function_depth so
5724        // that a local `fun` nested inside it gets depth=1, making its structural elements
5725        // cost +2 instead of +1.
5726        check_metrics::<KotlinParser>(
5727            "class Foo {
5728                constructor(x: Int) {
5729                    fun inner(): Boolean {
5730                        if (x > 0) { return true } // +2 (depth=1)
5731                        return false
5732                    }
5733                }
5734            }",
5735            "foo.kt",
5736            |metric| {
5737                insta::assert_json_snapshot!(metric.cognitive, @r#"
5738                {
5739                  "sum": 2,
5740                  "value": 0,
5741                  "average": 1.0,
5742                  "min": 0,
5743                  "max": 2
5744                }
5745                "#);
5746            },
5747        );
5748    }
5749
5750    #[test]
5751    fn go_no_cognitive() {
5752        check_metrics::<GoParser>("package main\nvar x = 42", "foo.go", |metric| {
5753            insta::assert_json_snapshot!(
5754                metric.cognitive,
5755                @r#"
5756            {
5757              "sum": 0,
5758              "value": 0,
5759              "average": 0.0,
5760              "min": 0,
5761              "max": 0
5762            }
5763            "#
5764            );
5765        });
5766    }
5767
5768    #[test]
5769    fn go_simple_function() {
5770        check_metrics::<GoParser>(
5771            "package main
5772            func f(a, b bool) {
5773                if a && b {    // +1 (if) +1 (&&)
5774                    return
5775                }
5776                if a || b {    // +1 (if) +1 (||)
5777                    return
5778                }
5779            }",
5780            "foo.go",
5781            |metric| {
5782                insta::assert_json_snapshot!(
5783                    metric.cognitive,
5784                    @r#"
5785                {
5786                  "sum": 4,
5787                  "value": 0,
5788                  "average": 4.0,
5789                  "min": 0,
5790                  "max": 4
5791                }
5792                "#
5793                );
5794            },
5795        );
5796    }
5797
5798    #[test]
5799    fn go_nesting() {
5800        check_metrics::<GoParser>(
5801            "package main
5802            func f(x int, items []int) {
5803                if x > 0 {                    // +1 (nesting 0)
5804                    for _, v := range items {  // +2 (nesting 1)
5805                        if v > 0 {             // +3 (nesting 2)
5806                            println(v)
5807                        }
5808                    }
5809                }
5810            }",
5811            "foo.go",
5812            |metric| {
5813                insta::assert_json_snapshot!(
5814                    metric.cognitive,
5815                    @r#"
5816                {
5817                  "sum": 6,
5818                  "value": 0,
5819                  "average": 6.0,
5820                  "min": 0,
5821                  "max": 6
5822                }
5823                "#
5824                );
5825            },
5826        );
5827    }
5828
5829    #[test]
5830    fn go_switch() {
5831        check_metrics::<GoParser>(
5832            "package main
5833            func f(x int) {
5834                switch x {         // +1 (nesting 0)
5835                case 1:
5836                    if x > 0 {     // +2 (nesting 1)
5837                        println(x)
5838                    }
5839                default:
5840                    println(x)
5841                }
5842            }",
5843            "foo.go",
5844            |metric| {
5845                insta::assert_json_snapshot!(
5846                    metric.cognitive,
5847                    @r#"
5848                {
5849                  "sum": 3,
5850                  "value": 0,
5851                  "average": 3.0,
5852                  "min": 0,
5853                  "max": 3
5854                }
5855                "#
5856                );
5857            },
5858        );
5859    }
5860
5861    #[test]
5862    fn go_goto() {
5863        check_metrics::<GoParser>(
5864            "package main
5865            func f(n int) {
5866                if n > 10 {    // +1 (nesting 0)
5867                    goto end   // +1 (goto)
5868                }
5869            end:
5870                return
5871            }",
5872            "foo.go",
5873            |metric| {
5874                insta::assert_json_snapshot!(
5875                    metric.cognitive,
5876                    @r#"
5877                {
5878                  "sum": 2,
5879                  "value": 0,
5880                  "average": 2.0,
5881                  "min": 0,
5882                  "max": 2
5883                }
5884                "#
5885                );
5886            },
5887        );
5888    }
5889
5890    #[test]
5891    fn go_else_if_chain() {
5892        check_metrics::<GoParser>(
5893            "package main
5894            func f(x int) {
5895                if x > 0 {           // +1 (nesting 0)
5896                    println(x)
5897                } else if x < 0 {    // +1 (else-if)
5898                    println(-x)
5899                } else {              // +1 (else)
5900                    println(0)
5901                }
5902            }",
5903            "foo.go",
5904            |metric| {
5905                insta::assert_json_snapshot!(
5906                    metric.cognitive,
5907                    @r#"
5908                {
5909                  "sum": 3,
5910                  "value": 0,
5911                  "average": 3.0,
5912                  "min": 0,
5913                  "max": 3
5914                }
5915                "#
5916                );
5917            },
5918        );
5919    }
5920
5921    #[test]
5922    fn go_labeled_break_continue() {
5923        check_metrics::<GoParser>(
5924            "package main
5925            func f() {
5926            outer:
5927                for i := 0; i < 3; i++ {       // +1 (nesting 0)
5928                    for j := 0; j < 3; j++ {    // +2 (nesting 1)
5929                        if i == j {              // +3 (nesting 2)
5930                            continue outer       // +1 (labeled continue)
5931                        }
5932                    }
5933                }
5934            }",
5935            "foo.go",
5936            |metric| {
5937                insta::assert_json_snapshot!(
5938                    metric.cognitive,
5939                    @r#"
5940                {
5941                  "sum": 7,
5942                  "value": 0,
5943                  "average": 7.0,
5944                  "min": 0,
5945                  "max": 7
5946                }
5947                "#
5948                );
5949            },
5950        );
5951    }
5952
5953    #[test]
5954    fn go_method_declaration() {
5955        // Coverage: MethodDeclaration is processed as a function boundary (nesting
5956        // reset) identically to FunctionDeclaration.  The depth-stop fix from
5957        // 081f893 (adding MethodDeclaration to increment_function_depth's stop
5958        // list) cannot be regression-tested with valid Go because method
5959        // declarations cannot be nested inside other functions or methods.
5960        check_metrics::<GoParser>(
5961            "package main
5962            type T struct{ val int }
5963            func (t T) positive() bool {
5964                if t.val > 0 { // +1
5965                    return true
5966                }
5967                return false
5968            }",
5969            "foo.go",
5970            |metric| {
5971                insta::assert_json_snapshot!(metric.cognitive, @r#"
5972                {
5973                  "sum": 1,
5974                  "value": 0,
5975                  "average": 1.0,
5976                  "min": 0,
5977                  "max": 1
5978                }
5979                "#);
5980            },
5981        );
5982    }
5983
5984    #[test]
5985    fn bash_no_cognitive() {
5986        check_metrics::<BashParser>("a=42", "foo.sh", |metric| {
5987            insta::assert_json_snapshot!(
5988                metric.cognitive,
5989                @r#"
5990            {
5991              "sum": 0,
5992              "value": 0,
5993              "average": 0.0,
5994              "min": 0,
5995              "max": 0
5996            }
5997            "#
5998            );
5999        });
6000    }
6001
6002    #[test]
6003    fn bash_simple_if() {
6004        check_metrics::<BashParser>(
6005            "f() {
6006                 if [ -z \"$1\" ]; then  # +1
6007                     echo empty
6008                 fi
6009             }",
6010            "foo.sh",
6011            |metric| {
6012                insta::assert_json_snapshot!(
6013                    metric.cognitive,
6014                    @r#"
6015                {
6016                  "sum": 1,
6017                  "value": 0,
6018                  "average": 1.0,
6019                  "min": 0,
6020                  "max": 1
6021                }
6022                "#
6023                );
6024            },
6025        );
6026    }
6027
6028    #[test]
6029    fn bash_if_elif_else() {
6030        check_metrics::<BashParser>(
6031            "f() {
6032                 if [ \"$1\" = a ]; then     # +1
6033                     echo a
6034                 elif [ \"$1\" = b ]; then   # +1
6035                     echo b
6036                 else                         # +1
6037                     echo other
6038                 fi
6039             }",
6040            "foo.sh",
6041            |metric| {
6042                insta::assert_json_snapshot!(
6043                    metric.cognitive,
6044                    @r#"
6045                {
6046                  "sum": 3,
6047                  "value": 0,
6048                  "average": 3.0,
6049                  "min": 0,
6050                  "max": 3
6051                }
6052                "#
6053                );
6054            },
6055        );
6056    }
6057
6058    #[test]
6059    fn bash_nested_loops() {
6060        check_metrics::<BashParser>(
6061            "f() {
6062                 for i in 1 2 3; do            # +1
6063                     while [ \"$x\" -lt 10 ]; do  # +2 (nested)
6064                         x=$((x+1))
6065                     done
6066                 done
6067             }",
6068            "foo.sh",
6069            |metric| {
6070                insta::assert_json_snapshot!(
6071                    metric.cognitive,
6072                    @r#"
6073                {
6074                  "sum": 3,
6075                  "value": 0,
6076                  "average": 3.0,
6077                  "min": 0,
6078                  "max": 3
6079                }
6080                "#
6081                );
6082            },
6083        );
6084    }
6085
6086    #[test]
6087    fn bash_until_loop() {
6088        // `until` parses to `Bash::WhileStatement`; this test pins that
6089        // assumption so a future grammar bump that adds a dedicated
6090        // `UntilStatement` variant is caught.
6091        check_metrics::<BashParser>(
6092            "f() {
6093                 until [ -z \"$x\" ]; do  # +1
6094                     x=$(pop)
6095                 done
6096             }",
6097            "foo.sh",
6098            |metric| {
6099                insta::assert_json_snapshot!(
6100                    metric.cognitive,
6101                    @r#"
6102                {
6103                  "sum": 1,
6104                  "value": 0,
6105                  "average": 1.0,
6106                  "min": 0,
6107                  "max": 1
6108                }
6109                "#
6110                );
6111            },
6112        );
6113    }
6114
6115    #[test]
6116    fn bash_case() {
6117        // `case` adds +1 nesting; case arms do not contribute extra cognitive
6118        // cost (matching Kotlin's `WhenExpression` treatment).
6119        check_metrics::<BashParser>(
6120            "f() {
6121                 case \"$1\" in       # +1
6122                     a) echo a ;;
6123                     b) echo b ;;
6124                     *) echo other ;;
6125                 esac
6126             }",
6127            "foo.sh",
6128            |metric| {
6129                insta::assert_json_snapshot!(
6130                    metric.cognitive,
6131                    @r#"
6132                {
6133                  "sum": 1,
6134                  "value": 0,
6135                  "average": 1.0,
6136                  "min": 0,
6137                  "max": 1
6138                }
6139                "#
6140                );
6141            },
6142        );
6143    }
6144
6145    #[test]
6146    fn bash_arithmetic_ternary_increases_nesting() {
6147        // Regression for #1268: Bash's only ternary form scored zero
6148        // cognitive complexity. It nests like the C-family
6149        // `ConditionalExpression`. Both arithmetic contexts are covered —
6150        // the `$(( … ))` expansion and the bare `(( … ))` statement.
6151        check_metrics::<BashParser>(
6152            "f() {
6153                 local m=$(( a > b ? a : b ))  # +1 ternary
6154             }
6155             g() {
6156                 (( x = a ? b : c ))  # +1 ternary
6157             }",
6158            "foo.sh",
6159            |metric| {
6160                // One increment per function, at nesting 0 in each.
6161                insta::assert_json_snapshot!(
6162                    metric.cognitive,
6163                    @r#"
6164                {
6165                  "sum": 2,
6166                  "value": 0,
6167                  "average": 1.0,
6168                  "min": 0,
6169                  "max": 1
6170                }
6171                "#
6172                );
6173            },
6174        );
6175    }
6176
6177    #[test]
6178    fn bash_nested_arithmetic_ternary_charges_the_inner_one_twice() {
6179        // A ternary inside a ternary is +1 for the outer and +2 for the
6180        // inner (one increment plus one nesting level), the same charge
6181        // every nesting construct carries (#1268).
6182        check_metrics::<BashParser>(
6183            "h() {
6184                 local n=$(( a ? b : c ? d : e ))  # +1 outer, +2 inner
6185             }",
6186            "foo.sh",
6187            |metric| {
6188                insta::assert_json_snapshot!(
6189                    metric.cognitive,
6190                    @r#"
6191                {
6192                  "sum": 3,
6193                  "value": 0,
6194                  "average": 3.0,
6195                  "min": 0,
6196                  "max": 3
6197                }
6198                "#
6199                );
6200            },
6201        );
6202    }
6203
6204    #[test]
6205    fn bash_boolean_sequence() {
6206        // First if: a chain of `&&` is one boolean increment regardless of
6207        // length (consecutive same-operator chain). Second if: `&& … ||` is
6208        // two operator transitions, so two boolean increments.
6209        check_metrics::<BashParser>(
6210            "f() {
6211                 if [[ -n \"$x\" ]] && [[ -n \"$y\" ]] && [[ -n \"$z\" ]]; then
6212                     # +1 if, +1 boolean (one && chain)
6213                     echo all
6214                 fi
6215                 if [[ -n \"$x\" ]] && [[ -n \"$y\" ]] || [[ -n \"$z\" ]]; then
6216                     # +1 if, +2 boolean (&& then ||)
6217                     echo mixed
6218                 fi
6219             }",
6220            "foo.sh",
6221            |metric| {
6222                insta::assert_json_snapshot!(
6223                    metric.cognitive,
6224                    @r#"
6225                {
6226                  "sum": 5,
6227                  "value": 0,
6228                  "average": 5.0,
6229                  "min": 0,
6230                  "max": 5
6231                }
6232                "#
6233                );
6234            },
6235        );
6236    }
6237
6238    #[test]
6239    fn tcl_no_cognitive() {
6240        // No proc, no control flow → cognitive complexity is zero everywhere.
6241        check_metrics::<TclParser>("set x 1", "foo.tcl", |metric| {
6242            assert_eq!(metric.cognitive.cognitive_sum(), 0);
6243            assert_eq!(metric.cognitive.cognitive_max(), 0);
6244            insta::assert_json_snapshot!(metric.cognitive);
6245        });
6246    }
6247
6248    #[test]
6249    fn tcl_simple_function() {
6250        // proc with one if and one &&: if(+1) + &&(+1) = 2.
6251        check_metrics::<TclParser>(
6252            "proc f {a} {
6253    if {$a > 0 && $a < 10} {
6254        puts yes
6255    }
6256}",
6257            "foo.tcl",
6258            |metric| {
6259                assert_eq!(metric.cognitive.cognitive_sum(), 2);
6260                assert_eq!(metric.cognitive.cognitive_max(), 2);
6261                insta::assert_json_snapshot!(metric.cognitive);
6262            },
6263        );
6264    }
6265
6266    #[test]
6267    fn tcl_sequence_same_booleans() {
6268        // Sequences of the same boolean operator count as a single increment.
6269        // `$a && $b && $c` → +1 (one && group), not +2.
6270        check_metrics::<TclParser>(
6271            "proc f {a b c d} {
6272    if {$a && $b && $c} {
6273        puts yes
6274    }
6275    if {$a || $b || $c || $d} {
6276        puts no
6277    }
6278}",
6279            "foo.tcl",
6280            |metric| {
6281                // Two ifs (+1 each) + two single-op chains (+1 each) = 4.
6282                assert_eq!(metric.cognitive.cognitive_sum(), 4);
6283                assert_eq!(metric.cognitive.cognitive_max(), 4);
6284                insta::assert_json_snapshot!(metric.cognitive);
6285            },
6286        );
6287    }
6288
6289    #[test]
6290    fn tcl_sequence_different_booleans() {
6291        // Switching operator type increments again: `$a && $b || $c` → +2 (one &&, one ||).
6292        check_metrics::<TclParser>(
6293            "proc f {a b c} {
6294    if {$a && $b || $c} {
6295        puts yes
6296    }
6297}",
6298            "foo.tcl",
6299            |metric| {
6300                // if(+1) + &&(+1) + ||(+1) = 3.
6301                assert_eq!(metric.cognitive.cognitive_sum(), 3);
6302                assert_eq!(metric.cognitive.cognitive_max(), 3);
6303                insta::assert_json_snapshot!(metric.cognitive);
6304            },
6305        );
6306    }
6307
6308    #[test]
6309    fn tcl_not_booleans() {
6310        // `!` does not contribute cognitive cost on its own (issue
6311        // #392). The single `&&` between the two negations contributes
6312        // +1, plus +1 for the surrounding `if`.
6313        check_metrics::<TclParser>(
6314            "proc f {a b} {
6315    if {!$a && !$b} {
6316        puts yes
6317    }
6318}",
6319            "foo.tcl",
6320            |metric| {
6321                // if(+1) + &&(+1) = 2; the `!` operators do not increment.
6322                assert_eq!(metric.cognitive.cognitive_sum(), 2);
6323                assert_eq!(metric.cognitive.cognitive_max(), 2);
6324                insta::assert_json_snapshot!(metric.cognitive);
6325            },
6326        );
6327    }
6328
6329    #[test]
6330    fn tcl_1_level_nesting() {
6331        // while(+1) then if at depth 1 (+2) = 3 for the proc.
6332        check_metrics::<TclParser>(
6333            "proc f {x} {
6334    while {$x > 0} {
6335        if {$x > 10} {
6336            set x [expr {$x - 1}]
6337        }
6338    }
6339}",
6340            "foo.tcl",
6341            |metric| {
6342                // while(+1) + if at depth 1 (+2) = 3.
6343                assert_eq!(metric.cognitive.cognitive_sum(), 3);
6344                assert_eq!(metric.cognitive.cognitive_max(), 3);
6345                insta::assert_json_snapshot!(metric.cognitive);
6346            },
6347        );
6348    }
6349
6350    #[test]
6351    fn tcl_2_level_nesting() {
6352        // while(+1) + foreach at depth 1 (+2) + if at depth 2 (+3) = 6.
6353        check_metrics::<TclParser>(
6354            "proc f {x} {
6355    while {$x > 0} {
6356        foreach y {1 2 3} {
6357            if {$y > $x} {
6358                puts found
6359            }
6360        }
6361    }
6362}",
6363            "foo.tcl",
6364            |metric| {
6365                // while(+1) + foreach at depth 1 (+2) + if at depth 2 (+3) = 6.
6366                assert_eq!(metric.cognitive.cognitive_sum(), 6);
6367                assert_eq!(metric.cognitive.cognitive_max(), 6);
6368                insta::assert_json_snapshot!(metric.cognitive);
6369            },
6370        );
6371    }
6372
6373    #[test]
6374    fn tcl_catch_cognitive() {
6375        // `catch` is a conditional handler: +1 at nesting 0, then body at nesting 1.
6376        // Nested if inside catch body: +2 (depth 1).
6377        check_metrics::<TclParser>(
6378            "proc f {x} {
6379    catch {
6380        if {$x < 0} {
6381            error negative
6382        }
6383    } msg
6384}",
6385            "foo.tcl",
6386            |metric| {
6387                // catch(+1) + if at depth 1 (+2) = 3.
6388                assert_eq!(metric.cognitive.cognitive_sum(), 3);
6389                assert_eq!(metric.cognitive.cognitive_max(), 3);
6390                insta::assert_json_snapshot!(metric.cognitive);
6391            },
6392        );
6393    }
6394
6395    #[test]
6396    fn tcl_if_elseif_else() {
6397        // if(+1) + elseif(+1) + else(+1) = 3; nesting does not increase for elseif/else.
6398        check_metrics::<TclParser>(
6399            "proc f {x} {
6400    if {$x > 10} {
6401        puts big
6402    } elseif {$x > 5} {
6403        puts medium
6404    } else {
6405        puts small
6406    }
6407}",
6408            "foo.tcl",
6409            |metric| {
6410                // if(+1) + elseif(+1) + else(+1) = 3.
6411                assert_eq!(metric.cognitive.cognitive_sum(), 3);
6412                assert_eq!(metric.cognitive.cognitive_max(), 3);
6413                insta::assert_json_snapshot!(metric.cognitive);
6414            },
6415        );
6416    }
6417
6418    #[test]
6419    fn tcl_not_booleans_nested() {
6420        // `$a && !($b && $c)`: `!` does not break boolean sequences
6421        // (issue #392); inner `&&` is a continuation of the outer.
6422        check_metrics::<TclParser>(
6423            "proc f {a b c} {
6424    if {$a && !($b && $c)} {
6425        puts yes
6426    }
6427}",
6428            "foo.tcl",
6429            |metric| {
6430                // if(+1) + outer &&(+1); inner && continues outer's span → 2.
6431                assert_eq!(metric.cognitive.cognitive_sum(), 2);
6432                assert_eq!(metric.cognitive.cognitive_max(), 2);
6433                insta::assert_json_snapshot!(metric.cognitive);
6434            },
6435        );
6436    }
6437
6438    #[test]
6439    fn tcl_not_booleans_double_nested() {
6440        // `!($a || $b) && !($c || $d)`: the two `||` sub-expressions and
6441        // the connecting `&&` are at distinct positions with distinct
6442        // operator tokens, so each starts a new boolean sequence
6443        // regardless of the `!` wrapping (issue #392). if(+1) + &&(+1)
6444        // + first ||(+1) + second ||(+1) = 4.
6445        check_metrics::<TclParser>(
6446            "proc f {a b c d} {
6447    if {!($a || $b) && !($c || $d)} {
6448        puts yes
6449    }
6450}",
6451            "foo.tcl",
6452            |metric| {
6453                // if(+1) + &&(+1) + first || (+1) + second || (+1) = 4.
6454                assert_eq!(metric.cognitive.cognitive_sum(), 4);
6455                assert_eq!(metric.cognitive.cognitive_max(), 4);
6456                insta::assert_json_snapshot!(metric.cognitive);
6457            },
6458        );
6459    }
6460
6461    #[test]
6462    fn tcl_nested_procedure_cognitive() {
6463        // Inner proc is at depth=1; its `if` adds +1+1=2 instead of +1+0=1.
6464        check_metrics::<TclParser>(
6465            "proc outer {x} {
6466    proc inner {y} {
6467        if {$y > 0} {
6468            puts positive
6469        }
6470    }
6471    inner $x
6472}",
6473            "foo.tcl",
6474            |metric| {
6475                // Aggregated: inner proc's `if` at depth 1 contributes 2.
6476                assert_eq!(metric.cognitive.cognitive_sum(), 2);
6477                assert_eq!(metric.cognitive.cognitive_max(), 2);
6478                insta::assert_json_snapshot!(metric.cognitive);
6479            },
6480        );
6481    }
6482
6483    #[test]
6484    fn tcl_ternary_cognitive() {
6485        // Ternary `? :` inside expr is a conditional expression: adds +1+depth.
6486        // At proc body depth 0: +1. Inside a while (depth 1): +2.
6487        check_metrics::<TclParser>(
6488            "proc f {x} {
6489    set y [expr {$x > 0 ? $x : -$x}]
6490    while {$y > 10} {
6491        set y [expr {$y > 5 ? $y - 1 : 0}]
6492    }
6493}",
6494            "foo.tcl",
6495            |metric| {
6496                // outer ternary(+1) + while(+1) + inner ternary at depth 1 (+2) = 4.
6497                assert_eq!(metric.cognitive.cognitive_sum(), 4);
6498                assert_eq!(metric.cognitive.cognitive_max(), 4);
6499                insta::assert_json_snapshot!(metric.cognitive);
6500            },
6501        );
6502    }
6503
6504    #[test]
6505    fn tcl_switch_cognitive() {
6506        // Tcl `switch` is a generic command, not a dedicated kind. As a
6507        // switch-like structure it adds +1 plus current nesting once; the arm
6508        // count and the `default` arm do not add cognitive cost, matching
6509        // C-family `SwitchStatement` and Bash `case` (issue #467, lesson 11).
6510        check_metrics::<TclParser>(
6511            "proc f {x} {
6512    switch $x {
6513        1 { puts a }
6514        2 { puts b }
6515        default { puts c }
6516    }
6517}",
6518            "foo.tcl",
6519            |metric| {
6520                // One switch structure at proc-body nesting 0 → +1.
6521                assert_eq!(metric.cognitive.cognitive_sum(), 1);
6522                assert_eq!(metric.cognitive.cognitive_max(), 1);
6523            },
6524        );
6525    }
6526
6527    #[test]
6528    fn tcl_switch_cognitive_nested() {
6529        // A `switch` nested inside an outer `switch` arm pays the nesting
6530        // penalty: outer +1 (nesting 0), inner +1+1 (nesting 1) = 3 (issue #467).
6531        check_metrics::<TclParser>(
6532            "proc f {x y} {
6533    switch $x {
6534        1 {
6535            switch $y {
6536                a { puts p }
6537                b { puts q }
6538            }
6539        }
6540        2 { puts b }
6541    }
6542}",
6543            "foo.tcl",
6544            |metric| {
6545                // outer switch(+1) + inner switch at nesting 1 (+2) = 3.
6546                assert_eq!(metric.cognitive.cognitive_sum(), 3);
6547                assert_eq!(metric.cognitive.cognitive_max(), 3);
6548            },
6549        );
6550    }
6551
6552    #[test]
6553    fn tcl_switch_split_form_adds_no_cognitive() {
6554        // The split arm form passes each arm body as its own sibling
6555        // `braced_word` argument, so `tcl_switch_arm_list` finds no
6556        // single wrapping arm list and declines the construct (issue
6557        // #467). Cognitive then treats it as an ordinary command: no
6558        // structural increment and no nesting for its bodies, which is
6559        // why the inner `if` here charges 1 rather than 2.
6560        check_metrics::<TclParser>(
6561            "proc f {x} {
6562    switch $x a { puts a } b { if {$x} { puts b } }
6563}",
6564            "foo.tcl",
6565            |metric| {
6566                // The `switch` adds nothing; the `if` sits at proc-body
6567                // nesting 0 and charges +1.
6568                assert_eq!(metric.cognitive.cognitive_sum(), 1);
6569                assert_eq!(metric.cognitive.cognitive_max(), 1);
6570            },
6571        );
6572    }
6573
6574    #[test]
6575    fn tcl_for_cognitive() {
6576        // Tcl `for` is a generic command — the grammar has no `for` rule —
6577        // so it is detected by leading word (issue #1264): a loop adds +1
6578        // plus current nesting, matching `while`/`foreach`.
6579        check_metrics::<TclParser>(
6580            "proc f {n} {
6581    for {set i 0} {$i < $n} {incr i} {
6582        puts $i
6583    }
6584}",
6585            "foo.tcl",
6586            |metric| {
6587                // One `for` at proc-body nesting 0 → +1.
6588                assert_eq!(metric.cognitive.cognitive_sum(), 1);
6589                assert_eq!(metric.cognitive.cognitive_max(), 1);
6590            },
6591        );
6592    }
6593
6594    #[test]
6595    fn tcl_for_cognitive_nested() {
6596        // The `for` also nests its body: constructs inside it pay the
6597        // nesting penalty the missing loop previously swallowed (#1264).
6598        check_metrics::<TclParser>(
6599            "proc f {n} {
6600    for {set i 0} {$i < $n} {incr i} {
6601        if {$i == 2} {
6602            puts $i
6603        }
6604    }
6605}",
6606            "foo.tcl",
6607            |metric| {
6608                // for(+1, nesting 0) + if at nesting 1 (+2) = 3.
6609                assert_eq!(metric.cognitive.cognitive_sum(), 3);
6610                assert_eq!(metric.cognitive.cognitive_max(), 3);
6611            },
6612        );
6613    }
6614
6615    #[test]
6616    fn tcl_for_cognitive_name_gate() {
6617        // The detection reads the command's `name` field: a command whose
6618        // name merely starts with "for" (`format`, with `for`-shaped braced
6619        // arguments) and a `for` word in argument position (`puts for`) must
6620        // both stay at zero (issue #1264).
6621        check_metrics::<TclParser>(
6622            "proc f {} {
6623    format {a} {b} {c} {d}
6624    puts for
6625}",
6626            "foo.tcl",
6627            |metric| {
6628                assert_eq!(metric.cognitive.cognitive_sum(), 0);
6629                assert_eq!(metric.cognitive.cognitive_max(), 0);
6630            },
6631        );
6632    }
6633
6634    #[test]
6635    fn tcl_irules_for_parity() {
6636        // iRules models `for` as a dedicated kind counted by the kind
6637        // dispatch; Tcl detects it by leading word (issue #1264). The same
6638        // loop must score identically in both — and the iRules figure also
6639        // pins that its dedicated kind is not double-counted through the
6640        // Tcl command-name path. One loop at nesting 0 → +1 in each.
6641        check_metrics::<TclParser>(
6642            "proc f {} {
6643    for {set i 0} {$i < 10} {incr i} {
6644        puts $i
6645    }
6646}",
6647            "foo.tcl",
6648            |metric| {
6649                assert_eq!(metric.cognitive.cognitive_sum(), 1);
6650                assert_eq!(metric.cognitive.cognitive_max(), 1);
6651            },
6652        );
6653        check_metrics::<IrulesParser>(
6654            "when HTTP_REQUEST {
6655    for {set i 0} {$i < 10} {incr i} {
6656        puts $i
6657    }
6658}
6659",
6660            "foo.irule",
6661            |metric| {
6662                assert_eq!(metric.cognitive.cognitive_sum(), 1);
6663                assert_eq!(metric.cognitive.cognitive_max(), 1);
6664            },
6665        );
6666    }
6667
6668    #[test]
6669    fn tcl_try_on_error_cognitive() {
6670        // Tcl `try`'s `on error` handler is a conditional error path:
6671        // +1 plus current nesting (issue #1266), matching `catch`;
6672        // `finally` is unconditional and free.
6673        check_metrics::<TclParser>(
6674            "proc f {} {
6675    try {
6676        risky
6677    } on error {msg} {
6678        puts $msg
6679    } finally {
6680        cleanup
6681    }
6682}",
6683            "foo.tcl",
6684            |metric| {
6685                // One handler at proc-body nesting 0 → +1.
6686                assert_eq!(metric.cognitive.cognitive_sum(), 1);
6687                assert_eq!(metric.cognitive.cognitive_max(), 1);
6688            },
6689        );
6690    }
6691
6692    #[test]
6693    fn tcl_try_finally_only_cognitive() {
6694        // A `try` with only a `finally` has no conditional path and must
6695        // stay at zero (issue #1266, the cross-language `finally`
6696        // convention of #416).
6697        check_metrics::<TclParser>(
6698            "proc f {} {
6699    try {
6700        risky
6701    } finally {
6702        cleanup
6703    }
6704}",
6705            "foo.tcl",
6706            |metric| {
6707                assert_eq!(metric.cognitive.cognitive_sum(), 0);
6708                assert_eq!(metric.cognitive.cognitive_max(), 0);
6709            },
6710        );
6711    }
6712
6713    #[test]
6714    fn tcl_try_handler_nesting_cognitive() {
6715        // Only the handler body nests (issue #1266): the `try` body and
6716        // `finally` body run unconditionally and stay at the inherited
6717        // level, so an `if` inside each of the three blocks is charged
6718        // differently. This pins the per-child nesting seed — charging
6719        // `increase_nesting` at the whole `try` node instead would score
6720        // the try-body and finally-body `if`s +2 each (sum 7).
6721        check_metrics::<TclParser>(
6722            "proc f {} {
6723    try {
6724        if {1} {
6725            a
6726        }
6727    } on error {msg} {
6728        if {1} {
6729            b
6730        }
6731    } finally {
6732        if {1} {
6733            c
6734        }
6735    }
6736}",
6737            "foo.tcl",
6738            |metric| {
6739                // handler(+1) + try-body if(+1, nesting 0)
6740                // + handler-body if(+2, nesting 1)
6741                // + finally-body if(+1, nesting 0) = 5.
6742                assert_eq!(metric.cognitive.cognitive_sum(), 5);
6743                assert_eq!(metric.cognitive.cognitive_max(), 5);
6744            },
6745        );
6746    }
6747
6748    #[test]
6749    fn irules_try_trap_cognitive() {
6750        // iRules wraps each `try` handler in a dedicated `on_handler` /
6751        // `trap_handler` node: +1 each plus nesting, and the handler body
6752        // nests (issue #1266). Before this the handlers opened anonymous
6753        // function spaces, which reset nesting and hid the construct from
6754        // the enclosing proc entirely.
6755        check_metrics::<IrulesParser>(
6756            "proc f {} {
6757    try {
6758        risky
6759    } on error {msg} {
6760        puts $msg
6761    } trap {POSIX} {msg} {
6762        if {1} {
6763            puts $msg
6764        }
6765    }
6766}",
6767            "foo.irule",
6768            |metric| {
6769                // on(+1) + trap(+1) + if at nesting 1 inside trap (+2) = 4.
6770                assert_eq!(metric.cognitive.cognitive_sum(), 4);
6771                assert_eq!(metric.cognitive.cognitive_max(), 4);
6772            },
6773        );
6774    }
6775
6776    #[test]
6777    fn tcl_irules_try_parity() {
6778        // The same single-handler `try` must score identically in Tcl
6779        // (flat `on`/`error` tokens under `try`, per-child nesting seed)
6780        // and iRules (a dedicated `on_handler` wrapper) — issue #1266.
6781        // handler(+1) + if at nesting 1 inside the handler (+2) = 3.
6782        let source = "proc f {} {
6783    try {
6784        risky
6785    } on error {msg} {
6786        if {1} {
6787            puts $msg
6788        }
6789    }
6790}";
6791        check_metrics::<TclParser>(source, "foo.tcl", |metric| {
6792            assert_eq!(metric.cognitive.cognitive_sum(), 3);
6793            assert_eq!(metric.cognitive.cognitive_max(), 3);
6794        });
6795        check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
6796            assert_eq!(metric.cognitive.cognitive_sum(), 3);
6797            assert_eq!(metric.cognitive.cognitive_max(), 3);
6798        });
6799    }
6800
6801    /// Drift marker for #1266 (lesson 34 / grammar-dispatch §2): the
6802    /// iRules `on_handler` / `trap_handler` kinds are emitted **only**
6803    /// under `try`.
6804    ///
6805    /// Statement-level `on <event> { … }` and `trap { … }` inside an event
6806    /// body are generic `command` nodes at the pinned grammar, which is
6807    /// what lets Cognitive and Cyclomatic charge those two kinds
6808    /// unconditionally as catch clauses. A bump that promoted the
6809    /// statement-level spelling to the same kinds would silently turn
6810    /// every one of those commands into a decision point — and, since the
6811    /// checker treats handler kinds as part of the `try` shape, would
6812    /// re-run the mistake #1266 fixed, where the handlers were read as
6813    /// `when`-style event handlers and opened function spaces of their
6814    /// own. Nothing else in the suite would go red.
6815    #[test]
6816    fn irules_try_handler_kinds_appear_only_under_try() {
6817        use std::path::PathBuf;
6818
6819        use crate::test_support::ast_has_kind_id;
6820
6821        let path = PathBuf::from("foo.irule");
6822
6823        let statement_level = "when HTTP_REQUEST {\n\
6824                               \x20   on scriptbody { log \"x\" }\n\
6825                               \x20   trap { log \"y\" }\n\
6826                               }\n";
6827        let parser = IrulesParser::new(statement_level.as_bytes().to_vec(), &path, None);
6828        // Non-vacuity: both spellings must really be in this parse, as
6829        // `command`s whose leading word is the handler keyword. Asserting
6830        // only the absence below would pass just as well against a source
6831        // the grammar rejected outright.
6832        let command_names: Vec<&str> = parser
6833            .root()
6834            .preorder()
6835            .filter(|node| node.kind_id() == Irules::Command as u16)
6836            .filter_map(|node| {
6837                node.child_by_field_name("name")
6838                    .and_then(|name| name.utf8_text(statement_level.as_bytes()))
6839            })
6840            .collect();
6841        assert!(
6842            command_names.contains(&"on") && command_names.contains(&"trap"),
6843            "statement-level `on` / `trap` no longer parse as commands: {command_names:?}",
6844        );
6845        assert!(
6846            !ast_has_kind_id(&parser, Irules::OnHandler as u16),
6847            "statement-level `on` now emits `on_handler`; re-derive the \
6848             Cognitive / Cyclomatic handler arms and the checker's `try` \
6849             shape before trusting them",
6850        );
6851        assert!(
6852            !ast_has_kind_id(&parser, Irules::TrapHandler as u16),
6853            "statement-level `trap` now emits `trap_handler`; re-derive the \
6854             Cognitive / Cyclomatic handler arms and the checker's `try` \
6855             shape before trusting them",
6856        );
6857
6858        // And the kinds are reachable, so the assertions above discriminate
6859        // rather than naming variants the grammar never emits at all.
6860        let under_try = "when HTTP_REQUEST {\n\
6861                         \x20   try { risky } on error {e} { log $e } trap {POSIX} {e} { log $e }\n\
6862                         }\n";
6863        let parser = IrulesParser::new(under_try.as_bytes().to_vec(), &path, None);
6864        assert!(
6865            ast_has_kind_id(&parser, Irules::OnHandler as u16),
6866            "`on error` under `try` no longer emits `on_handler`",
6867        );
6868        assert!(
6869            ast_has_kind_id(&parser, Irules::TrapHandler as u16),
6870            "`trap` under `try` no longer emits `trap_handler`",
6871        );
6872    }
6873
6874    #[test]
6875    fn lua_cognitive_no_cognitive() {
6876        // Top-level local assignment, no control flow → cognitive complexity is zero.
6877        check_metrics::<LuaParser>("local x = 42", "foo.lua", |metric| {
6878            insta::assert_json_snapshot!(
6879                metric.cognitive,
6880                @r#"
6881            {
6882              "sum": 0,
6883              "value": 0,
6884              "average": 0.0,
6885              "min": 0,
6886              "max": 0
6887            }
6888            "#
6889            );
6890        });
6891    }
6892
6893    #[test]
6894    fn lua_cognitive_simple_function() {
6895        // Two `if … and …` statements at function scope: each contributes
6896        // +1 (if) + 1 (and) = 2; total 4.
6897        check_metrics::<LuaParser>(
6898            "local function f(a, b, c, d)
6899    if a and b then
6900        return 1
6901    end
6902    if c and d then
6903        return 1
6904    end
6905end",
6906            "foo.lua",
6907            |metric| {
6908                insta::assert_json_snapshot!(
6909                    metric.cognitive,
6910                    @r#"
6911                {
6912                  "sum": 4,
6913                  "value": 0,
6914                  "average": 4.0,
6915                  "min": 0,
6916                  "max": 4
6917                }
6918                "#
6919                );
6920            },
6921        );
6922    }
6923
6924    #[test]
6925    fn lua_cognitive_sequence_same_booleans() {
6926        // Sequences of the same boolean operator count as a single increment.
6927        // `a and b and c` → +1 (one and-group), `a or b or c or d` → +1.
6928        // Plus +1 per `if` ⇒ 4 total.
6929        check_metrics::<LuaParser>(
6930            "local function f(a, b, c, d)
6931    if a and b and c then
6932        return 1
6933    end
6934    if a or b or c or d then
6935        return 1
6936    end
6937end",
6938            "foo.lua",
6939            |metric| {
6940                insta::assert_json_snapshot!(
6941                    metric.cognitive,
6942                    @r#"
6943                {
6944                  "sum": 4,
6945                  "value": 0,
6946                  "average": 4.0,
6947                  "min": 0,
6948                  "max": 4
6949                }
6950                "#
6951                );
6952            },
6953        );
6954    }
6955
6956    #[test]
6957    fn lua_cognitive_not_booleans() {
6958        // `not a and not b`: `not` does not contribute cognitive cost
6959        // on its own (issue #392); the single `and` between the two
6960        // negations contributes +1. if(+1) + and(+1) = 2.
6961        check_metrics::<LuaParser>(
6962            "local function f(a, b)
6963    if not a and not b then
6964        return 1
6965    end
6966end",
6967            "foo.lua",
6968            |metric| {
6969                insta::assert_json_snapshot!(
6970                    metric.cognitive,
6971                    @r#"
6972                {
6973                  "sum": 2,
6974                  "value": 0,
6975                  "average": 2.0,
6976                  "min": 0,
6977                  "max": 2
6978                }
6979                "#
6980                );
6981            },
6982        );
6983    }
6984
6985    #[test]
6986    fn lua_cognitive_sequence_different_booleans() {
6987        // Switching operator type increments again: `a and b or c`
6988        // → if(+1) + and(+1) + or(+1) = 3.
6989        check_metrics::<LuaParser>(
6990            "local function f(a, b, c)
6991    if a and b or c then
6992        return 1
6993    end
6994end",
6995            "foo.lua",
6996            |metric| {
6997                insta::assert_json_snapshot!(
6998                    metric.cognitive,
6999                    @r#"
7000                {
7001                  "sum": 3,
7002                  "value": 0,
7003                  "average": 3.0,
7004                  "min": 0,
7005                  "max": 3
7006                }
7007                "#
7008                );
7009            },
7010        );
7011    }
7012
7013    #[test]
7014    fn lua_cognitive_1_level_nesting() {
7015        // for at depth 0 (+1) + if at depth 1 (+2) = 3.
7016        check_metrics::<LuaParser>(
7017            "local function f(t)
7018    for i = 1, #t do
7019        if t[i] > 0 then
7020            return t[i]
7021        end
7022    end
7023end",
7024            "foo.lua",
7025            |metric| {
7026                insta::assert_json_snapshot!(
7027                    metric.cognitive,
7028                    @r#"
7029                {
7030                  "sum": 3,
7031                  "value": 0,
7032                  "average": 3.0,
7033                  "min": 0,
7034                  "max": 3
7035                }
7036                "#
7037                );
7038            },
7039        );
7040    }
7041
7042    #[test]
7043    fn lua_cognitive_2_level_nesting() {
7044        // outer for (+1) + inner for at depth 1 (+2) + if at depth 2 (+3) = 6.
7045        check_metrics::<LuaParser>(
7046            "local function f(t)
7047    for i = 1, #t do
7048        for j = 1, #t do
7049            if t[i] > t[j] then
7050                return t[i]
7051            end
7052        end
7053    end
7054end",
7055            "foo.lua",
7056            |metric| {
7057                insta::assert_json_snapshot!(
7058                    metric.cognitive,
7059                    @r#"
7060                {
7061                  "sum": 6,
7062                  "value": 0,
7063                  "average": 6.0,
7064                  "min": 0,
7065                  "max": 6
7066                }
7067                "#
7068                );
7069            },
7070        );
7071    }
7072
7073    #[test]
7074    fn lua_cognitive_break_continue() {
7075        // Lua's `break` is always unlabeled (the grammar has no labeled
7076        // break and no `continue`), so per SonarSource Cognitive Complexity
7077        // §B2 it adds +0 — issue #435. for(+1) + if at depth 1 (+2) = 3.
7078        check_metrics::<LuaParser>(
7079            "local function f(t)
7080    for i = 1, #t do
7081        if t[i] < 0 then
7082            break
7083        end
7084    end
7085end",
7086            "foo.lua",
7087            |metric| {
7088                assert_eq!(metric.cognitive.cognitive_sum(), 3);
7089                insta::assert_json_snapshot!(
7090                    metric.cognitive,
7091                    @r#"
7092                {
7093                  "sum": 3,
7094                  "value": 0,
7095                  "average": 3.0,
7096                  "min": 0,
7097                  "max": 3
7098                }
7099                "#
7100                );
7101            },
7102        );
7103    }
7104
7105    #[test]
7106    fn lua_cognitive_goto_counted() {
7107        // `goto label` is a genuinely unstructured jump and adds +1 per
7108        // SonarSource §B2, even though Lua's unlabeled `break` does not
7109        // (issue #435). Only the `goto` contributes: +1.
7110        check_metrics::<LuaParser>(
7111            "local function f()
7112    ::top::
7113    goto top
7114end",
7115            "foo.lua",
7116            |metric| {
7117                assert_eq!(metric.cognitive.cognitive_sum(), 1);
7118                insta::assert_json_snapshot!(
7119                    metric.cognitive,
7120                    @r#"
7121                {
7122                  "sum": 1,
7123                  "value": 0,
7124                  "average": 1.0,
7125                  "min": 0,
7126                  "max": 1
7127                }
7128                "#
7129                );
7130            },
7131        );
7132    }
7133
7134    #[test]
7135    fn lua_cognitive_elseif_nesting() {
7136        // Lua-specific: `elseif_statement` is a dedicated grammar node that
7137        // stays at the same nesting level as the enclosing `if`. Chain:
7138        // if(+1) + elseif(+1) + elseif(+1) + else(+1) = 4.
7139        check_metrics::<LuaParser>(
7140            "local function classify(x)
7141    if x > 0 then
7142        return 1
7143    elseif x < 0 then
7144        return -1
7145    elseif x == 0 then
7146        return 0
7147    else
7148        return 0
7149    end
7150end",
7151            "foo.lua",
7152            |metric| {
7153                insta::assert_json_snapshot!(
7154                    metric.cognitive,
7155                    @r#"
7156                {
7157                  "sum": 4,
7158                  "value": 0,
7159                  "average": 4.0,
7160                  "min": 0,
7161                  "max": 4
7162                }
7163                "#
7164                );
7165            },
7166        );
7167    }
7168
7169    #[test]
7170    fn typescript_switch_statement() {
7171        check_metrics::<TypescriptParser>(
7172            "function describe(x: number): string {
7173                 switch (x) {   // +1
7174                     case 1:
7175                         return 'one';
7176                     case 2:
7177                         return 'two';
7178                     default:
7179                         return 'other';
7180                 }
7181             }",
7182            "foo.ts",
7183            |metric| {
7184                assert_eq!(metric.cognitive.cognitive_sum(), 1);
7185                assert_eq!(metric.cognitive.cognitive_max(), 1);
7186                insta::assert_json_snapshot!(metric.cognitive);
7187            },
7188        );
7189    }
7190
7191    #[test]
7192    fn typescript_no_cognitive() {
7193        check_metrics::<TypescriptParser>(
7194            "function f(a: number, b: number): number {
7195                 return a + b;
7196             }",
7197            "foo.ts",
7198            |metric| {
7199                assert_eq!(metric.cognitive.cognitive_sum(), 0);
7200                assert_eq!(metric.cognitive.cognitive_max(), 0);
7201                insta::assert_json_snapshot!(metric.cognitive);
7202            },
7203        );
7204    }
7205
7206    #[test]
7207    fn tsx_no_cognitive() {
7208        check_metrics::<TsxParser>(
7209            "function f(a: number, b: number): number {
7210                 return a + b;
7211             }",
7212            "foo.tsx",
7213            |metric| {
7214                assert_eq!(metric.cognitive.cognitive_sum(), 0);
7215                assert_eq!(metric.cognitive.cognitive_max(), 0);
7216                insta::assert_json_snapshot!(metric.cognitive);
7217            },
7218        );
7219    }
7220
7221    #[test]
7222    fn tsx_simple_if() {
7223        check_metrics::<TsxParser>(
7224            "function f(x: number): number {
7225                 if (x > 0) {  // +1
7226                     return x;
7227                 }
7228                 return 0;
7229             }",
7230            "foo.tsx",
7231            |metric| {
7232                assert_eq!(metric.cognitive.cognitive_sum(), 1);
7233                assert_eq!(metric.cognitive.cognitive_max(), 1);
7234                insta::assert_json_snapshot!(metric.cognitive);
7235            },
7236        );
7237    }
7238
7239    #[test]
7240    fn tsx_boolean_sequence() {
7241        check_metrics::<TsxParser>(
7242            "function f(a: boolean, b: boolean, c: boolean): boolean {
7243                 return a && b && c;  // +1 (&&, sequence)
7244             }",
7245            "foo.tsx",
7246            |metric| {
7247                assert_eq!(metric.cognitive.cognitive_sum(), 1);
7248                assert_eq!(metric.cognitive.cognitive_max(), 1);
7249                insta::assert_json_snapshot!(metric.cognitive);
7250            },
7251        );
7252    }
7253
7254    #[test]
7255    fn tsx_2_level_nesting() {
7256        check_metrics::<TsxParser>(
7257            "function f(a: number[], n: number): number {
7258                 for (let i = 0; i < a.length; i++) {  // +1
7259                     if (a[i] > n) {  // +2 (nesting=1)
7260                         return a[i];
7261                     }
7262                 }
7263                 return -1;
7264             }",
7265            "foo.tsx",
7266            |metric| {
7267                // for(+1) + if at depth 1 (+2) = 3.
7268                assert_eq!(metric.cognitive.cognitive_sum(), 3);
7269                assert_eq!(metric.cognitive.cognitive_max(), 3);
7270                insta::assert_json_snapshot!(metric.cognitive);
7271            },
7272        );
7273    }
7274
7275    #[test]
7276    fn tsx_else_if_chain() {
7277        check_metrics::<TsxParser>(
7278            "function classify(x: number): string {
7279                 if (x < 0) {         // +1
7280                     return 'neg';
7281                 } else if (x === 0) { // +1 (else if = structural, not nesting)
7282                     return 'zero';
7283                 } else {              // +1
7284                     return 'pos';
7285                 }
7286             }",
7287            "foo.tsx",
7288            |metric| {
7289                // if(+1) + else-if(+1) + else(+1) = 3.
7290                assert_eq!(metric.cognitive.cognitive_sum(), 3);
7291                assert_eq!(metric.cognitive.cognitive_max(), 3);
7292                insta::assert_json_snapshot!(metric.cognitive);
7293            },
7294        );
7295    }
7296
7297    #[test]
7298    fn js_sibling_bool_sequences() {
7299        // (a&&b)||(c&&d) — the right-hand && is a *new* sequence (sibling, not nested),
7300        // so it should score +1, giving a total of 3 (&&, ||, &&).
7301        // The pre-existing bug stored only (kind_id) and treated the right && as a
7302        // continuation of the earlier && sequence, incorrectly yielding 2.
7303        check_metrics::<JavascriptParser>(
7304            "function f(a, b, c, d) {
7305                 return (a && b) || (c && d);  // +1(&&) +1(||) +1(&&) = 3
7306             }",
7307            "foo.js",
7308            |metric| {
7309                assert_eq!(metric.cognitive.cognitive_sum(), 3);
7310                assert_eq!(metric.cognitive.cognitive_max(), 3);
7311                insta::assert_json_snapshot!(metric.cognitive);
7312            },
7313        );
7314    }
7315
7316    #[test]
7317    fn js_nested_bool_same_op() {
7318        // a||(b&&c&&d) — the inner && operators are nested inside ||, so they form
7319        // one sequence and only the first should score +1. Total = 2 (||, &&).
7320        check_metrics::<JavascriptParser>(
7321            "function f(a, b, c, d) {
7322                 return a || (b && c && d);  // +1(||) +1(&&) = 2
7323             }",
7324            "foo.js",
7325            |metric| {
7326                assert_eq!(metric.cognitive.cognitive_sum(), 2);
7327                assert_eq!(metric.cognitive.cognitive_max(), 2);
7328                insta::assert_json_snapshot!(metric.cognitive);
7329            },
7330        );
7331    }
7332
7333    #[test]
7334    fn python_sibling_bool_sequences() {
7335        // Python uses keyword boolean operators (`and`/`or`), routed through a
7336        // different `T` instantiation of `compute_booleans` than the JS `&&`/`||`
7337        // tests. Verifies the sibling-detection fix applies across operator kinds.
7338        // (a and b) or (c and d) — the right-hand `and` is a sibling, not nested.
7339        // Expected: and_left(+1) + or(+1) + and_right(+1) = 3.
7340        check_metrics::<PythonParser>(
7341            "def f(a, b, c, d):
7342                 return (a and b) or (c and d)  # +1(and) +1(or) +1(and) = 3
7343             ",
7344            "foo.py",
7345            |metric| {
7346                assert_eq!(metric.cognitive.cognitive_sum(), 3);
7347                assert_eq!(metric.cognitive.cognitive_max(), 3);
7348                insta::assert_json_snapshot!(metric.cognitive);
7349            },
7350        );
7351    }
7352
7353    #[test]
7354    fn python_nested_bool_same_op() {
7355        // a or (b and c and d) — the inner `and` operators are nested inside `or`,
7356        // forming one sequence. Expected: or(+1) + and(+1) = 2.
7357        check_metrics::<PythonParser>(
7358            "def f(a, b, c, d):
7359                 return a or (b and c and d)  # +1(or) +1(and) = 2
7360             ",
7361            "foo.py",
7362            |metric| {
7363                assert_eq!(metric.cognitive.cognitive_sum(), 2);
7364                assert_eq!(metric.cognitive.cognitive_max(), 2);
7365                insta::assert_json_snapshot!(metric.cognitive);
7366            },
7367        );
7368    }
7369
7370    #[test]
7371    fn perl_sibling_bool_sequences() {
7372        // Perl uses `compute_perl_booleans` (a separate function supporting five
7373        // operator kinds including `//`). Verifies the sibling-detection fix also
7374        // covers that code path.
7375        // ($a && $b) || ($c && $d) — the right-hand `&&` is a sibling.
7376        // Expected: &&(+1) + ||(+1) + &&(+1) = 3.
7377        check_metrics::<PerlParser>(
7378            "sub f {
7379                 my ($a, $b, $c, $d) = @_;
7380                 return ($a && $b) || ($c && $d);  # +1(&&) +1(||) +1(&&) = 3
7381             }",
7382            "foo.pl",
7383            |metric| {
7384                assert_eq!(metric.cognitive.cognitive_sum(), 3);
7385                assert_eq!(metric.cognitive.cognitive_max(), 3);
7386                insta::assert_json_snapshot!(metric.cognitive);
7387            },
7388        );
7389    }
7390
7391    #[test]
7392    fn perl_nested_bool_same_op() {
7393        // $a || ($b && $c && $d) — the inner `&&` operators are nested inside `||`,
7394        // forming one sequence. Exercises the `compute_perl_booleans` continuation
7395        // guard (the only path distinct from `compute_booleans`).
7396        // Expected: ||(+1) + &&(+1) = 2.
7397        check_metrics::<PerlParser>(
7398            "sub f {
7399                 my ($a, $b, $c, $d) = @_;
7400                 return $a || ($b && $c && $d);  # +1(||) +1(&&) = 2
7401             }",
7402            "foo.pl",
7403            |metric| {
7404                assert_eq!(metric.cognitive.cognitive_sum(), 2);
7405                assert_eq!(metric.cognitive.cognitive_max(), 2);
7406                insta::assert_json_snapshot!(metric.cognitive);
7407            },
7408        );
7409    }
7410
7411    #[test]
7412    fn rust_sibling_bool_sequences() {
7413        // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested.
7414        // Expected: &&(+1) + ||(+1) + &&(+1) = 3.
7415        check_metrics::<RustParser>(
7416            "fn f(a: bool, b: bool, c: bool, d: bool) -> bool {
7417                 (a && b) || (c && d)  // +1(&&) +1(||) +1(&&) = 3
7418             }",
7419            "foo.rs",
7420            |metric| {
7421                assert_eq!(metric.cognitive.cognitive_sum(), 3);
7422                assert_eq!(metric.cognitive.cognitive_max(), 3);
7423                insta::assert_json_snapshot!(metric.cognitive);
7424            },
7425        );
7426    }
7427
7428    #[test]
7429    fn rust_nested_bool_same_op() {
7430        // a||(b&&c&&d) — the inner && operators are nested, forming one sequence.
7431        // Expected: ||(+1) + &&(+1) = 2.
7432        check_metrics::<RustParser>(
7433            "fn f(a: bool, b: bool, c: bool, d: bool) -> bool {
7434                 a || (b && c && d)  // +1(||) +1(&&) = 2
7435             }",
7436            "foo.rs",
7437            |metric| {
7438                assert_eq!(metric.cognitive.cognitive_sum(), 2);
7439                assert_eq!(metric.cognitive.cognitive_max(), 2);
7440                insta::assert_json_snapshot!(metric.cognitive);
7441            },
7442        );
7443    }
7444
7445    #[test]
7446    fn c_sibling_bool_sequences() {
7447        // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested.
7448        // Expected: &&(+1) + ||(+1) + &&(+1) = 3.
7449        check_metrics::<CParser>(
7450            "int f(int a, int b, int c, int d) {
7451                 return (a && b) || (c && d);  // +1(&&) +1(||) +1(&&) = 3
7452             }",
7453            "foo.c",
7454            |metric| {
7455                assert_eq!(metric.cognitive.cognitive_sum(), 3);
7456                assert_eq!(metric.cognitive.cognitive_max(), 3);
7457                insta::assert_json_snapshot!(metric.cognitive);
7458            },
7459        );
7460    }
7461
7462    #[test]
7463    fn c_nested_bool_same_op() {
7464        // a||(b&&c&&d) — the inner && operators are nested, forming one sequence.
7465        // Expected: ||(+1) + &&(+1) = 2.
7466        check_metrics::<CParser>(
7467            "int f(int a, int b, int c, int d) {
7468                 return a || (b && c && d);  // +1(||) +1(&&) = 2
7469             }",
7470            "foo.c",
7471            |metric| {
7472                assert_eq!(metric.cognitive.cognitive_sum(), 2);
7473                assert_eq!(metric.cognitive.cognitive_max(), 2);
7474                insta::assert_json_snapshot!(metric.cognitive);
7475            },
7476        );
7477    }
7478
7479    #[test]
7480    fn mozjs_sibling_bool_sequences() {
7481        // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested.
7482        // Expected: &&(+1) + ||(+1) + &&(+1) = 3.
7483        check_metrics::<MozjsParser>(
7484            "function f(a, b, c, d) {
7485                 return (a && b) || (c && d);  // +1(&&) +1(||) +1(&&) = 3
7486             }",
7487            "foo.js",
7488            |metric| {
7489                assert_eq!(metric.cognitive.cognitive_sum(), 3);
7490                assert_eq!(metric.cognitive.cognitive_max(), 3);
7491                insta::assert_json_snapshot!(metric.cognitive);
7492            },
7493        );
7494    }
7495
7496    #[test]
7497    fn mozjs_nested_bool_same_op() {
7498        // a||(b&&c&&d) — the inner && operators are nested, forming one sequence.
7499        // Expected: ||(+1) + &&(+1) = 2.
7500        check_metrics::<MozjsParser>(
7501            "function f(a, b, c, d) {
7502                 return a || (b && c && d);  // +1(||) +1(&&) = 2
7503             }",
7504            "foo.js",
7505            |metric| {
7506                assert_eq!(metric.cognitive.cognitive_sum(), 2);
7507                assert_eq!(metric.cognitive.cognitive_max(), 2);
7508                insta::assert_json_snapshot!(metric.cognitive);
7509            },
7510        );
7511    }
7512
7513    #[test]
7514    fn typescript_sibling_bool_sequences() {
7515        // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested.
7516        // Expected: &&(+1) + ||(+1) + &&(+1) = 3.
7517        check_metrics::<TypescriptParser>(
7518            "function f(a: boolean, b: boolean, c: boolean, d: boolean): boolean {
7519                 return (a && b) || (c && d);  // +1(&&) +1(||) +1(&&) = 3
7520             }",
7521            "foo.ts",
7522            |metric| {
7523                assert_eq!(metric.cognitive.cognitive_sum(), 3);
7524                assert_eq!(metric.cognitive.cognitive_max(), 3);
7525                insta::assert_json_snapshot!(metric.cognitive);
7526            },
7527        );
7528    }
7529
7530    #[test]
7531    fn typescript_nested_bool_same_op() {
7532        // a||(b&&c&&d) — the inner && operators are nested, forming one sequence.
7533        // Expected: ||(+1) + &&(+1) = 2.
7534        check_metrics::<TypescriptParser>(
7535            "function f(a: boolean, b: boolean, c: boolean, d: boolean): boolean {
7536                 return a || (b && c && d);  // +1(||) +1(&&) = 2
7537             }",
7538            "foo.ts",
7539            |metric| {
7540                assert_eq!(metric.cognitive.cognitive_sum(), 2);
7541                assert_eq!(metric.cognitive.cognitive_max(), 2);
7542                insta::assert_json_snapshot!(metric.cognitive);
7543            },
7544        );
7545    }
7546
7547    #[test]
7548    fn tsx_sibling_bool_sequences() {
7549        // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested.
7550        // Expected: &&(+1) + ||(+1) + &&(+1) = 3.
7551        check_metrics::<TsxParser>(
7552            "function f(a: boolean, b: boolean, c: boolean, d: boolean): boolean {
7553                 return (a && b) || (c && d);  // +1(&&) +1(||) +1(&&) = 3
7554             }",
7555            "foo.tsx",
7556            |metric| {
7557                assert_eq!(metric.cognitive.cognitive_sum(), 3);
7558                assert_eq!(metric.cognitive.cognitive_max(), 3);
7559                insta::assert_json_snapshot!(metric.cognitive);
7560            },
7561        );
7562    }
7563
7564    #[test]
7565    fn tsx_nested_bool_same_op() {
7566        // a||(b&&c&&d) — the inner && operators are nested, forming one sequence.
7567        // Expected: ||(+1) + &&(+1) = 2.
7568        check_metrics::<TsxParser>(
7569            "function f(a: boolean, b: boolean, c: boolean, d: boolean): boolean {
7570                 return a || (b && c && d);  // +1(||) +1(&&) = 2
7571             }",
7572            "foo.tsx",
7573            |metric| {
7574                assert_eq!(metric.cognitive.cognitive_sum(), 2);
7575                assert_eq!(metric.cognitive.cognitive_max(), 2);
7576                insta::assert_json_snapshot!(metric.cognitive);
7577            },
7578        );
7579    }
7580
7581    #[test]
7582    fn javascript_nullish_coalescing_chain_230() {
7583        // Regression for issue #230: `??` is a short-circuit operator and
7584        // must form a boolean sequence. `a ?? b ?? c` is a single chain
7585        // of identical operators and collapses to a single +1 under
7586        // Sonar B1 (same rule as `&&` / `||`).
7587        check_metrics::<JavascriptParser>(
7588            "function pick(a, b, c) {
7589                 return a ?? b ?? c; // +1 (chain of ??)
7590             }",
7591            "foo.js",
7592            |metric| {
7593                assert_eq!(metric.cognitive.cognitive_sum(), 1);
7594                assert_eq!(metric.cognitive.cognitive_max(), 1);
7595                insta::assert_json_snapshot!(
7596                    metric.cognitive,
7597                    @r#"
7598                {
7599                  "sum": 1,
7600                  "value": 0,
7601                  "average": 1.0,
7602                  "min": 0,
7603                  "max": 1
7604                }
7605                "#
7606                );
7607            },
7608        );
7609    }
7610
7611    #[test]
7612    fn typescript_nullish_coalescing_with_if_230() {
7613        // Regression for issue #230: the example from the issue body.
7614        // Boolean sequences pay a flat +1 (no nesting penalty) per Sonar
7615        // B1, so the issue body's stated total of 3 was wrong — the
7616        // correct answer is if(+1) + ?? chain (+1) = 2. Previously the
7617        // `??` chain was not counted at all (= 1).
7618        check_metrics::<TypescriptParser>(
7619            "function risky(x: string | null, fallback: string | null): string {
7620                 if (x === \"y\") { // +1
7621                     return x ?? fallback ?? \"unknown\"; // +1 (chain of ??)
7622                 }
7623                 return \"no\";
7624             }",
7625            "foo.ts",
7626            |metric| {
7627                assert_eq!(metric.cognitive.cognitive_sum(), 2);
7628                assert_eq!(metric.cognitive.cognitive_max(), 2);
7629                insta::assert_json_snapshot!(
7630                    metric.cognitive,
7631                    @r#"
7632                {
7633                  "sum": 2,
7634                  "value": 0,
7635                  "average": 2.0,
7636                  "min": 0,
7637                  "max": 2
7638                }
7639                "#
7640                );
7641            },
7642        );
7643    }
7644
7645    #[test]
7646    fn tsx_nullish_coalescing_chain_230() {
7647        // Regression for issue #230: TSX parity with JS/TS for `??`.
7648        check_metrics::<TsxParser>(
7649            "function pick(a: number | null, b: number | null, c: number): number {
7650                 return a ?? b ?? c; // +1 (chain of ??)
7651             }",
7652            "foo.tsx",
7653            |metric| {
7654                assert_eq!(metric.cognitive.cognitive_sum(), 1);
7655                assert_eq!(metric.cognitive.cognitive_max(), 1);
7656                insta::assert_json_snapshot!(
7657                    metric.cognitive,
7658                    @r#"
7659                {
7660                  "sum": 1,
7661                  "value": 0,
7662                  "average": 1.0,
7663                  "min": 0,
7664                  "max": 1
7665                }
7666                "#
7667                );
7668            },
7669        );
7670    }
7671
7672    #[test]
7673    fn mozjs_nullish_coalescing_chain_230() {
7674        // Regression for issue #230: Mozjs parity with JS for `??`.
7675        check_metrics::<MozjsParser>(
7676            "function pick(a, b, c) {
7677                 return a ?? b ?? c; // +1 (chain of ??)
7678             }",
7679            "foo.js",
7680            |metric| {
7681                assert_eq!(metric.cognitive.cognitive_sum(), 1);
7682                assert_eq!(metric.cognitive.cognitive_max(), 1);
7683                insta::assert_json_snapshot!(
7684                    metric.cognitive,
7685                    @r#"
7686                {
7687                  "sum": 1,
7688                  "value": 0,
7689                  "average": 1.0,
7690                  "min": 0,
7691                  "max": 1
7692                }
7693                "#
7694                );
7695            },
7696        );
7697    }
7698
7699    #[test]
7700    fn csharp_null_coalescing_cognitive_230() {
7701        // Regression for issue #230: C# `??` must form a boolean sequence
7702        // just like `&&` / `||`. Boolean sequences pay a flat +1 (no
7703        // nesting penalty) per Sonar B1.
7704        // if(+1) + ?? chain (+1) = 2. Previously the `??` chain
7705        // contributed nothing and the function scored 1.
7706        check_metrics::<CsharpParser>(
7707            "class C {
7708                 string Risky(string x, string fallback) {
7709                     if (x == \"y\") { // +1
7710                         return x ?? fallback ?? \"unknown\"; // +1 (chain of ??)
7711                     }
7712                     return \"no\";
7713                 }
7714             }",
7715            "foo.cs",
7716            |metric| {
7717                assert_eq!(metric.cognitive.cognitive_sum(), 2);
7718                assert_eq!(metric.cognitive.cognitive_max(), 2);
7719                insta::assert_json_snapshot!(
7720                    metric.cognitive,
7721                    @r#"
7722                {
7723                  "sum": 2,
7724                  "value": 0,
7725                  "average": 2.0,
7726                  "min": 0,
7727                  "max": 2
7728                }
7729                "#
7730                );
7731            },
7732        );
7733    }
7734
7735    #[test]
7736    fn php_null_coalescing_cognitive_230() {
7737        // Regression for issue #230: PHP `??` must form a boolean sequence
7738        // just like `&&` / `||`. Parallels the PHP cyclomatic
7739        // null-coalescing handling. Boolean sequences pay a flat +1 (no
7740        // nesting penalty) per Sonar B1.
7741        // if(+1) + ?? chain (+1) = 2.
7742        check_metrics::<PhpParser>(
7743            "<?php
7744            function risky($x, $fallback) {
7745                if ($x === \"y\") { // +1
7746                    return $x ?? $fallback ?? \"unknown\"; // +1 (chain of ??)
7747                }
7748                return \"no\";
7749            }",
7750            "foo.php",
7751            |metric| {
7752                assert_eq!(metric.cognitive.cognitive_sum(), 2);
7753                assert_eq!(metric.cognitive.cognitive_max(), 2);
7754                insta::assert_json_snapshot!(
7755                    metric.cognitive,
7756                    @r#"
7757                {
7758                  "sum": 2,
7759                  "value": 0,
7760                  "average": 2.0,
7761                  "min": 0,
7762                  "max": 2
7763                }
7764                "#
7765                );
7766            },
7767        );
7768    }
7769
7770    // Companions to `php_null_coalescing_cognitive_230`: the PHP
7771    // cognitive operator set extends past `&&` / `||` / `??` to include
7772    // the word-form `and` / `or` / `xor`, mirroring PHP cyclomatic. A
7773    // chain of identical word-form operators collapses to a single
7774    // boolean-sequence increment under Sonar B1, the same way `&&` /
7775    // `||` chains do. Each word-form gets its own test so a regression
7776    // that drops a single variant (e.g. only `Or`) is still caught.
7777
7778    #[test]
7779    fn php_word_form_and_forms_boolean_sequence_230() {
7780        check_metrics::<PhpParser>(
7781            "<?php
7782            function check_and($a, $b, $c, $d) {
7783                if ($a and $b and $c and $d) { // +1 (if) + 1 (and chain)
7784                    return true;
7785                }
7786                return false;
7787            }",
7788            "foo.php",
7789            |metric| {
7790                assert_eq!(metric.cognitive.cognitive_sum(), 2);
7791                assert_eq!(metric.cognitive.cognitive_max(), 2);
7792            },
7793        );
7794    }
7795
7796    #[test]
7797    fn php_word_form_or_forms_boolean_sequence_230() {
7798        check_metrics::<PhpParser>(
7799            "<?php
7800            function check_or($a, $b, $c, $d) {
7801                if ($a or $b or $c or $d) { // +1 (if) + 1 (or chain)
7802                    return true;
7803                }
7804                return false;
7805            }",
7806            "foo.php",
7807            |metric| {
7808                assert_eq!(metric.cognitive.cognitive_sum(), 2);
7809                assert_eq!(metric.cognitive.cognitive_max(), 2);
7810            },
7811        );
7812    }
7813
7814    #[test]
7815    fn php_word_form_xor_forms_boolean_sequence_230() {
7816        check_metrics::<PhpParser>(
7817            "<?php
7818            function check_xor($a, $b, $c, $d) {
7819                if ($a xor $b xor $c xor $d) { // +1 (if) + 1 (xor chain)
7820                    return true;
7821                }
7822                return false;
7823            }",
7824            "foo.php",
7825            |metric| {
7826                assert_eq!(metric.cognitive.cognitive_sum(), 2);
7827                assert_eq!(metric.cognitive.cognitive_max(), 2);
7828            },
7829        );
7830    }
7831
7832    #[test]
7833    fn java_cognitive_else_if_chain() {
7834        // Regression for #115: else-if chains must not receive a nesting
7835        // increment for the `if` inside `else if`. Expected breakdown:
7836        // if(+1) + else(+1) + else(+1) + else(+1) = 4.
7837        check_metrics::<JavaParser>(
7838            "class X {
7839                public static void f(int x) {
7840                    if (x > 10) {
7841                    } else if (x > 5) {
7842                    } else if (x > 0) {
7843                    } else {
7844                    }
7845                }
7846            }",
7847            "foo.java",
7848            |metric| {
7849                insta::assert_json_snapshot!(
7850                    metric.cognitive,
7851                    @r#"
7852                {
7853                  "sum": 4,
7854                  "value": 0,
7855                  "average": 4.0,
7856                  "min": 0,
7857                  "max": 4
7858                }
7859                "#
7860                );
7861            },
7862        );
7863    }
7864
7865    #[test]
7866    fn java_cognitive_nested_else_if() {
7867        // Regression for #115: else-if inside a loop must still respect
7868        // the loop's nesting for the initial `if`, but the `else if`
7869        // branch should only pay a flat +1 via the `else` keyword.
7870        // for(+1) + if at nesting=1(+2) + else(+1) + else(+1) = 5.
7871        check_metrics::<JavaParser>(
7872            "class X {
7873                public static void f(int x) {
7874                    for (int i = 0; i < x; i++) {
7875                        if (i > 10) {
7876                        } else if (i > 5) {
7877                        } else {
7878                        }
7879                    }
7880                }
7881            }",
7882            "foo.java",
7883            |metric| {
7884                insta::assert_json_snapshot!(
7885                    metric.cognitive,
7886                    @r#"
7887                {
7888                  "sum": 5,
7889                  "value": 0,
7890                  "average": 5.0,
7891                  "min": 0,
7892                  "max": 5
7893                }
7894                "#
7895                );
7896            },
7897        );
7898    }
7899
7900    #[test]
7901    fn java_cognitive_if_inside_else_block_is_not_else_if() {
7902        // Regression for #115: an `if` whose previous sibling is the block's
7903        // opening brace (not the `else` keyword) is a nested independent
7904        // statement, NOT an else-if continuation. It must pay the full
7905        // nesting penalty.
7906        // if(+1, nesting=0) + else(+1) + inner if(+2, nesting=1) = 4.
7907        check_metrics::<JavaParser>(
7908            "class X {
7909                public static void f(int a, int c) {
7910                    if (a > 0) {
7911                    } else {
7912                        if (c > 0) {
7913                        }
7914                    }
7915                }
7916            }",
7917            "foo.java",
7918            |metric| {
7919                insta::assert_json_snapshot!(
7920                    metric.cognitive,
7921                    @r#"
7922                {
7923                  "sum": 4,
7924                  "value": 0,
7925                  "average": 4.0,
7926                  "min": 0,
7927                  "max": 4
7928                }
7929                "#
7930                );
7931            },
7932        );
7933    }
7934
7935    #[test]
7936    fn java_sibling_bool_sequences() {
7937        // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested.
7938        // Expected: &&(+1) + ||(+1) + &&(+1) = 3.
7939        check_metrics::<JavaParser>(
7940            "class X {
7941                 boolean f(boolean a, boolean b, boolean c, boolean d) {
7942                     return (a && b) || (c && d);  // +1(&&) +1(||) +1(&&) = 3
7943                 }
7944             }",
7945            "foo.java",
7946            |metric| {
7947                assert_eq!(metric.cognitive.cognitive_sum(), 3);
7948                assert_eq!(metric.cognitive.cognitive_max(), 3);
7949                insta::assert_json_snapshot!(metric.cognitive);
7950            },
7951        );
7952    }
7953
7954    #[test]
7955    fn java_nested_bool_same_op() {
7956        // a||(b&&c&&d) — the inner && operators are nested, forming one sequence.
7957        // Expected: ||(+1) + &&(+1) = 2.
7958        check_metrics::<JavaParser>(
7959            "class X {
7960                 boolean f(boolean a, boolean b, boolean c, boolean d) {
7961                     return a || (b && c && d);  // +1(||) +1(&&) = 2
7962                 }
7963             }",
7964            "foo.java",
7965            |metric| {
7966                assert_eq!(metric.cognitive.cognitive_sum(), 2);
7967                assert_eq!(metric.cognitive.cognitive_max(), 2);
7968                insta::assert_json_snapshot!(metric.cognitive);
7969            },
7970        );
7971    }
7972
7973    #[test]
7974    fn groovy_no_cognitive() {
7975        check_metrics::<GroovyParser>("class A { int x = 42 }", "foo.groovy", |metric| {
7976            assert_eq!(metric.cognitive.cognitive_sum(), 0);
7977        });
7978    }
7979
7980    #[test]
7981    fn groovy_single_branch_function() {
7982        check_metrics::<GroovyParser>(
7983            "void f(int x) {
7984                if (x > 0) {
7985                    println(x)
7986                }
7987            }",
7988            "foo.groovy",
7989            |metric| {
7990                // if = +1
7991                assert_eq!(metric.cognitive.cognitive_sum(), 1);
7992            },
7993        );
7994    }
7995
7996    #[test]
7997    fn groovy_nested_if() {
7998        check_metrics::<GroovyParser>(
7999            "void f(int x, int y) {
8000                if (x > 0) {
8001                    if (y > 0) {
8002                        println(x)
8003                    }
8004                }
8005            }",
8006            "foo.groovy",
8007            |metric| {
8008                // outer if (+1) + inner if (+2 for nesting depth 1) = 3
8009                assert_eq!(metric.cognitive.cognitive_sum(), 3);
8010            },
8011        );
8012    }
8013
8014    #[test]
8015    fn groovy_else_if_chain() {
8016        // Regression for the #115 / #239 stub pattern: an `else if`
8017        // chain must NOT receive a nesting increment for the `if`
8018        // inside `else if`. Without the sibling-`Else` pattern in
8019        // `Checker::is_else_if`, this would have scored higher.
8020        check_metrics::<GroovyParser>(
8021            "class X {
8022                static void f(int x) {
8023                    if (x > 10) {
8024                    } else if (x > 5) {
8025                    } else if (x > 0) {
8026                    } else {
8027                    }
8028                }
8029            }",
8030            "foo.groovy",
8031            |metric| {
8032                // if(+1) + else(+1) + else(+1) + else(+1) = 4
8033                assert_eq!(metric.cognitive.cognitive_sum(), 4);
8034            },
8035        );
8036    }
8037
8038    #[test]
8039    fn groovy_else_if_chain_lower_than_nested_ifs() {
8040        // The `else if` chain in `groovy_else_if_chain` MUST score
8041        // lower than an equivalent depth of nested `if` blocks — this
8042        // is the inequality the test exists to defend (lesson 10).
8043        check_metrics::<GroovyParser>(
8044            "class X {
8045                static void f(int x) {
8046                    if (x > 10) {
8047                        if (x > 5) {
8048                            if (x > 0) {
8049                            }
8050                        }
8051                    }
8052                }
8053            }",
8054            "foo.groovy",
8055            |metric| {
8056                // 3 nested `if`s: 1 + 2 + 3 = 6 (each deeper layer
8057                // pays a higher nesting cost). The chain in
8058                // `groovy_else_if_chain` produces 4, so this MUST
8059                // exceed it.
8060                assert!(metric.cognitive.cognitive_sum() > 4);
8061            },
8062        );
8063    }
8064
8065    #[test]
8066    fn groovy_sequence_booleans_same_op() {
8067        // SonarSource B1: a chain of identical short-circuit ops counts as one.
8068        check_metrics::<GroovyParser>(
8069            "void f(boolean a, boolean b, boolean c) {
8070                if (a && b && c) { println(a) }
8071            }",
8072            "foo.groovy",
8073            |metric| {
8074                // if (+1) + boolean sequence (+1) = 2
8075                assert_eq!(metric.cognitive.cognitive_sum(), 2);
8076            },
8077        );
8078    }
8079
8080    #[test]
8081    fn groovy_sequence_booleans_mixed_ops() {
8082        // A `&&` followed by `||` is two distinct sequences = +2.
8083        check_metrics::<GroovyParser>(
8084            "void f(boolean a, boolean b, boolean c) {
8085                if (a && b || c) { println(a) }
8086            }",
8087            "foo.groovy",
8088            |metric| {
8089                // if (+1) + && (+1) + || (+1) = 3
8090                assert_eq!(metric.cognitive.cognitive_sum(), 3);
8091            },
8092        );
8093    }
8094
8095    #[test]
8096    fn groovy_not_operator_negation() {
8097        // SonarSource: `!` negation flips a boolean sequence's polarity
8098        // but doesn't add cognitive cost on its own.
8099        check_metrics::<GroovyParser>(
8100            "void f(boolean a, boolean b) {
8101                if (a && !b) { println(a) }
8102            }",
8103            "foo.groovy",
8104            |metric| {
8105                // if(+1) + && (+1) = 2
8106                assert_eq!(metric.cognitive.cognitive_sum(), 2);
8107            },
8108        );
8109    }
8110
8111    #[test]
8112    fn groovy_for_while_do_loops() {
8113        check_metrics::<GroovyParser>(
8114            "void f(int n) {
8115                for (int i = 0; i < n; i++) {
8116                    while (i > 0) {
8117                        i--
8118                    }
8119                }
8120            }",
8121            "foo.groovy",
8122            |metric| {
8123                // for(+1) + while inside for(+2) = 3
8124                assert_eq!(metric.cognitive.cognitive_sum(), 3);
8125            },
8126        );
8127    }
8128
8129    #[test]
8130    fn groovy_enhanced_for() {
8131        check_metrics::<GroovyParser>(
8132            "void f(List items) {
8133                for (item in items) {
8134                    println(item)
8135                }
8136            }",
8137            "foo.groovy",
8138            |metric| {
8139                assert_eq!(metric.cognitive.cognitive_sum(), 1);
8140            },
8141        );
8142    }
8143
8144    #[test]
8145    fn groovy_try_catch_nesting() {
8146        check_metrics::<GroovyParser>(
8147            "void f() {
8148                try {
8149                    risky()
8150                } catch (Exception e) {
8151                    handle(e)
8152                }
8153            }",
8154            "foo.groovy",
8155            |metric| {
8156                // catch(+1) = 1
8157                assert_eq!(metric.cognitive.cognitive_sum(), 1);
8158            },
8159        );
8160    }
8161
8162    #[test]
8163    fn groovy_ternary_expression() {
8164        check_metrics::<GroovyParser>(
8165            "void f(int x) {
8166                def y = (x > 0) ? 1 : 2
8167            }",
8168            "foo.groovy",
8169            |metric| {
8170                // ternary(+1) = 1
8171                assert_eq!(metric.cognitive.cognitive_sum(), 1);
8172            },
8173        );
8174    }
8175
8176    #[test]
8177    fn groovy_elvis_chain_246() {
8178        // Regression for issue #246: Groovy's Elvis operator `?:` is
8179        // a short-circuit nullish operator analogous to Kotlin's `?:`
8180        // (#239) and JS `??`. `a ?: b ?: c` is a single chain of
8181        // identical operators and collapses to a single +1 under
8182        // SonarSource Cognitive Complexity B1 — the same rule applied
8183        // to `&&` / `||`. Closed by swapping the prior amaanq grammar
8184        // (which mis-parsed Elvis as `ternary_expression` + MISSING
8185        // identifier) for `dekobon-tree-sitter-groovy`, which models
8186        // Elvis as a distinct `elvis_expression` node.
8187        check_metrics::<GroovyParser>(
8188            "def pick(a, b, c) {
8189                return a ?: b ?: c // +1 (Elvis chain)
8190            }",
8191            "foo.groovy",
8192            |metric| {
8193                assert_eq!(metric.cognitive.cognitive_sum(), 1);
8194                assert_eq!(metric.cognitive.cognitive_max(), 1);
8195            },
8196        );
8197    }
8198
8199    #[test]
8200    fn groovy_elvis_inside_if_246() {
8201        // Regression for issue #246: Elvis chain inside an `if` body.
8202        // Boolean sequences pay a flat +1 (no nesting penalty) per
8203        // SonarSource B1: if(+1) + Elvis chain(+1) = 2.
8204        check_metrics::<GroovyParser>(
8205            "def f(a, b) {
8206                if (a != null) { // +1
8207                    return a ?: b ?: 'x' // +1 (Elvis chain)
8208                }
8209                return 'no'
8210            }",
8211            "foo.groovy",
8212            |metric| {
8213                assert_eq!(metric.cognitive.cognitive_sum(), 2);
8214                assert_eq!(metric.cognitive.cognitive_max(), 2);
8215            },
8216        );
8217    }
8218
8219    #[test]
8220    fn groovy_labeled_break_continue() {
8221        // SonarSource B2: labeled break/continue each add +1.
8222        check_metrics::<GroovyParser>(
8223            "void f() {
8224                outer:
8225                for (int i = 0; i < 10; i++) {
8226                    inner:
8227                    for (int j = 0; j < 10; j++) {
8228                        if (i == j) break outer
8229                        if (i < j) continue inner
8230                    }
8231                }
8232            }",
8233            "foo.groovy",
8234            |metric| {
8235                // for(+1) + for(+2 nested) + if(+3) + break label(+1)
8236                // + if(+3) + continue label(+1) = 11
8237                assert_eq!(metric.cognitive.cognitive_sum(), 11);
8238            },
8239        );
8240    }
8241
8242    #[test]
8243    fn groovy_multiple_branch_function() {
8244        // Sibling `if` statements at the same nesting level each
8245        // contribute +1; an `else` at the same level adds another
8246        // +1 via the Else arm.
8247        check_metrics::<GroovyParser>(
8248            "class X {
8249                static void print(boolean a, boolean b) {
8250                    if (a) {
8251                        println 'test1'
8252                    }
8253                    if (b) {
8254                        println 'test2'
8255                    } else {
8256                        println 'test3'
8257                    }
8258                }
8259            }",
8260            "foo.groovy",
8261            |metric| {
8262                // if(+1) + if(+1) + else(+1) = 3
8263                assert_eq!(metric.cognitive.cognitive_sum(), 3);
8264            },
8265        );
8266    }
8267
8268    #[test]
8269    fn groovy_unlabeled_break_continue_not_counted() {
8270        // SonarSource B2: plain `break` / `continue` are NOT
8271        // unstructured jumps and must add 0 — only labeled forms
8272        // pay the +1. Matches Java's identical fixture.
8273        check_metrics::<GroovyParser>(
8274            "class X {
8275                void scan(int[] m) {
8276                    for (int i = 0; i < m.length; i++) {
8277                        if (m[i] < 0) continue
8278                        if (m[i] > 100) break
8279                    }
8280                }
8281            }",
8282            "foo.groovy",
8283            |metric| {
8284                // for(+1) + if(+2) + if(+2) = 5 (break/continue add 0)
8285                assert_eq!(metric.cognitive.cognitive_sum(), 5);
8286            },
8287        );
8288    }
8289
8290    #[test]
8291    fn groovy_cognitive_closure_body_counts_lambda_nesting() {
8292        // #519: control flow inside a Groovy closure must pay the same
8293        // lambda-nesting surcharge as Java's `LambdaExpression`, so the
8294        // byte-equivalent construct scores identically across languages
8295        // (lesson #11). The byte-for-byte Java equivalent
8296        // (`list.forEach(item -> { if (a) { while (b) {} } })`) also
8297        // reports cognitive sum 5.0.
8298        //
8299        // Test-via-revert (.claude/rules/testing.md): removing the
8300        // `Closure => { lambda += 1; }` arm drops this to 3.0 — the
8301        // missing +2 lambda surcharge on the nested `if`/`while`.
8302        check_metrics::<GroovyParser>(
8303            "class X {
8304                static void f(java.util.List list, boolean a, boolean b) {
8305                    list.each { if (a) { while (b) {} } }
8306                }
8307            }",
8308            "foo.groovy",
8309            |metric| {
8310                // closure(lambda=1) -> if at nesting=1(+2)
8311                // -> while at nesting=2(+3) = 5
8312                assert_eq!(metric.cognitive.cognitive_sum(), 5);
8313            },
8314        );
8315    }
8316
8317    #[test]
8318    fn groovy_nested_method_resets_nesting_and_adds_depth() {
8319        // Regression for #696: a local-class method declared two `if`s deep
8320        // inside an outer method must reset nesting to 0 and gain a
8321        // function-depth surcharge — not inherit the enclosing nesting.
8322        //
8323        // expected: outer `if` (+1, nesting=0) + inner `if` (+2, nesting=1)
8324        // + Local.f's `if` (+1 base + 1 depth = +2, nesting=0, depth=1) = 5.
8325        // Before the fix, `f` inherited nesting=2 from the two enclosing
8326        // `if`s, scoring its inner `if` at nesting 2 (+3) for a sum of 6.
8327        // The two-deep nesting is load-bearing: one level deep, the
8328        // inherited nesting (1) coincidentally equals the depth bump (1).
8329        check_metrics::<GroovyParser>(
8330            "class Outer {
8331                void outer(boolean a) {
8332                    if (a) {
8333                        if (a) {
8334                            class Local {
8335                                void f(boolean b) {
8336                                    if (b) { g() }
8337                                }
8338                            }
8339                        }
8340                    }
8341                }
8342            }",
8343            "foo.groovy",
8344            |metric| {
8345                assert_eq!(metric.cognitive.cognitive_sum(), 5);
8346                assert_eq!(metric.cognitive.cognitive_max(), 3);
8347            },
8348        );
8349    }
8350
8351    #[test]
8352    fn groovy_cognitive_top_level_typed_method_parity() {
8353        // Regression for the upstream grammar defect
8354        // tree-sitter-groovy#20, fixed in =0.2.2: a top-level method
8355        // with an explicit return type whose body contained a `;`
8356        // (e.g. a C-style `for`) misparsed into identifier + call +
8357        // standalone closure, so it was not recognized as a function and
8358        // its body brace-block was a `Closure` — which the new lambda arm
8359        // would have spuriously surcharged. Post-fix the typed form must
8360        // parse as a real method and score identically to the `def` form.
8361        let typed = "void f(int n) {
8362            for (int i = 0; i < n; i++) {
8363                if (i > 0) { }
8364            }
8365        }";
8366        let untyped = "def f(int n) {
8367            for (int i = 0; i < n; i++) {
8368                if (i > 0) { }
8369            }
8370        }";
8371        // for(+1) + if at nesting=1(+2) = 3; no lambda surcharge because
8372        // the body is a `block`, not a misparsed `Closure`.
8373        check_metrics::<GroovyParser>(typed, "foo.groovy", |metric| {
8374            assert_eq!(metric.cognitive.cognitive_sum(), 3);
8375        });
8376        check_metrics::<GroovyParser>(untyped, "foo.groovy", |metric| {
8377            assert_eq!(metric.cognitive.cognitive_sum(), 3);
8378        });
8379    }
8380
8381    #[test]
8382    fn groovy_cognitive_nested_else_if() {
8383        // Regression for the #115 stub pattern at deeper nesting:
8384        // an `else if` chain inside a `for` loop must still respect
8385        // the loop's nesting for the initial `if`, but each
8386        // `else`-chained branch pays a flat +1 via the Else arm.
8387        // Matches Java's identical fixture.
8388        check_metrics::<GroovyParser>(
8389            "class X {
8390                static void f(int x) {
8391                    for (int i = 0; i < x; i++) {
8392                        if (i > 10) {
8393                        } else if (i > 5) {
8394                        } else {
8395                        }
8396                    }
8397                }
8398            }",
8399            "foo.groovy",
8400            |metric| {
8401                // for(+1) + if at nesting=1(+2) + else(+1) + else(+1) = 5
8402                assert_eq!(metric.cognitive.cognitive_sum(), 5);
8403            },
8404        );
8405    }
8406
8407    #[test]
8408    fn groovy_cognitive_if_inside_else_block_is_not_else_if() {
8409        // Regression for #115 — an inner `if` whose previous sibling
8410        // is the block's opening brace (not the `else` keyword) is a
8411        // nested independent statement, NOT an else-if continuation,
8412        // so it pays the full nesting penalty. Matches Java's
8413        // identical fixture.
8414        check_metrics::<GroovyParser>(
8415            "class X {
8416                static void f(int a, int c) {
8417                    if (a > 0) {
8418                    } else {
8419                        if (c > 0) {
8420                        }
8421                    }
8422                }
8423            }",
8424            "foo.groovy",
8425            |metric| {
8426                // if(+1, nesting=0) + else(+1) + inner if(+2, nesting=1) = 4
8427                assert_eq!(metric.cognitive.cognitive_sum(), 4);
8428            },
8429        );
8430    }
8431
8432    #[test]
8433    fn groovy_nested_ternary() {
8434        // Nested ternaries inside an `if` compound by nesting — same
8435        // rule as Java's `java_nested_ternary` (which itself mirrors
8436        // the C++ regression for #172).
8437        check_metrics::<GroovyParser>(
8438            "class X {
8439                static String classify(int a, int b) {
8440                    if (a > 0) {
8441                        return b > 0 ? (b > 10 ? 'big' : 'small') : 'neg'
8442                    }
8443                    return 'zero'
8444                }
8445            }",
8446            "foo.groovy",
8447            |metric| {
8448                // if(+1, nesting=0) + outer ternary(+1+1=+2, nesting=1)
8449                // + inner ternary(+1+2=+3, nesting=2) = 6
8450                assert_eq!(metric.cognitive.cognitive_sum(), 6);
8451            },
8452        );
8453    }
8454
8455    #[test]
8456    fn csharp_cognitive_else_if_chain() {
8457        // Regression for #115: else-if chains must not receive a nesting
8458        // increment for the `if` inside `else if`. Expected breakdown:
8459        // if(+1) + else(+1) + else(+1) + else(+1) = 4.
8460        check_metrics::<CsharpParser>(
8461            "class X {
8462                public static void F(int x) {
8463                    if (x > 10) {
8464                    } else if (x > 5) {
8465                    } else if (x > 0) {
8466                    } else {
8467                    }
8468                }
8469            }",
8470            "foo.cs",
8471            |metric| {
8472                insta::assert_json_snapshot!(
8473                    metric.cognitive,
8474                    @r#"
8475                {
8476                  "sum": 4,
8477                  "value": 0,
8478                  "average": 4.0,
8479                  "min": 0,
8480                  "max": 4
8481                }
8482                "#
8483                );
8484            },
8485        );
8486    }
8487
8488    #[test]
8489    fn csharp_cognitive_nested_else_if() {
8490        // Regression for #115: else-if inside a loop must still respect
8491        // the loop's nesting for the initial `if`, but the `else if`
8492        // branch should only pay a flat +1 via the `else` keyword.
8493        // for(+1) + if at nesting=1(+2) + else(+1) + else(+1) = 5.
8494        check_metrics::<CsharpParser>(
8495            "class X {
8496                public static void F(int x) {
8497                    for (int i = 0; i < x; i++) {
8498                        if (i > 10) {
8499                        } else if (i > 5) {
8500                        } else {
8501                        }
8502                    }
8503                }
8504            }",
8505            "foo.cs",
8506            |metric| {
8507                insta::assert_json_snapshot!(
8508                    metric.cognitive,
8509                    @r#"
8510                {
8511                  "sum": 5,
8512                  "value": 0,
8513                  "average": 5.0,
8514                  "min": 0,
8515                  "max": 5
8516                }
8517                "#
8518                );
8519            },
8520        );
8521    }
8522
8523    #[test]
8524    fn csharp_cognitive_if_inside_else_block_is_not_else_if() {
8525        // Regression for #115: an `if` whose previous sibling is the block's
8526        // opening brace (not the `else` keyword) is a nested independent
8527        // statement, NOT an else-if continuation. It must pay the full
8528        // nesting penalty.
8529        // if(+1, nesting=0) + else(+1) + inner if(+2, nesting=1) = 4.
8530        check_metrics::<CsharpParser>(
8531            "class X {
8532                public static void F(int a, int c) {
8533                    if (a > 0) {
8534                    } else {
8535                        if (c > 0) {
8536                        }
8537                    }
8538                }
8539            }",
8540            "foo.cs",
8541            |metric| {
8542                insta::assert_json_snapshot!(
8543                    metric.cognitive,
8544                    @r#"
8545                {
8546                  "sum": 4,
8547                  "value": 0,
8548                  "average": 4.0,
8549                  "min": 0,
8550                  "max": 4
8551                }
8552                "#
8553                );
8554            },
8555        );
8556    }
8557
8558    #[test]
8559    fn csharp_sibling_bool_sequences() {
8560        // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested.
8561        // Expected: &&(+1) + ||(+1) + &&(+1) = 3.
8562        check_metrics::<CsharpParser>(
8563            "class X {
8564                bool F(bool a, bool b, bool c, bool d) {
8565                    return (a && b) || (c && d);
8566                }
8567            }",
8568            "foo.cs",
8569            |metric| {
8570                assert_eq!(metric.cognitive.cognitive_sum(), 3);
8571                assert_eq!(metric.cognitive.cognitive_max(), 3);
8572                insta::assert_json_snapshot!(metric.cognitive);
8573            },
8574        );
8575    }
8576
8577    #[test]
8578    fn csharp_nested_bool_same_op() {
8579        // a||(b&&c&&d) — the inner && operators are nested, forming one sequence.
8580        // Expected: ||(+1) + &&(+1) = 2.
8581        check_metrics::<CsharpParser>(
8582            "class X {
8583                bool F(bool a, bool b, bool c, bool d) {
8584                    return a || (b && c && d);
8585                }
8586            }",
8587            "foo.cs",
8588            |metric| {
8589                assert_eq!(metric.cognitive.cognitive_sum(), 2);
8590                assert_eq!(metric.cognitive.cognitive_max(), 2);
8591                insta::assert_json_snapshot!(metric.cognitive);
8592            },
8593        );
8594    }
8595
8596    #[test]
8597    fn kotlin_sibling_bool_sequences() {
8598        // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested.
8599        // Expected: &&(+1) + ||(+1) + &&(+1) = 3.
8600        check_metrics::<KotlinParser>(
8601            "fun f(a: Boolean, b: Boolean, c: Boolean, d: Boolean) =
8602                 (a && b) || (c && d)  // +1(&&) +1(||) +1(&&) = 3",
8603            "foo.kt",
8604            |metric| {
8605                assert_eq!(metric.cognitive.cognitive_sum(), 3);
8606                assert_eq!(metric.cognitive.cognitive_max(), 3);
8607                insta::assert_json_snapshot!(metric.cognitive);
8608            },
8609        );
8610    }
8611
8612    #[test]
8613    fn kotlin_nested_bool_same_op() {
8614        // a||(b&&c&&d) — the inner && operators are nested, forming one sequence.
8615        // Expected: ||(+1) + &&(+1) = 2.
8616        check_metrics::<KotlinParser>(
8617            "fun f(a: Boolean, b: Boolean, c: Boolean, d: Boolean) =
8618                 a || (b && c && d)  // +1(||) +1(&&) = 2",
8619            "foo.kt",
8620            |metric| {
8621                assert_eq!(metric.cognitive.cognitive_sum(), 2);
8622                assert_eq!(metric.cognitive.cognitive_max(), 2);
8623                insta::assert_json_snapshot!(metric.cognitive);
8624            },
8625        );
8626    }
8627
8628    #[test]
8629    fn kotlin_elvis_chain_239() {
8630        // Regression for issue #239: Kotlin's Elvis operator `?:` is a
8631        // short-circuit nullish operator analogous to JS `??` and must
8632        // form a boolean sequence. `a ?: b ?: c` is a single chain of
8633        // identical operators and collapses to a single +1 under Sonar
8634        // B1 (same rule as `&&` / `||`). Previously the Elvis chain was
8635        // not counted at all (= 0).
8636        check_metrics::<KotlinParser>(
8637            "fun pick(a: String?, b: String?, c: String): String = a ?: b ?: c // +1 (Elvis chain)",
8638            "foo.kt",
8639            |metric| {
8640                assert_eq!(metric.cognitive.cognitive_sum(), 1);
8641                assert_eq!(metric.cognitive.cognitive_max(), 1);
8642                insta::assert_json_snapshot!(
8643                    metric.cognitive,
8644                    @r#"
8645                {
8646                  "sum": 1,
8647                  "value": 0,
8648                  "average": 1.0,
8649                  "min": 0,
8650                  "max": 1
8651                }
8652                "#
8653                );
8654            },
8655        );
8656    }
8657
8658    #[test]
8659    fn kotlin_elvis_inside_if_239() {
8660        // Regression for issue #239: Elvis chain inside an `if` body.
8661        // Boolean sequences pay a flat +1 (no nesting penalty) per
8662        // Sonar B1: if(+1) + ?: chain(+1) = 2. Previously the Elvis
8663        // chain was not counted at all and the function scored 1.
8664        check_metrics::<KotlinParser>(
8665            "fun f(a: String?, b: String?): String {
8666                 if (a != null) { // +1
8667                     return a ?: b ?: \"x\" // +1 (Elvis chain)
8668                 }
8669                 return \"no\"
8670             }",
8671            "foo.kt",
8672            |metric| {
8673                assert_eq!(metric.cognitive.cognitive_sum(), 2);
8674                assert_eq!(metric.cognitive.cognitive_max(), 2);
8675                insta::assert_json_snapshot!(
8676                    metric.cognitive,
8677                    @r#"
8678                {
8679                  "sum": 2,
8680                  "value": 0,
8681                  "average": 2.0,
8682                  "min": 0,
8683                  "max": 2
8684                }
8685                "#
8686                );
8687            },
8688        );
8689    }
8690
8691    #[test]
8692    fn go_sibling_bool_sequences() {
8693        // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested.
8694        // Expected: &&(+1) + ||(+1) + &&(+1) = 3.
8695        check_metrics::<GoParser>(
8696            "package main
8697            func f(a, b, c, d bool) bool {
8698                return (a && b) || (c && d)  // +1(&&) +1(||) +1(&&) = 3
8699            }",
8700            "foo.go",
8701            |metric| {
8702                assert_eq!(metric.cognitive.cognitive_sum(), 3);
8703                assert_eq!(metric.cognitive.cognitive_max(), 3);
8704                insta::assert_json_snapshot!(metric.cognitive);
8705            },
8706        );
8707    }
8708
8709    #[test]
8710    fn go_nested_bool_same_op() {
8711        // a||(b&&c&&d) — the inner && operators are nested, forming one sequence.
8712        // Expected: ||(+1) + &&(+1) = 2.
8713        check_metrics::<GoParser>(
8714            "package main
8715            func f(a, b, c, d bool) bool {
8716                return a || (b && c && d)  // +1(||) +1(&&) = 2
8717            }",
8718            "foo.go",
8719            |metric| {
8720                assert_eq!(metric.cognitive.cognitive_sum(), 2);
8721                assert_eq!(metric.cognitive.cognitive_max(), 2);
8722                insta::assert_json_snapshot!(metric.cognitive);
8723            },
8724        );
8725    }
8726
8727    #[test]
8728    fn tcl_sibling_bool_sequences() {
8729        // ($a && $b) || ($c && $d) — the right-hand && is a sibling, not nested.
8730        // Expected: if(+1) + ||(+1) + &&(+1) + &&(+1) = 4.
8731        check_metrics::<TclParser>(
8732            "proc f {a b c d} {
8733    if {($a && $b) || ($c && $d)} {
8734        puts yes
8735    }
8736}",
8737            "foo.tcl",
8738            |metric| {
8739                assert_eq!(metric.cognitive.cognitive_sum(), 4);
8740                assert_eq!(metric.cognitive.cognitive_max(), 4);
8741                insta::assert_json_snapshot!(metric.cognitive);
8742            },
8743        );
8744    }
8745
8746    #[test]
8747    fn tcl_nested_bool_same_op() {
8748        // $a || ($b && $c && $d) — the inner && operators are nested, one sequence.
8749        // Expected: if(+1) + ||(+1) + &&(+1) = 3.
8750        check_metrics::<TclParser>(
8751            "proc f {a b c d} {
8752    if {$a || ($b && $c && $d)} {
8753        puts yes
8754    }
8755}",
8756            "foo.tcl",
8757            |metric| {
8758                assert_eq!(metric.cognitive.cognitive_sum(), 3);
8759                assert_eq!(metric.cognitive.cognitive_max(), 3);
8760                insta::assert_json_snapshot!(metric.cognitive);
8761            },
8762        );
8763    }
8764
8765    #[test]
8766    fn lua_sibling_bool_sequences() {
8767        // (a and b) or (c and d) — the right-hand `and` is a sibling, not nested.
8768        // Expected: if(+1) + or(+1) + and(+1) + and(+1) = 4.
8769        check_metrics::<LuaParser>(
8770            "local function f(a, b, c, d)
8771    if (a and b) or (c and d) then
8772        return 1
8773    end
8774end",
8775            "foo.lua",
8776            |metric| {
8777                assert_eq!(metric.cognitive.cognitive_sum(), 4);
8778                assert_eq!(metric.cognitive.cognitive_max(), 4);
8779                insta::assert_json_snapshot!(metric.cognitive);
8780            },
8781        );
8782    }
8783
8784    #[test]
8785    fn lua_nested_bool_same_op() {
8786        // a or (b and c and d) — the inner `and` operators are nested, one sequence.
8787        // Expected: if(+1) + or(+1) + and(+1) = 3.
8788        check_metrics::<LuaParser>(
8789            "local function f(a, b, c, d)
8790    if a or (b and c and d) then
8791        return 1
8792    end
8793end",
8794            "foo.lua",
8795            |metric| {
8796                assert_eq!(metric.cognitive.cognitive_sum(), 3);
8797                assert_eq!(metric.cognitive.cognitive_max(), 3);
8798                insta::assert_json_snapshot!(metric.cognitive);
8799            },
8800        );
8801    }
8802
8803    #[test]
8804    fn bash_sibling_bool_sequences() {
8805        // [[ a ]] && [[ b ]] || [[ c ]] && [[ d ]] — bash is left-associative so this
8806        // parses as ((a&&b)||c)&&d with three distinct operator-type transitions.
8807        // Expected: if(+1) + &&(+1) + ||(+1) + &&(+1) = 4.
8808        check_metrics::<BashParser>(
8809            "f() {
8810                 if [[ -n \"$a\" ]] && [[ -n \"$b\" ]] || [[ -n \"$c\" ]] && [[ -n \"$d\" ]]; then
8811                     echo test
8812                 fi
8813             }",
8814            "foo.sh",
8815            |metric| {
8816                assert_eq!(metric.cognitive.cognitive_sum(), 4);
8817                assert_eq!(metric.cognitive.cognitive_max(), 4);
8818                insta::assert_json_snapshot!(metric.cognitive);
8819            },
8820        );
8821    }
8822
8823    #[test]
8824    fn bash_nested_bool_same_op() {
8825        // [[ a ]] || [[ b ]] && [[ c ]] && [[ d ]] — bash left-associativity gives
8826        // ((a||b)&&c)&&d: the two && operators are parent/child so the second is
8827        // a continuation (no extra increment).
8828        // Expected: if(+1) + &&(+1, outer chain) + ||(+1) = 3.
8829        check_metrics::<BashParser>(
8830            "f() {
8831                 if [[ -n \"$a\" ]] || [[ -n \"$b\" ]] && [[ -n \"$c\" ]] && [[ -n \"$d\" ]]; then
8832                     echo test
8833                 fi
8834             }",
8835            "foo.sh",
8836            |metric| {
8837                assert_eq!(metric.cognitive.cognitive_sum(), 3);
8838                assert_eq!(metric.cognitive.cognitive_max(), 3);
8839                insta::assert_json_snapshot!(metric.cognitive);
8840            },
8841        );
8842    }
8843
8844    #[test]
8845    fn php_no_cognitive() {
8846        check_metrics::<PhpParser>("<?php $a = 42;", "foo.php", |metric| {
8847            assert_eq!(metric.cognitive.cognitive_sum(), 0);
8848            assert_eq!(metric.cognitive.cognitive_max(), 0);
8849            insta::assert_json_snapshot!(metric.cognitive);
8850        });
8851    }
8852
8853    #[test]
8854    fn php_simple_function() {
8855        // Single `if` inside a function: +1.
8856        check_metrics::<PhpParser>(
8857            "<?php
8858            function f(bool $a): void {
8859                if ($a) {
8860                    echo 'hi';
8861                }
8862            }",
8863            "foo.php",
8864            |metric| {
8865                assert_eq!(metric.cognitive.cognitive_sum(), 1);
8866                assert_eq!(metric.cognitive.cognitive_max(), 1);
8867                insta::assert_json_snapshot!(metric.cognitive);
8868            },
8869        );
8870    }
8871
8872    #[test]
8873    fn php_nested_function_resets_nesting_775() {
8874        // Regression for #775 (the #696 gap): a PHP named function defined
8875        // inside control flow must reset structural nesting to 0 at the
8876        // definition boundary and pick up the +1 function-depth surcharge,
8877        // exactly like Java/C/Rust/etc. Before the fix, `inner` inherited
8878        // `outer`'s leaked nesting (2 by the time the definition is reached)
8879        // and scored its body against it: `inner` was 7 (and the file 10).
8880        //
8881        // After the fix, inside `inner` nesting resets to 0 and depth = 1
8882        // (it is nested in `outer`), so:
8883        //   inner `if ($b)`: structural += (nesting 0 + depth 1) + 1 = 2
8884        //   inner `if ($d)`: structural += (nesting 1 + depth 1) + 1 = 3
8885        //   => inner = 5
8886        // `outer` itself (excluding the nested space) is:
8887        //   `if ($a)`: +1 (nesting 0→1); `if ($c)`: +2 (nesting 1→2) => 3
8888        // so the file sum is 3 + 5 = 8, max = 5 (the `inner` space).
8889        check_metrics::<PhpParser>(
8890            "<?php
8891            function outer() {
8892                if ($a) {
8893                    if ($c) {
8894                        function inner() {
8895                            if ($b) {
8896                                if ($d) {
8897                                    echo 'x';
8898                                }
8899                            }
8900                        }
8901                    }
8902                }
8903            }",
8904            "foo.php",
8905            |metric| {
8906                assert_eq!(metric.cognitive.cognitive_sum(), 8);
8907                assert_eq!(metric.cognitive.cognitive_max(), 5);
8908            },
8909        );
8910    }
8911
8912    #[test]
8913    fn php_top_level_function_unchanged_775() {
8914        // Regression guard paired with `php_nested_function_resets_nesting_775`:
8915        // a *top-level* PHP function with the same body is unaffected by the
8916        // #775 fix — nesting is already 0 and depth is 0 there. The two
8917        // `if` statements score +1 and +2 respectively, so cognitive = 3.
8918        // If the #775 boundary arm ever over-fires on top-level functions,
8919        // this value moves and the test fails.
8920        check_metrics::<PhpParser>(
8921            "<?php
8922            function inner() {
8923                if ($b) {
8924                    if ($d) {
8925                        echo 'x';
8926                    }
8927                }
8928            }",
8929            "foo.php",
8930            |metric| {
8931                assert_eq!(metric.cognitive.cognitive_sum(), 3);
8932                assert_eq!(metric.cognitive.cognitive_max(), 3);
8933            },
8934        );
8935    }
8936
8937    #[test]
8938    fn php_if_elseif_else() {
8939        // PHP exposes `elseif` as a dedicated `else_if_clause` node, scored
8940        // as a branch extension (+1, no nesting) via the `ElseIfClause` arm
8941        // — parallel to bash/perl/ruby. An `if … elseif … else` chain is
8942        // therefore +1 each = 3. PHP previously had no cognitive test for
8943        // the `elseif` dispatch; this pins it.
8944        //
8945        // Note: the one-word `elseif` parses as its own `else_if_clause`
8946        // node, dispatched directly to the branch-extension arm, so
8947        // `is_else_if` is never consulted on this path. PHP's *two-word*
8948        // `else if` is the nested-`if` shape that C++/JS/Java have, and it
8949        // does go through the `IfStatement if !Self::is_else_if` guard —
8950        // see `php_two_word_else_if_529` (#529).
8951        check_metrics::<PhpParser>(
8952            "<?php
8953            function f(int $a): void {
8954                if ($a > 0) {        // +1
8955                    echo 'pos';
8956                } elseif ($a < 0) {  // +1
8957                    echo 'neg';
8958                } else {             // +1
8959                    echo 'zero';
8960                }
8961            }",
8962            "foo.php",
8963            |metric| {
8964                assert_eq!(metric.cognitive.cognitive_sum(), 3);
8965                assert_eq!(metric.cognitive.cognitive_max(), 3);
8966                insta::assert_json_snapshot!(
8967                    metric.cognitive,
8968                    @r#"
8969                {
8970                  "sum": 3,
8971                  "value": 0,
8972                  "average": 3.0,
8973                  "min": 0,
8974                  "max": 3
8975                }
8976                "#
8977                );
8978            },
8979        );
8980    }
8981
8982    #[test]
8983    fn php_two_word_else_if_529() {
8984        // PHP's two-word `else if` parses as an `else_clause` wrapping a
8985        // nested `if_statement` (`else_clause → if_statement`), unlike the
8986        // one-word `elseif` keyword which is a dedicated `else_if_clause`
8987        // node. Before #529 the nested `IfStatement` fell through PHP's
8988        // unguarded cognitive `IfStatement` arm: it fired `increase_nesting`
8989        // (+1, plus an inflated nesting level for later arms) on top of the
8990        // wrapping `else_clause`'s branch extension (+1), so the chain below
8991        // scored 5 instead of the correct 3 — and worse for deeper chains.
8992        //
8993        // Correct SonarSource value: `if` +1, `else if` +1 branch
8994        // extension, `else` +1 = 3. The fix adds the
8995        // `IfStatement if !Self::is_else_if(node)` guard and teaches PHP's
8996        // `is_else_if` to recognize the `else_clause → if_statement` shape.
8997        // This test guards both halves: it scores identically to the
8998        // one-word `php_if_elseif_else` form. Verified by revert — against
8999        // pre-#529 code it asserts 5 and fails.
9000        check_metrics::<PhpParser>(
9001            "<?php
9002            function f(int $a): void {
9003                if ($a == 1) {        // +1
9004                    echo 'one';
9005                } else if ($a == 2) { // +1 branch extension, no nesting
9006                    echo 'two';
9007                } else {              // +1 branch extension
9008                    echo 'zero';
9009                }
9010            }",
9011            "foo.php",
9012            |metric| {
9013                assert_eq!(metric.cognitive.cognitive_sum(), 3);
9014                assert_eq!(metric.cognitive.cognitive_max(), 3);
9015                insta::assert_json_snapshot!(
9016                    metric.cognitive,
9017                    @r#"
9018                {
9019                  "sum": 3,
9020                  "value": 0,
9021                  "average": 3.0,
9022                  "min": 0,
9023                  "max": 3
9024                }
9025                "#
9026                );
9027            },
9028        );
9029    }
9030
9031    #[test]
9032    fn php_two_word_else_if_chain_nesting_529() {
9033        // A genuinely nested `if` inside a two-word `else if` arm must still
9034        // pay its nesting penalty — the #529 guard suppresses only the
9035        // else-if-continuation `IfStatement`, not real nesting. Here the
9036        // inner `if ($a > 0)` sits one level deep inside the `else if` arm:
9037        // `if` +1, `else if` +1, inner `if` +2 (base + nesting), final
9038        // `else` +1 = 5. Pre-#529 the misattributed nesting inflated this
9039        // super-linearly; this pins the corrected total and confirms the
9040        // guard does not over-suppress real nesting.
9041        check_metrics::<PhpParser>(
9042            "<?php
9043            function f(int $a): void {
9044                if ($a == 1) {         // +1
9045                    echo 'one';
9046                } else if ($a == 2) {  // +1
9047                    if ($a > 0) {      // +2 (base + nesting)
9048                        echo 'pos';
9049                    }
9050                } else {               // +1
9051                    echo 'zero';
9052                }
9053            }",
9054            "foo.php",
9055            |metric| {
9056                assert_eq!(metric.cognitive.cognitive_sum(), 5);
9057                assert_eq!(metric.cognitive.cognitive_max(), 5);
9058                insta::assert_json_snapshot!(
9059                    metric.cognitive,
9060                    @r#"
9061                {
9062                  "sum": 5,
9063                  "value": 0,
9064                  "average": 5.0,
9065                  "min": 0,
9066                  "max": 5
9067                }
9068                "#
9069                );
9070            },
9071        );
9072    }
9073
9074    #[test]
9075    fn php_alternative_syntax_elseif_529() {
9076        // PHP's alternative (colon) syntax `if …: … elseif …: … else: …
9077        // endif;` requires the one-word `elseif` keyword — two-word
9078        // `else if` is a PHP fatal parse error there (the grammar emits an
9079        // `ERROR` node). The valid one-word form parses as the dedicated
9080        // `else_if_clause` node, scored as a branch extension (+1, no
9081        // nesting) just like the brace form. Discovered while fixing #529:
9082        // pins that the colon-syntax `elseif` chain scores 3, the same as
9083        // the brace `php_if_elseif_else` form, and guards against a future
9084        // change that mishandles the alternative-syntax dispatch.
9085        check_metrics::<PhpParser>(
9086            "<?php
9087            function f(int $a): void {
9088                if ($a > 0):        // +1
9089                    echo 'pos';
9090                elseif ($a < 0):    // +1 branch extension, no nesting
9091                    echo 'neg';
9092                else:               // +1 branch extension
9093                    echo 'zero';
9094                endif;
9095            }",
9096            "foo.php",
9097            |metric| {
9098                assert_eq!(metric.cognitive.cognitive_sum(), 3);
9099                assert_eq!(metric.cognitive.cognitive_max(), 3);
9100                insta::assert_json_snapshot!(
9101                    metric.cognitive,
9102                    @r#"
9103                {
9104                  "sum": 3,
9105                  "value": 0,
9106                  "average": 3.0,
9107                  "min": 0,
9108                  "max": 3
9109                }
9110                "#
9111                );
9112            },
9113        );
9114    }
9115
9116    #[test]
9117    fn php_ternary() {
9118        // PHP's ternary `?:` (grammar `conditional_expression`) is a
9119        // conditional construct: +1 base + nesting. Regression test for
9120        // issue #224. Note: this differs from PHP's
9121        // `match_conditional_expression` (the `match` expression),
9122        // which is handled separately by `MatchExpression`.
9123        check_metrics::<PhpParser>(
9124            "<?php
9125            function check(int $a): bool {
9126                return $a > 0 ? true : false; // +1
9127            }",
9128            "foo.php",
9129            |metric| {
9130                assert_eq!(metric.cognitive.cognitive_sum(), 1);
9131                assert_eq!(metric.cognitive.cognitive_max(), 1);
9132                insta::assert_json_snapshot!(
9133                    metric.cognitive,
9134                    @r#"
9135                {
9136                  "sum": 1,
9137                  "value": 0,
9138                  "average": 1.0,
9139                  "min": 0,
9140                  "max": 1
9141                }
9142                "#
9143                );
9144            },
9145        );
9146    }
9147
9148    #[test]
9149    fn php_nested_ternary() {
9150        // Nested ternaries inside an `if` compound by nesting (mirrors
9151        // the C++ regression test for #172).
9152        // expected: if (+1) + outer ternary (+2, nesting=1) + inner
9153        // ternary (+3, nesting=2) = 6.
9154        check_metrics::<PhpParser>(
9155            "<?php
9156            function classify(int $a, int $b): string {
9157                if ($a > 0) { // +1
9158                    return $b > 0 ? ($b > 10 ? 'big' : 'small') : 'neg'; // +2, +3
9159                }
9160                return 'zero';
9161            }",
9162            "foo.php",
9163            |metric| {
9164                assert_eq!(metric.cognitive.cognitive_sum(), 6);
9165                assert_eq!(metric.cognitive.cognitive_max(), 6);
9166                insta::assert_json_snapshot!(
9167                    metric.cognitive,
9168                    @r#"
9169                {
9170                  "sum": 6,
9171                  "value": 0,
9172                  "average": 6.0,
9173                  "min": 0,
9174                  "max": 6
9175                }
9176                "#
9177                );
9178            },
9179        );
9180    }
9181
9182    #[test]
9183    fn php_sequence_same_booleans() {
9184        // Sequence of same-operator booleans collapses: a chain of `&&`
9185        // counts as +1 total, not per-operand.
9186        check_metrics::<PhpParser>(
9187            "<?php
9188            function f(bool $a, bool $b, bool $c): bool {
9189                return $a && $b && $c;
9190            }",
9191            "foo.php",
9192            |metric| {
9193                // Chain of identical && collapses to a single +1.
9194                assert_eq!(metric.cognitive.cognitive_sum(), 1);
9195                assert_eq!(metric.cognitive.cognitive_max(), 1);
9196                insta::assert_json_snapshot!(metric.cognitive);
9197            },
9198        );
9199    }
9200
9201    #[test]
9202    fn php_sequence_different_booleans() {
9203        // Mix of `&&` and `||` — each operator switch costs +1.
9204        check_metrics::<PhpParser>(
9205            "<?php
9206            function f(bool $a, bool $b, bool $c): bool {
9207                return $a && $b || $c;
9208            }",
9209            "foo.php",
9210            |metric| {
9211                // && chain (+1) + switch to || (+1) = 2.
9212                assert_eq!(metric.cognitive.cognitive_sum(), 2);
9213                assert_eq!(metric.cognitive.cognitive_max(), 2);
9214                insta::assert_json_snapshot!(metric.cognitive);
9215            },
9216        );
9217    }
9218
9219    #[test]
9220    fn php_not_booleans() {
9221        // `!` does not break boolean sequences (issue #392): pre-order
9222        // visits the outer `&&` BinaryExpression first, so the inner
9223        // `&&` lies within its span and is a continuation.
9224        check_metrics::<PhpParser>(
9225            "<?php
9226            function f(bool $a, bool $b, bool $c): bool {
9227                return $a && !($b && $c);
9228            }",
9229            "foo.php",
9230            |metric| {
9231                // Outer && (+1); inner && continues outer's span → 1.
9232                assert_eq!(metric.cognitive.cognitive_sum(), 1);
9233                assert_eq!(metric.cognitive.cognitive_max(), 1);
9234                insta::assert_json_snapshot!(metric.cognitive);
9235            },
9236        );
9237    }
9238
9239    #[test]
9240    fn php_1_level_nesting() {
9241        // if-inside-loop: outer for (+1) + inner if at depth 1 (+2) = +3.
9242        check_metrics::<PhpParser>(
9243            "<?php
9244            function f(int $n): int {
9245                for ($i = 0; $i < $n; $i++) {
9246                    if ($i % 2 === 0) {
9247                        return $i;
9248                    }
9249                }
9250                return -1;
9251            }",
9252            "foo.php",
9253            |metric| {
9254                // for(+1) + if at depth 1 (+2) = 3.
9255                assert_eq!(metric.cognitive.cognitive_sum(), 3);
9256                assert_eq!(metric.cognitive.cognitive_max(), 3);
9257                insta::assert_json_snapshot!(metric.cognitive);
9258            },
9259        );
9260    }
9261
9262    #[test]
9263    fn php_2_level_nesting() {
9264        // for + while + if = +1 +2 +3 = +6.
9265        check_metrics::<PhpParser>(
9266            "<?php
9267            function f(int $n): int {
9268                for ($i = 0; $i < $n; $i++) {
9269                    while ($i > 0) {
9270                        if ($i % 2 === 0) {
9271                            return $i;
9272                        }
9273                    }
9274                }
9275                return -1;
9276            }",
9277            "foo.php",
9278            |metric| {
9279                // for(+1) + while at depth 1 (+2) + if at depth 2 (+3) = 6.
9280                assert_eq!(metric.cognitive.cognitive_sum(), 6);
9281                assert_eq!(metric.cognitive.cognitive_max(), 6);
9282                insta::assert_json_snapshot!(metric.cognitive);
9283            },
9284        );
9285    }
9286
9287    #[test]
9288    fn php_break_continue() {
9289        // PHP `break` and `continue` are not cognitive drivers in this
9290        // impl; only the surrounding loops count.
9291        check_metrics::<PhpParser>(
9292            "<?php
9293            function f(int $n): int {
9294                for ($i = 0; $i < $n; $i++) {
9295                    if ($i % 2 === 0) {
9296                        continue;
9297                    }
9298                    if ($i > 100) {
9299                        break;
9300                    }
9301                }
9302                return 0;
9303            }",
9304            "foo.php",
9305            |metric| {
9306                // for(+1) + first if at depth 1 (+2) + second if at depth 1 (+2) = 5.
9307                assert_eq!(metric.cognitive.cognitive_sum(), 5);
9308                assert_eq!(metric.cognitive.cognitive_max(), 5);
9309                insta::assert_json_snapshot!(metric.cognitive);
9310            },
9311        );
9312    }
9313
9314    #[test]
9315    fn php_goto_counted() {
9316        // `goto label;` is a genuinely unstructured jump and adds +1 per
9317        // SonarSource Cognitive Complexity §B2 (issue #435), matching
9318        // C++/C#/Go/Perl/Lua goto handling.
9319        check_metrics::<PhpParser>(
9320            "<?php
9321            function f(int $n): int {
9322                if ($n < 0) {
9323                    goto done;
9324                }
9325                done:
9326                return 0;
9327            }",
9328            "foo.php",
9329            |metric| {
9330                // if(+1) + goto(+1) = 2.
9331                assert_eq!(metric.cognitive.cognitive_sum(), 2);
9332                assert_eq!(metric.cognitive.cognitive_max(), 2);
9333                insta::assert_json_snapshot!(metric.cognitive);
9334            },
9335        );
9336    }
9337
9338    #[test]
9339    fn php_numeric_break_not_counted() {
9340        // PHP has no labeled break/continue; only the numeric level form
9341        // `break N;` / `continue N;`, which exits N enclosing loops already
9342        // accounted for by nesting. Per issue #435 the numeric form is a
9343        // structured loop-level exit and adds +0.
9344        check_metrics::<PhpParser>(
9345            "<?php
9346            function f(int $n): int {
9347                for ($i = 0; $i < $n; $i++) {
9348                    while (true) {
9349                        if ($i > 100) {
9350                            break 2;
9351                        }
9352                    }
9353                }
9354                return 0;
9355            }",
9356            "foo.php",
9357            |metric| {
9358                // for(+1) + while at depth 1 (+2) + if at depth 2 (+3) = 6;
9359                // `break 2` adds +0.
9360                assert_eq!(metric.cognitive.cognitive_sum(), 6);
9361                assert_eq!(metric.cognitive.cognitive_max(), 6);
9362                insta::assert_json_snapshot!(metric.cognitive);
9363            },
9364        );
9365    }
9366
9367    // ----- Elixir -----
9368
9369    // No control flow → cognitive complexity is 0.
9370    #[test]
9371    fn elixir_empty_function() {
9372        check_metrics::<ElixirParser>(
9373            "defmodule Foo do\n  def f(x) do\n    x\n  end\nend\n",
9374            "foo.ex",
9375            |metric| {
9376                assert_eq!(metric.cognitive.cognitive_sum(), 0);
9377                insta::assert_json_snapshot!(
9378                    metric.cognitive,
9379                    @r#"
9380                {
9381                  "sum": 0,
9382                  "value": 0,
9383                  "average": 0.0,
9384                  "min": 0,
9385                  "max": 0
9386                }
9387                "#
9388                );
9389            },
9390        );
9391    }
9392
9393    // `if cond do … end`: single-branch construct → +1 nesting at depth
9394    // 0 inside `def` body → cognitive 1.
9395    #[test]
9396    fn elixir_simple_if() {
9397        check_metrics::<ElixirParser>(
9398            "defmodule Foo do\n  def f(x) do\n    if x > 0 do\n      :pos\n    end\n  end\nend\n",
9399            "foo.ex",
9400            |metric| {
9401                assert_eq!(metric.cognitive.cognitive_sum(), 1);
9402                insta::assert_json_snapshot!(metric.cognitive);
9403            },
9404        );
9405    }
9406
9407    // `if cond do … else … end`: +1 nesting for `if`, +1 for `else` token
9408    // (matches Java/Kotlin) → cognitive 2.
9409    #[test]
9410    fn elixir_if_else() {
9411        check_metrics::<ElixirParser>(
9412            "defmodule Foo do\n  def f(x) do\n    if x > 0 do\n      :pos\n    else\n      :neg\n    end\n  end\nend\n",
9413            "foo.ex",
9414            |metric| {
9415                // expected: if (+1) + else (+1) = 2
9416                assert_eq!(metric.cognitive.cognitive_sum(), 2);
9417                insta::assert_json_snapshot!(metric.cognitive);
9418            },
9419        );
9420    }
9421
9422    // `case x do … end` with three arms: only the container Call earns
9423    // a nesting bump (matches Java's `SwitchBlock` rule). Individual
9424    // `stab_clause` arms add no extra cost. Expected cognitive 1.
9425    #[test]
9426    fn elixir_case_arms_count_once() {
9427        check_metrics::<ElixirParser>(
9428            "defmodule Foo do\n  def f(x) do\n    case x do\n      1 -> :one\n      2 -> :two\n      _ -> :other\n    end\n  end\nend\n",
9429            "foo.ex",
9430            |metric| {
9431                // expected: case +1 (one nesting bump on the container)
9432                assert_eq!(metric.cognitive.cognitive_sum(), 1);
9433                insta::assert_json_snapshot!(metric.cognitive);
9434            },
9435        );
9436    }
9437
9438    // `cond do … end` is structurally identical to `case` for our
9439    // purposes: container Call earns +1 nesting; arms add nothing.
9440    #[test]
9441    fn elixir_cond_counts_once() {
9442        check_metrics::<ElixirParser>(
9443            "defmodule Foo do\n  def f(x) do\n    cond do\n      x > 0 -> :pos\n      x < 0 -> :neg\n      true -> :zero\n    end\n  end\nend\n",
9444            "foo.ex",
9445            |metric| {
9446                // expected: cond +1
9447                assert_eq!(metric.cognitive.cognitive_sum(), 1);
9448                insta::assert_json_snapshot!(metric.cognitive);
9449            },
9450        );
9451    }
9452
9453    // Nested `if` inside another `if`: outer +1, inner +2 (nested
9454    // depth 1) → cognitive 3.
9455    #[test]
9456    fn elixir_nested_if_amplifies() {
9457        check_metrics::<ElixirParser>(
9458            "defmodule Foo do\n  def f(x, y) do\n    if x > 0 do\n      if y > 0 do\n        :both\n      end\n    end\n  end\nend\n",
9459            "foo.ex",
9460            |metric| {
9461                // expected: outer if (+1) + nested if (+2 because nesting=1)
9462                assert_eq!(metric.cognitive.cognitive_sum(), 3);
9463                insta::assert_json_snapshot!(metric.cognitive);
9464            },
9465        );
9466    }
9467
9468    // `try` with `rescue` and `catch`: the `try` wrapper itself does
9469    // NOT bump nesting (matches Java / C#'s "try is a wrapper" rule);
9470    // each `rescue` / `catch` block bumps +1 nesting at depth 0. The
9471    // single `stab_clause` inside each block adds no extra cost.
9472    #[test]
9473    fn elixir_try_rescue_catch() {
9474        check_metrics::<ElixirParser>(
9475            "defmodule Foo do\n  def f do\n    try do\n      :ok\n    rescue\n      _ -> :err\n    catch\n      _ -> :thrown\n    end\n  end\nend\n",
9476            "foo.ex",
9477            |metric| {
9478                // expected: rescue (+1) + catch (+1) = 2
9479                assert_eq!(metric.cognitive.cognitive_sum(), 2);
9480                insta::assert_json_snapshot!(metric.cognitive);
9481            },
9482        );
9483    }
9484
9485    // Short-circuit booleans: `x && y || z` is two operator types in
9486    // sequence — `&&` once, `||` once → +2. The `if` container that
9487    // surrounds them adds +1 → total cognitive 3.
9488    #[test]
9489    fn elixir_boolean_sequence() {
9490        check_metrics::<ElixirParser>(
9491            "defmodule Foo do\n  def f(x, y, z) do\n    if x && y || z do\n      :hit\n    end\n  end\nend\n",
9492            "foo.ex",
9493            |metric| {
9494                // expected: if (+1) + && (+1) + || (+1) = 3
9495                assert_eq!(metric.cognitive.cognitive_sum(), 3);
9496                insta::assert_json_snapshot!(metric.cognitive);
9497            },
9498        );
9499    }
9500
9501    // `Enum.reduce` (and friends) are higher-order calls, NOT control
9502    // flow per the SonarSource spec. They contribute nothing to
9503    // cognitive complexity. The anonymous function body inside
9504    // contributes +1 lambda nesting, but its only operation is a
9505    // function call (no control flow) → cognitive 0.
9506    #[test]
9507    fn elixir_enum_reduce_is_zero() {
9508        check_metrics::<ElixirParser>(
9509            "defmodule Foo do\n  def sum(xs) do\n    Enum.reduce(xs, 0, fn x, acc -> acc + x end)\n  end\nend\n",
9510            "foo.ex",
9511            |metric| {
9512                // expected: 0 — Enum.reduce is a function call, not
9513                // syntactic control flow; the `fn` body has no decisions.
9514                assert_eq!(metric.cognitive.cognitive_sum(), 0);
9515                insta::assert_json_snapshot!(metric.cognitive);
9516            },
9517        );
9518    }
9519
9520    // Recursion: a `def` whose body calls itself by name. Per the
9521    // SonarSource spec recursion is +1, but our impl skips it for
9522    // scope reasons (documented). The body's lone Call earns nothing,
9523    // so cognitive stays at 0. This test pins the documented omission
9524    // so any future recursion work has to update it deliberately.
9525    #[test]
9526    fn elixir_recursion_is_zero_documented_limitation() {
9527        check_metrics::<ElixirParser>(
9528            "defmodule Foo do\n  def fact(0), do: 1\n  def fact(n), do: n * fact(n - 1)\nend\n",
9529            "foo.ex",
9530            |metric| {
9531                assert_eq!(metric.cognitive.cognitive_sum(), 0);
9532                insta::assert_json_snapshot!(metric.cognitive);
9533            },
9534        );
9535    }
9536
9537    #[test]
9538    fn php_match_cognitive() {
9539        // `match` is treated like `switch`: a single nesting bump for the
9540        // whole construct, not per arm.
9541        check_metrics::<PhpParser>(
9542            "<?php
9543            function color(string $c): int {
9544                return match ($c) {
9545                    'red' => 1,
9546                    'green' => 2,
9547                    default => 0,
9548                };
9549            }",
9550            "foo.php",
9551            |metric| {
9552                // `match` is treated like `switch`: a single +1 for the construct.
9553                assert_eq!(metric.cognitive.cognitive_sum(), 1);
9554                assert_eq!(metric.cognitive.cognitive_max(), 1);
9555                insta::assert_json_snapshot!(metric.cognitive);
9556            },
9557        );
9558    }
9559
9560    #[test]
9561    fn ruby_no_cognitive() {
9562        check_metrics::<RubyParser>("a = 42\n", "foo.rb", |metric| {
9563            assert_eq!(metric.cognitive.cognitive_sum(), 0);
9564            insta::assert_json_snapshot!(metric.cognitive);
9565        });
9566    }
9567
9568    #[test]
9569    fn ruby_simple_function() {
9570        // A function body with no branching scores zero cognitive.
9571        check_metrics::<RubyParser>("def foo\n  a = 1\nend\n", "foo.rb", |metric| {
9572            assert_eq!(metric.cognitive.cognitive_sum(), 0);
9573            insta::assert_json_snapshot!(metric.cognitive);
9574        });
9575    }
9576
9577    #[test]
9578    fn ruby_1_level_nesting() {
9579        // Single `if` inside a function: +1.
9580        check_metrics::<RubyParser>("def foo\n  if a\n    b\n  end\nend\n", "foo.rb", |metric| {
9581            assert_eq!(metric.cognitive.cognitive_sum(), 1);
9582            insta::assert_json_snapshot!(metric.cognitive);
9583        });
9584    }
9585
9586    #[test]
9587    fn ruby_2_level_nesting() {
9588        // expected: outer `if` (+1) + inner `if` (+2, nested) = 3.
9589        check_metrics::<RubyParser>(
9590            "def foo\n  if a\n    if b\n      c\n    end\n  end\nend\n",
9591            "foo.rb",
9592            |metric| {
9593                assert_eq!(metric.cognitive.cognitive_sum(), 3);
9594                insta::assert_json_snapshot!(metric.cognitive);
9595            },
9596        );
9597    }
9598
9599    #[test]
9600    fn ruby_sequence_same_booleans() {
9601        // `a && b && c`: same operator collapses to a single boolean
9602        // sequence (+1). Plus the enclosing `if` (+1) → 2.
9603        check_metrics::<RubyParser>(
9604            "def foo\n  if a && b && c\n    d\n  end\nend\n",
9605            "foo.rb",
9606            |metric| {
9607                assert_eq!(metric.cognitive.cognitive_sum(), 2);
9608                insta::assert_json_snapshot!(metric.cognitive);
9609            },
9610        );
9611    }
9612
9613    #[test]
9614    fn ruby_sequence_different_booleans() {
9615        // `a && b || c`: alternating operators add per change.
9616        check_metrics::<RubyParser>(
9617            "def foo\n  if a && b || c\n    d\n  end\nend\n",
9618            "foo.rb",
9619            |metric| {
9620                assert_eq!(metric.cognitive.cognitive_sum(), 3);
9621                insta::assert_json_snapshot!(metric.cognitive);
9622            },
9623        );
9624    }
9625
9626    #[test]
9627    fn ruby_not_booleans() {
9628        // `!a` (Unary) is the not-operator: it doesn't add cognitive
9629        // load by itself. Only the enclosing `if` counts.
9630        check_metrics::<RubyParser>(
9631            "def foo\n  if !a\n    b\n  end\nend\n",
9632            "foo.rb",
9633            |metric| {
9634                assert_eq!(metric.cognitive.cognitive_sum(), 1);
9635                insta::assert_json_snapshot!(metric.cognitive);
9636            },
9637        );
9638    }
9639
9640    #[test]
9641    fn ruby_break_next() {
9642        // Ruby has no labeled loops, so `break`/`next` are always
9643        // unlabeled. Per SonarSource Cognitive Complexity §B2 an unlabeled
9644        // break/continue adds +0 (issue #435) — only the enclosing `while`
9645        // (+1) counts → 1.
9646        check_metrics::<RubyParser>(
9647            "def foo\n  while a\n    break\n    next\n  end\nend\n",
9648            "foo.rb",
9649            |metric| {
9650                assert_eq!(metric.cognitive.cognitive_sum(), 1);
9651                insta::assert_json_snapshot!(metric.cognitive);
9652            },
9653        );
9654    }
9655
9656    #[test]
9657    fn ruby_redo_retry_counted() {
9658        // `redo` (restart the current loop iteration) and `retry` (re-run a
9659        // rescued `begin` block) are genuinely unstructured jumps with no
9660        // structured equivalent, so each adds +1 per SonarSource §B2
9661        // (issue #435) even though `break`/`next` do not.
9662        check_metrics::<RubyParser>(
9663            "def foo\n  while a\n    redo\n  end\n  begin\n    work\n  rescue\n    retry\n  end\nend\n",
9664            "foo.rb",
9665            |metric| {
9666                // while(+1) + redo(+1) + rescue(+1) + retry(+1) = 4.
9667                assert_eq!(metric.cognitive.cognitive_sum(), 4);
9668                insta::assert_json_snapshot!(metric.cognitive);
9669            },
9670        );
9671    }
9672
9673    #[test]
9674    fn ruby_else_if_chain() {
9675        // `elsif` extends the parent branch (no extra nesting). An
9676        // `if/elsif/elsif/else` chain scores strictly LESS than the
9677        // same number of nested `if` blocks. tree-sitter-ruby gives
9678        // `elsif` its own clause node, so the lesson-10 trap (a buggy
9679        // `is_else_if` that returns false makes `elsif` nest like
9680        // `if`) doesn't apply directly here — the test still pins the
9681        // chain vs nested cost difference so a future refactor that
9682        // mis-classifies `Elsif` would regress it.
9683        // expected: chain = 1 (`if`) + 2 (two `elsif`) + 1 (`else`) = 4;
9684        // nested = 1 + 2 + 3 = 6. The literal `4 < 6` asserts the
9685        // intended relationship.
9686        check_metrics::<RubyParser>(
9687            "def foo\n  if a\n    1\n  elsif b\n    2\n  elsif c\n    3\n  else\n    4\n  end\nend\n",
9688            "foo.rb",
9689            |metric| {
9690                assert_eq!(metric.cognitive.cognitive_sum(), 4);
9691                insta::assert_json_snapshot!(metric.cognitive);
9692            },
9693        );
9694        check_metrics::<RubyParser>(
9695            "def foo\n  if a\n    if b\n      if c\n        1\n      end\n    end\n  end\nend\n",
9696            "foo.rb",
9697            |metric| {
9698                assert_eq!(metric.cognitive.cognitive_sum(), 6);
9699            },
9700        );
9701    }
9702
9703    #[test]
9704    fn ruby_case_else_no_extra_increment() {
9705        // #451: the `else` arm of a `case/when` is the default arm of a
9706        // switch-like construct. The `case` node already pays nesting
9707        // (+1), so the default arm must add +0 — adding `else` to a
9708        // `case` must not change the cognitive score.
9709        //
9710        // Pre-fix, the shared `R::Elsif | R::Else` arm added +1 to the
9711        // case-`else`, scoring 2 (revert-verified). Now both forms score 1.
9712        let case_with_else = "case x\nwhen 1 then 1\nelse 0\nend\n";
9713        let case_without_else = "case x\nwhen 1 then 1\nwhen 2 then 2\nend\n";
9714        check_metrics::<RubyParser>(case_with_else, "foo.rb", |metric| {
9715            assert_eq!(metric.cognitive.cognitive_sum(), 1);
9716            insta::assert_json_snapshot!(metric.cognitive, @r#"
9717            {
9718              "sum": 1,
9719              "value": 1,
9720              "average": 1.0,
9721              "min": 1,
9722              "max": 1
9723            }
9724            "#);
9725        });
9726        check_metrics::<RubyParser>(case_without_else, "foo.rb", |metric| {
9727            assert_eq!(metric.cognitive.cognitive_sum(), 1);
9728        });
9729    }
9730
9731    #[test]
9732    fn ruby_case_else_matches_kotlin_when_and_java_switch() {
9733        // #451 cross-language parity (lesson #11): the catch-all arm of a
9734        // switch-like construct scores identically across languages. Ruby
9735        // `case`/`else`, Kotlin `when`/`else`, and Java `switch`/`default`
9736        // must all report cognitive == 1 on the equivalent two-branch
9737        // construct (one match arm + the default arm).
9738        check_metrics::<RubyParser>("case x\nwhen 1 then 1\nelse 0\nend\n", "foo.rb", |metric| {
9739            assert_eq!(metric.cognitive.cognitive_sum(), 1);
9740        });
9741        check_metrics::<KotlinParser>(
9742            "fun f(x: Int): Int {\n    return when (x) {\n        1 -> 1\n        else -> 0\n    }\n}\n",
9743            "foo.kt",
9744            |metric| {
9745                assert_eq!(metric.cognitive.cognitive_sum(), 1);
9746            },
9747        );
9748        check_metrics::<JavaParser>(
9749            "class C {\n  int f(int x) {\n    switch (x) {\n      case 1: return 1;\n      default: return 0;\n    }\n  }\n}\n",
9750            "foo.java",
9751            |metric| {
9752                assert_eq!(metric.cognitive.cognitive_sum(), 1);
9753            },
9754        );
9755    }
9756
9757    #[test]
9758    fn ruby_if_else_still_counts() {
9759        // #451 over-suppression guard: the `else` of an `if`/`elsif` chain
9760        // is *not* switch-like (its parent is the `if`/`elsif` clause, not a
9761        // `case`), so it must still add +1. `if`(+1) + `else`(+1) = 2.
9762        check_metrics::<RubyParser>("if a\n  1\nelse\n  2\nend\n", "foo.rb", |metric| {
9763            assert_eq!(metric.cognitive.cognitive_sum(), 2);
9764        });
9765        // `begin`/`rescue`/`else` is the no-exception branch, mirroring
9766        // Python `try`/`except`/`else` (+1), not a switch default. The
9767        // `rescue`(+1) and `else`(+1) both count: total 2.
9768        check_metrics::<RubyParser>(
9769            "begin\n  foo\nrescue\n  bar\nelse\n  baz\nend\n",
9770            "foo.rb",
9771            |metric| {
9772                assert_eq!(metric.cognitive.cognitive_sum(), 2);
9773            },
9774        );
9775    }
9776
9777    #[test]
9778    fn ruby_stabby_and_keyword_lambda_nesting_parity() {
9779        // A stabby lambda parses as a `Lambda` node CONTAINING its own
9780        // body `Block`; the keyword form parses as a `Call` carrying the
9781        // block argument. Only the `Lambda` wrapper pays the lambda
9782        // nesting surcharge — its body block must not pay again, or the
9783        // same logical construct scores differently across the two
9784        // spellings (lesson #11; the #1257 shared discriminator).
9785        //
9786        // expected: `if`(+1) + one level of lambda nesting(+1) = 2.
9787        check_metrics::<RubyParser>("f = ->(a) { if a then 1 end }\n", "foo.rb", |metric| {
9788            assert_eq!(metric.cognitive.cognitive_sum(), 2);
9789        });
9790        // expected: identical derivation for the keyword form — the
9791        // `Block` under the `lambda` call is the sole closure node:
9792        // `if`(+1) + lambda nesting(+1) = 2.
9793        check_metrics::<RubyParser>("f = lambda { |a| if a then 1 end }\n", "foo.rb", |metric| {
9794            assert_eq!(metric.cognitive.cognitive_sum(), 2);
9795        });
9796    }
9797
9798    #[test]
9799    fn ruby_if_inside_stabby_lambda_inside_method() {
9800        // expected: the method boundary resets nesting for its contents
9801        // (top-level method, so function_depth stays 0); the stabby
9802        // lambda adds exactly ONE lambda-nesting level (`Lambda` wrapper
9803        // only — its body `Block` is gated out); the `if` then charges
9804        // 1 + nesting(0 conditional + 0 function_depth + 1 lambda) = 2.
9805        // Before the gate the body block paid a second surcharge and
9806        // this scored 3.
9807        check_metrics::<RubyParser>(
9808            "def foo\n  f = ->(a) { if a then 1 end }\nend\n",
9809            "foo.rb",
9810            |metric| {
9811                assert_eq!(metric.cognitive.cognitive_sum(), 2);
9812                assert_eq!(metric.cognitive.cognitive_max(), 2);
9813            },
9814        );
9815    }
9816
9817    #[test]
9818    fn ruby_do_block_spelling_matches_brace_spelling() {
9819        // The lambda-nesting arm is gated on `Block | DoBlock`, and the
9820        // two alternatives are separate dispatch paths: every other Ruby
9821        // cognitive fixture uses the brace spelling, so the `do … end`
9822        // half of both the arm and its stabby-lambda-body gate went
9823        // unexercised. A block's spelling is pure syntax — the same
9824        // closure must score identically either way (lesson 11).
9825        //
9826        // expected: the `Lambda` wrapper pays the one lambda-nesting
9827        // level and its `do_block` body is gated out, so `if` charges
9828        // 1 + nesting(1) = 2 — the same figure the brace form reports in
9829        // `ruby_stabby_and_keyword_lambda_nesting_parity`.
9830        check_metrics::<RubyParser>(
9831            "f = ->(a) do\n  if a then 1 end\nend\n",
9832            "foo.rb",
9833            |metric| {
9834                assert_eq!(metric.cognitive.cognitive_sum(), 2);
9835                assert_eq!(metric.cognitive.cognitive_max(), 2);
9836            },
9837        );
9838        // expected: an iterator `do … end` has a `Call` parent, not a
9839        // `Lambda`, so the gate lets it through and the `do_block` is
9840        // itself the closure: `if`(+1) + lambda nesting(+1) = 2. This is
9841        // the arm-taken direction of the same gate.
9842        check_metrics::<RubyParser>(
9843            "[1].each do |x|\n  if x then 1 end\nend\n",
9844            "foo.rb",
9845            |metric| {
9846                assert_eq!(metric.cognitive.cognitive_sum(), 2);
9847                assert_eq!(metric.cognitive.cognitive_max(), 2);
9848            },
9849        );
9850    }
9851
9852    #[test]
9853    fn javascript_labeled_break_continue() {
9854        // Per SonarSource Cognitive Complexity §B2 (issue #435), a labeled
9855        // `break LABEL` / `continue LABEL` is an unstructured jump and adds
9856        // +1. The JS-family grammar exposes the label as a
9857        // `statement_identifier` child of the break/continue node.
9858        check_metrics::<JavascriptParser>(
9859            "function scan(m) {
9860                outer:
9861                for (let i = 0; i < m.length; i++) {      // +1
9862                    for (let j = 0; j < m[i].length; j++) { // +2
9863                        if (m[i][j] < 0) continue outer;    // +3, +1
9864                        if (m[i][j] > 100) break outer;     // +3, +1
9865                    }
9866                }
9867            }",
9868            "foo.js",
9869            |metric| {
9870                // outer for(+1) + inner for(+2) + if(+3) + continue outer(+1)
9871                // + if(+3) + break outer(+1) = 11.
9872                assert_eq!(metric.cognitive.cognitive_sum(), 11);
9873                assert_eq!(metric.cognitive.cognitive_max(), 11);
9874                insta::assert_json_snapshot!(
9875                    metric.cognitive,
9876                    @r#"
9877                {
9878                  "sum": 11,
9879                  "value": 0,
9880                  "average": 11.0,
9881                  "min": 0,
9882                  "max": 11
9883                }
9884                "#
9885                );
9886            },
9887        );
9888    }
9889
9890    #[test]
9891    fn javascript_unlabeled_break_continue_not_counted() {
9892        // Negative test for issue #435: plain `break;` / `continue;` are
9893        // not unstructured jumps under SonarSource §B2 and add +0. Only the
9894        // surrounding `for` + two `if`s contribute.
9895        check_metrics::<JavascriptParser>(
9896            "function scan(m) {
9897                for (let i = 0; i < m.length; i++) { // +1
9898                    if (m[i] < 0) continue;           // +2, +0
9899                    if (m[i] > 100) break;            // +2, +0
9900                }
9901            }",
9902            "foo.js",
9903            |metric| {
9904                // for(+1) + if(+2) + if(+2) = 5.
9905                assert_eq!(metric.cognitive.cognitive_sum(), 5);
9906                assert_eq!(metric.cognitive.cognitive_max(), 5);
9907                insta::assert_json_snapshot!(
9908                    metric.cognitive,
9909                    @r#"
9910                {
9911                  "sum": 5,
9912                  "value": 0,
9913                  "average": 5.0,
9914                  "min": 0,
9915                  "max": 5
9916                }
9917                "#
9918                );
9919            },
9920        );
9921    }
9922
9923    #[test]
9924    fn typescript_labeled_break_continue() {
9925        // TS parity with JS for labeled jumps (issue #435): labeled
9926        // break/continue each add +1 via the `statement_identifier` child.
9927        check_metrics::<TypescriptParser>(
9928            "function scan(m: number[][]) {
9929                outer:
9930                for (let i = 0; i < m.length; i++) {      // +1
9931                    for (let j = 0; j < m[i].length; j++) { // +2
9932                        if (m[i][j] < 0) continue outer;    // +3, +1
9933                        if (m[i][j] > 100) break outer;     // +3, +1
9934                    }
9935                }
9936            }",
9937            "foo.ts",
9938            |metric| {
9939                assert_eq!(metric.cognitive.cognitive_sum(), 11);
9940                assert_eq!(metric.cognitive.cognitive_max(), 11);
9941                insta::assert_json_snapshot!(
9942                    metric.cognitive,
9943                    @r#"
9944                {
9945                  "sum": 11,
9946                  "value": 0,
9947                  "average": 11.0,
9948                  "min": 0,
9949                  "max": 11
9950                }
9951                "#
9952                );
9953            },
9954        );
9955    }
9956
9957    /// Asserts the JS-family function-boundary rule over every shape
9958    /// #1159 moves, for one instantiating language.
9959    ///
9960    /// `js_cognitive!` listed `FunctionDeclaration` alone, so a
9961    /// `method_definition` or a bound `function_expression` opened its own
9962    /// `SpaceKind::Function` space — `get_space_kind` maps both — while
9963    /// inheriting the enclosing conditional nesting and skipping the
9964    /// function-depth surcharge. Its `stops` list was short by the same
9965    /// two kinds, which is a separately observable bug: the reset shows on
9966    /// a definition nested in *conditionals*, the `stops` entry on one
9967    /// nested in another *function*. Both are covered below, plus the two
9968    /// shapes the fix must leave alone.
9969    fn check_js_function_boundary<T: ParserTrait>(filename: &str) {
9970        fn score(space: &FuncSpace, name: &str) -> u64 {
9971            function_space(space, name).metrics.cognitive.cognitive()
9972        }
9973
9974        // expected: `outer`'s two `if`s are +1 and +2, so `outer` scores
9975        // 3. The definition nested inside them restarts structural
9976        // nesting at 0, so its own `if` costs +1 base plus +1 function
9977        // depth (it is lexically inside `outer`) = 2 — the score the same
9978        // body written as a `function_declaration` already had, which is
9979        // why that form is asserted here as the control.
9980        //
9981        // Two conditional levels are load-bearing. At one level the
9982        // missing reset (+1) and the missing surcharge (-1) cancel and
9983        // both implementations report 2, so a one-level fixture cannot
9984        // discriminate.
9985        for (label, definition) in [
9986            (
9987                "function_declaration",
9988                "function inner(c) { if (c) { return 1; } }",
9989            ),
9990            (
9991                "method_definition",
9992                "class I { inner(c) { if (c) { return 1; } } }",
9993            ),
9994            (
9995                "function_expression",
9996                "const inner = function (c) { if (c) { return 1; } };",
9997            ),
9998            // Generators reach this arm since #1186. Before it,
9999            // `is_js_func!` called them closures, so neither form
10000            // reached the boundary and each scored 3.
10001            (
10002                "generator_function_declaration",
10003                "function* inner(c) { if (c) { yield 1; } }",
10004            ),
10005            (
10006                "generator_function",
10007                "const inner = function* (c) { if (c) { yield 1; } };",
10008            ),
10009        ] {
10010            let source =
10011                format!("function outer(a, b) {{ if (a) {{ if (b) {{ {definition} }} }} }}");
10012            check_func_space::<T, _>(&source, filename, |space| {
10013                assert_eq!(
10014                    score(&space, "outer"),
10015                    3,
10016                    "{label}: enclosing function's own score",
10017                );
10018                assert_eq!(
10019                    score(&space, "inner"),
10020                    2,
10021                    "{label}: nested definition restarts structural nesting",
10022                );
10023            });
10024        }
10025
10026        // The `stops` half, on a definition nested in another function
10027        // rather than in conditionals.
10028        // expected: `inner`'s `if` is +1 base plus +1 function depth = 2.
10029        // With the enclosing kind absent from `stops` the surcharge is 0
10030        // and `inner` scores 1.
10031        for (label, source) in [
10032            (
10033                "method_definition",
10034                "class I { m() { function inner(c) { if (c) { return 1; } } } }",
10035            ),
10036            (
10037                "function_expression",
10038                "const m = function () { function inner(c) { if (c) { return 1; } } };",
10039            ),
10040            // The independent half of #1186: a plain `function` nested
10041            // inside a *generator* got no depth surcharge, because the
10042            // generator was excluded from `stops` by the same
10043            // `is_js_func!` gate. This scored 1 before the fix while the
10044            // non-generator control above scored 2.
10045            (
10046                "generator_function_declaration",
10047                "function* m() { function inner(c) { if (c) { return 1; } } }",
10048            ),
10049            (
10050                "generator_function",
10051                "const m = function* () { function inner(c) { if (c) { return 1; } } };",
10052            ),
10053        ] {
10054            check_func_space::<T, _>(source, filename, |space| {
10055                assert_eq!(
10056                    score(&space, "inner"),
10057                    2,
10058                    "{label}: +1 base, +1 depth from the enclosing definition",
10059                );
10060            });
10061        }
10062
10063        // An *anonymous* `function_expression` used positionally fails
10064        // `check_if_func!` and is a *closure*, so it must keep falling
10065        // through to `_` and inheriting the enclosing nesting. This is what
10066        // pins that the gate was re-derived rather than the kind list
10067        // copied flat: an ungated arm resets here and reports 2, because
10068        // `outer` is a `FunctionDeclaration` and so a `stops` entry.
10069        // Anonymity is load-bearing — `check_if_func!`'s `$extra` disjunct
10070        // makes `run(function named (c) {…})` a function, and `nom` agrees.
10071        // expected: nesting.conditional 2 from `outer`'s two `if`s, so the
10072        // callback's own `if` costs +3. The `nom` assertion states the
10073        // premise — that this shape really is on the closure side of
10074        // `is_func` / `is_closure` — rather than leaving it implied.
10075        check_func_space::<T, _>(
10076            "function outer(a, b) {
10077                 if (a) { if (b) { run(function (c) { if (c) { return 1; } }); } }
10078             }",
10079            filename,
10080            |space| {
10081                assert_eq!(
10082                    space.metrics.nom.closures_sum(),
10083                    1,
10084                    "a positional function expression is a closure",
10085                );
10086                assert_eq!(
10087                    score(&space, "<anonymous>"),
10088                    3,
10089                    "a closure inherits the enclosing conditional nesting",
10090                );
10091            },
10092        );
10093
10094        // `ArrowFunction` was deliberately left out of the boundary set —
10095        // it owns the lambda channel in `js_cognitive!`'s `ArrowFunction`
10096        // arm — so sweeping it in is the other way to get this fix wrong.
10097        // expected: nesting.conditional 2 from `outer`'s two `if`s plus
10098        // nesting.lambda 1 from the arrow, so its `if` costs +4. A
10099        // boundary arm that swept `ArrowFunction` in would report 2.
10100        check_func_space::<T, _>(
10101            "function outer(a, b) {
10102                 if (a) { if (b) { const inner = (c) => { if (c) { return 1; } }; } }
10103             }",
10104            filename,
10105            |space| {
10106                assert_eq!(
10107                    score(&space, "inner"),
10108                    4,
10109                    "an arrow function keeps the lambda channel",
10110                );
10111            },
10112        );
10113    }
10114
10115    // One `#[test]` per language instantiating `js_cognitive!`: the macro
10116    // body is shared but each grammar's `kind_id`s are its own, so a
10117    // per-language enum drift is invisible from a single language's run.
10118    #[test]
10119    fn javascript_function_boundary_covers_methods_and_function_expressions_1159() {
10120        check_js_function_boundary::<JavascriptParser>("foo.js");
10121    }
10122
10123    #[test]
10124    fn mozjs_function_boundary_covers_methods_and_function_expressions_1159() {
10125        check_js_function_boundary::<MozjsParser>("foo.js");
10126    }
10127
10128    #[test]
10129    fn typescript_function_boundary_covers_methods_and_function_expressions_1159() {
10130        check_js_function_boundary::<TypescriptParser>("foo.ts");
10131    }
10132
10133    #[test]
10134    fn tsx_function_boundary_covers_methods_and_function_expressions_1159() {
10135        check_js_function_boundary::<TsxParser>("foo.tsx");
10136    }
10137
10138    #[test]
10139    fn javascript_compound_short_circuit_assignment_236() {
10140        // Regression for issue #236: `&&=`, `||=`, `??=` are compound
10141        // short-circuit assignments (e.g. `x ??= y` ≡ `x = x ?? y`)
10142        // and each carries one boolean-sequence decision. Each lives
10143        // inside its own `expression_statement`, so the boolean
10144        // sequence resets between them and all three count.
10145        check_metrics::<JavascriptParser>(
10146            "function f(x) {
10147                 x ??= 1; // +1 (??=)
10148                 x &&= 2; // +1 (&&=)
10149                 x ||= 3; // +1 (||=)
10150             }",
10151            "foo.js",
10152            |metric| {
10153                assert_eq!(metric.cognitive.cognitive_sum(), 3);
10154                assert_eq!(metric.cognitive.cognitive_max(), 3);
10155                insta::assert_json_snapshot!(
10156                    metric.cognitive,
10157                    @r#"
10158                {
10159                  "sum": 3,
10160                  "value": 0,
10161                  "average": 3.0,
10162                  "min": 0,
10163                  "max": 3
10164                }
10165                "#
10166                );
10167            },
10168        );
10169    }
10170
10171    #[test]
10172    fn typescript_compound_short_circuit_assignment_236() {
10173        // Regression for issue #236: TS parity with JS for `&&=`,
10174        // `||=`, `??=`.
10175        check_metrics::<TypescriptParser>(
10176            "function f(x: number | null) {
10177                 x ??= 1; // +1 (??=)
10178                 x &&= 2; // +1 (&&=)
10179                 x ||= 3; // +1 (||=)
10180             }",
10181            "foo.ts",
10182            |metric| {
10183                assert_eq!(metric.cognitive.cognitive_sum(), 3);
10184                assert_eq!(metric.cognitive.cognitive_max(), 3);
10185                insta::assert_json_snapshot!(
10186                    metric.cognitive,
10187                    @r#"
10188                {
10189                  "sum": 3,
10190                  "value": 0,
10191                  "average": 3.0,
10192                  "min": 0,
10193                  "max": 3
10194                }
10195                "#
10196                );
10197            },
10198        );
10199    }
10200
10201    #[test]
10202    fn tsx_compound_short_circuit_assignment_236() {
10203        // Regression for issue #236: TSX parity with JS/TS for `&&=`,
10204        // `||=`, `??=`.
10205        check_metrics::<TsxParser>(
10206            "function f(x: number | null) {
10207                 x ??= 1; // +1 (??=)
10208                 x &&= 2; // +1 (&&=)
10209                 x ||= 3; // +1 (||=)
10210             }",
10211            "foo.tsx",
10212            |metric| {
10213                assert_eq!(metric.cognitive.cognitive_sum(), 3);
10214                assert_eq!(metric.cognitive.cognitive_max(), 3);
10215                insta::assert_json_snapshot!(
10216                    metric.cognitive,
10217                    @r#"
10218                {
10219                  "sum": 3,
10220                  "value": 0,
10221                  "average": 3.0,
10222                  "min": 0,
10223                  "max": 3
10224                }
10225                "#
10226                );
10227            },
10228        );
10229    }
10230
10231    #[test]
10232    fn mozjs_compound_short_circuit_assignment_236() {
10233        // Regression for issue #236: Mozjs (SpiderMonkey-flavoured JS)
10234        // shares the JS macro and must score `&&=` / `||=` / `??=`
10235        // identically.
10236        check_metrics::<MozjsParser>(
10237            "function f(x) {
10238                 x ??= 1; // +1 (??=)
10239                 x &&= 2; // +1 (&&=)
10240                 x ||= 3; // +1 (||=)
10241             }",
10242            "foo.js",
10243            |metric| {
10244                assert_eq!(metric.cognitive.cognitive_sum(), 3);
10245                assert_eq!(metric.cognitive.cognitive_max(), 3);
10246                insta::assert_json_snapshot!(
10247                    metric.cognitive,
10248                    @r#"
10249                {
10250                  "sum": 3,
10251                  "value": 0,
10252                  "average": 3.0,
10253                  "min": 0,
10254                  "max": 3
10255                }
10256                "#
10257                );
10258            },
10259        );
10260    }
10261
10262    #[test]
10263    fn csharp_compound_short_circuit_assignment_236() {
10264        // Regression for issue #236: C#'s grammar only provides `??=`
10265        // among the short-circuit assignments (no `&&=` / `||=`). The
10266        // operator lives inside `assignment_expression` rather than a
10267        // `BinaryExpression`, so without the #236 fix it was silently
10268        // skipped.
10269        check_metrics::<CsharpParser>(
10270            "class C {
10271                 int? F(int? x) {
10272                     x ??= 1; // +1 (??=)
10273                     return x ?? 0;
10274                 }
10275             }",
10276            "foo.cs",
10277            |metric| {
10278                // Outer `??` chain (+1) + `??=` (+1) = 2 at function max.
10279                assert_eq!(metric.cognitive.cognitive_sum(), 2);
10280                assert_eq!(metric.cognitive.cognitive_max(), 2);
10281                insta::assert_json_snapshot!(
10282                    metric.cognitive,
10283                    @r#"
10284                {
10285                  "sum": 2,
10286                  "value": 0,
10287                  "average": 2.0,
10288                  "min": 0,
10289                  "max": 2
10290                }
10291                "#
10292                );
10293            },
10294        );
10295    }
10296
10297    #[test]
10298    fn php_compound_short_circuit_assignment_236() {
10299        // Regression for issue #236: PHP's only compound short-circuit
10300        // assignment is `??=` (no `&&=` / `||=`). It lives inside
10301        // `augmented_assignment_expression` rather than a
10302        // `BinaryExpression`, so without the #236 fix it was silently
10303        // skipped.
10304        check_metrics::<PhpParser>(
10305            "<?php
10306            function f($x) {
10307                $x ??= 1; // +1 (??=)
10308                return $x ?? 0; // +1 (??)
10309            }",
10310            "foo.php",
10311            |metric| {
10312                assert_eq!(metric.cognitive.cognitive_sum(), 2);
10313                assert_eq!(metric.cognitive.cognitive_max(), 2);
10314                insta::assert_json_snapshot!(
10315                    metric.cognitive,
10316                    @r#"
10317                {
10318                  "sum": 2,
10319                  "value": 0,
10320                  "average": 2.0,
10321                  "min": 0,
10322                  "max": 2
10323                }
10324                "#
10325                );
10326            },
10327        );
10328    }
10329
10330    /// A handler with no control flow has zero cognitive complexity.
10331    #[test]
10332    fn irules_no_cognitive() {
10333        check_metrics::<IrulesParser>("when X { set a 1 }\n", "foo.irule", |metric| {
10334            assert_eq!(metric.cognitive.cognitive_sum(), 0);
10335        });
10336    }
10337
10338    /// A single `if` adds one.
10339    #[test]
10340    fn irules_simple_function() {
10341        check_metrics::<IrulesParser>(
10342            "when X { if { $a } { log local0. \"hi\" } }\n",
10343            "foo.irule",
10344            |metric| {
10345                assert_eq!(metric.cognitive.cognitive_sum(), 1);
10346            },
10347        );
10348    }
10349
10350    /// A run of the *same* boolean operator (`$a && $b && $c`) is one
10351    /// sequence: `if` (1) + boolean sequence (1) = 2.
10352    #[test]
10353    fn irules_sequence_same_booleans() {
10354        check_metrics::<IrulesParser>(
10355            "when X { if { $a && $b && $c } { log local0. \"hi\" } }\n",
10356            "foo.irule",
10357            |metric| {
10358                assert_eq!(metric.cognitive.cognitive_sum(), 2);
10359            },
10360        );
10361    }
10362
10363    /// Switching operator (`$a && $b || $c`) starts a new sequence: `if` (1)
10364    /// + `&&` sequence (1) + `||` sequence (1) = 3.
10365    #[test]
10366    fn irules_sequence_different_booleans() {
10367        check_metrics::<IrulesParser>(
10368            "when X { if { $a && $b || $c } { log local0. \"hi\" } }\n",
10369            "foo.irule",
10370            |metric| {
10371                assert_eq!(metric.cognitive.cognitive_sum(), 3);
10372            },
10373        );
10374    }
10375
10376    /// Unary negation (`!`) does not itself add cognitive cost; only the
10377    /// boolean sequence does: `if` (1) + `&&` sequence (1) = 2.
10378    #[test]
10379    fn irules_not_booleans() {
10380        check_metrics::<IrulesParser>(
10381            "when X { if { !$a && !$b } { log local0. \"hi\" } }\n",
10382            "foo.irule",
10383            |metric| {
10384                assert_eq!(metric.cognitive.cognitive_sum(), 2);
10385            },
10386        );
10387    }
10388
10389    /// One level of nesting: `while` (1) + `if` (1 + nesting 1 = 2) = 3.
10390    #[test]
10391    fn irules_1_level_nesting() {
10392        check_metrics::<IrulesParser>(
10393            "when X { while { $a } { if { $b } { log local0. \"hi\" } } }\n",
10394            "foo.irule",
10395            |metric| {
10396                assert_eq!(metric.cognitive.cognitive_sum(), 3);
10397            },
10398        );
10399    }
10400
10401    /// Two levels: `while` (1) + `if` (2) + `foreach` (1 + nesting 2 = 3) = 6.
10402    #[test]
10403    fn irules_2_level_nesting() {
10404        check_metrics::<IrulesParser>(
10405            "when X { while { $a } { if { $b } { foreach z $l { log local0. \"hi\" } } } }\n",
10406            "foo.irule",
10407            |metric| {
10408                assert_eq!(metric.cognitive.cognitive_sum(), 6);
10409            },
10410        );
10411    }
10412
10413    /// The lesson-10 guard for `is_else_if`: an `if … elseif … elseif …
10414    /// else` chain (each clause +1 at the same level = 4) must score
10415    /// *lower* than the same number of `if`s nested inside one another
10416    /// (1 + 2 + 3 = 6, paying the nesting penalty). A broken `is_else_if`
10417    /// predicate that treated `elseif` like a fresh nested `if` would push
10418    /// the chain's score up toward the nested value, so the strict `<`
10419    /// assertion catches the regression that #115 found in Java/C#.
10420    #[test]
10421    fn irules_else_if_chain() {
10422        use std::cell::Cell;
10423
10424        let chain = "when X { if { $a } { set r 1 } elseif { $b } { set r 2 } elseif { $c } { set r 3 } else { set r 4 } }\n";
10425        let nested = "when X { if { $a } { if { $b } { if { $c } { set r 1 } } } }\n";
10426
10427        // Capture each measured sum through a `Cell` (check_func_space takes an
10428        // `Fn` closure) so the final `<` assertion compares the *actual*
10429        // values rather than restating constants.
10430        let chain_cog = Cell::new(-1.0);
10431        check_func_space::<IrulesParser, _>(chain, "chain.irule", |fs| {
10432            chain_cog.set(fs.metrics.cognitive.cognitive_sum() as f64);
10433        });
10434        let nested_cog = Cell::new(-1.0);
10435        check_func_space::<IrulesParser, _>(nested, "nested.irule", |fs| {
10436            nested_cog.set(fs.metrics.cognitive.cognitive_sum() as f64);
10437        });
10438
10439        assert_eq!(chain_cog.get(), 4.0);
10440        assert_eq!(nested_cog.get(), 6.0);
10441        assert!(
10442            chain_cog.get() < nested_cog.get(),
10443            "else-if chain ({}) must score lower than equivalently nested ifs ({})",
10444            chain_cog.get(),
10445            nested_cog.get(),
10446        );
10447    }
10448
10449    /// A `switch` nested in an `if`: `if` (1) + `switch` (1 + nesting 1 = 2)
10450    /// = 3. Confirms `switch` participates in nesting like other branches.
10451    #[test]
10452    fn irules_switch_nesting() {
10453        check_metrics::<IrulesParser>(
10454            "when X { if { $a } { switch $h { a { log local0. \"a\" } b { log local0. \"b\" } } } }\n",
10455            "foo.irule",
10456            |metric| {
10457                assert_eq!(metric.cognitive.cognitive_sum(), 3);
10458            },
10459        );
10460    }
10461
10462    /// `catch` is a conditional error handler — its body runs only when
10463    /// the guarded command errors — so it pays nesting like any other
10464    /// branch. Flat: 1. Nested in an `if`: `if` (1) + `catch`
10465    /// (1 + nesting 1 = 2) = 3, matching `irules_switch_nesting`.
10466    ///
10467    /// The `Catch` arm had no test before this: the whole arm measured
10468    /// zero-coverage while every other iRules branch kind was exercised.
10469    #[test]
10470    fn irules_catch_nesting() {
10471        check_metrics::<IrulesParser>("when X { catch { foo } }\n", "foo.irule", |metric| {
10472            assert_eq!(metric.cognitive.cognitive_sum(), 1);
10473        });
10474        check_metrics::<IrulesParser>(
10475            "when X { if { $a } { catch { foo } } }\n",
10476            "foo.irule",
10477            |metric| {
10478                assert_eq!(metric.cognitive.cognitive_sum(), 3);
10479            },
10480        );
10481    }
10482
10483    /// Objective-C straight-line method body has zero cognitive
10484    /// complexity.
10485    #[test]
10486    fn objc_no_cognitive() {
10487        check_metrics::<ObjcParser>(
10488            "@implementation Foo
10489- (int)bar {
10490    int a = 1;
10491    return a;
10492}
10493@end
10494",
10495            "foo.m",
10496            |metric| {
10497                assert_eq!(metric.cognitive.cognitive_sum(), 0);
10498                insta::assert_json_snapshot!(metric.cognitive, @r#"
10499                {
10500                  "sum": 0,
10501                  "value": 0,
10502                  "average": 0.0,
10503                  "min": 0,
10504                  "max": 0
10505                }
10506                "#);
10507            },
10508        );
10509    }
10510
10511    /// Objective-C single `if` at method top level: +1, no nesting
10512    /// surcharge.
10513    #[test]
10514    fn objc_simple_if() {
10515        check_metrics::<ObjcParser>(
10516            "@implementation Foo
10517- (void)bar:(int)x {
10518    if (x > 0) {
10519        [self use:x];
10520    }
10521}
10522@end
10523",
10524            "foo.m",
10525            |metric| {
10526                assert_eq!(metric.cognitive.cognitive_sum(), 1);
10527                insta::assert_json_snapshot!(metric.cognitive, @r#"
10528                {
10529                  "sum": 1,
10530                  "value": 0,
10531                  "average": 1.0,
10532                  "min": 0,
10533                  "max": 1
10534                }
10535                "#);
10536            },
10537        );
10538    }
10539
10540    /// Objective-C chained booleans `a && b && c`: SonarSource counts
10541    /// one for the first `&&` and zero for each additional same-operator
10542    /// link in the sequence, so the whole `if (a && b && c)` is +1 (if)
10543    /// + 1 (one boolean sequence) = 2.
10544    #[test]
10545    fn objc_sequence_same_booleans() {
10546        check_metrics::<ObjcParser>(
10547            "@implementation Foo
10548- (void)bar:(int)a b:(int)b c:(int)c {
10549    if (a && b && c) {
10550        [self use:a];
10551    }
10552}
10553@end
10554",
10555            "foo.m",
10556            |metric| {
10557                assert_eq!(metric.cognitive.cognitive_sum(), 2);
10558                insta::assert_json_snapshot!(metric.cognitive, @r#"
10559                {
10560                  "sum": 2,
10561                  "value": 0,
10562                  "average": 2.0,
10563                  "min": 0,
10564                  "max": 2
10565                }
10566                "#);
10567            },
10568        );
10569    }
10570
10571    /// Objective-C nesting surcharge: an `if` nested inside a `for`
10572    /// scores `for` (+1) + `if` (+1 base +1 nesting) = 3.
10573    #[test]
10574    fn objc_nested() {
10575        check_metrics::<ObjcParser>(
10576            "@implementation Foo
10577- (void)bar:(NSArray *)arr {
10578    for (id x in arr) {
10579        if ([x boolValue]) {
10580            [self use:x];
10581        }
10582    }
10583}
10584@end
10585",
10586            "foo.m",
10587            |metric| {
10588                assert_eq!(metric.cognitive.cognitive_sum(), 3);
10589                insta::assert_json_snapshot!(metric.cognitive, @r#"
10590                {
10591                  "sum": 3,
10592                  "value": 0,
10593                  "average": 3.0,
10594                  "min": 0,
10595                  "max": 3
10596                }
10597                "#);
10598            },
10599        );
10600    }
10601
10602    #[test]
10603    fn objc_block_nesting() {
10604        // A decision inside an ObjC block `^{ … }` picks up the lambda
10605        // surcharge: the `if` scores base (1) + lambda nesting (1) = 2,
10606        // exercising the `BlockLiteral => lambda += 1` path (the ObjC
10607        // closure analogue of the C++ lambda).
10608        check_metrics::<ObjcParser>(
10609            "@implementation Foo
10610- (void)bar {
10611    void (^blk)(int) = ^(int x) {
10612        if (x > 0) {
10613            [self use];
10614        }
10615    };
10616}
10617@end
10618",
10619            "foo.m",
10620            |metric| {
10621                assert_eq!(metric.cognitive.cognitive_sum(), 2);
10622                insta::assert_json_snapshot!(metric.cognitive, @r#"
10623                {
10624                  "sum": 2,
10625                  "value": 0,
10626                  "average": 1.0,
10627                  "min": 0,
10628                  "max": 2
10629                }
10630                "#);
10631            },
10632        );
10633    }
10634
10635    /// Objective-C `if / else if / else if / else` chain must score
10636    /// LOWER than the same number of singly-nested `if`s, because
10637    /// else-if links add no nesting surcharge while deepening `if`s do.
10638    /// This guards the `is_else_if` predicate (a regression that failed
10639    /// to recognise the else-if extension would inflate the chain to the
10640    /// nested score).
10641    #[test]
10642    fn objc_else_if_chain() {
10643        use std::cell::Cell;
10644
10645        let chain_sum = Cell::new(u64::MAX);
10646        check_func_space::<ObjcParser, _>(
10647            "@implementation Foo
10648- (int)bar:(int)x {
10649    if (x == 1) {
10650        return 1;
10651    } else if (x == 2) {
10652        return 2;
10653    } else if (x == 3) {
10654        return 3;
10655    } else {
10656        return 0;
10657    }
10658}
10659@end
10660",
10661            "foo.m",
10662            |fs| chain_sum.set(fs.metrics.cognitive.cognitive_sum()),
10663        );
10664
10665        let nested_sum = Cell::new(u64::MAX);
10666        check_func_space::<ObjcParser, _>(
10667            "@implementation Foo
10668- (int)bar:(int)x {
10669    if (x == 1) {
10670        if (x == 2) {
10671            if (x == 3) {
10672                return 3;
10673            }
10674        }
10675    }
10676    return 0;
10677}
10678@end
10679",
10680            "foo.m",
10681            |fs| nested_sum.set(fs.metrics.cognitive.cognitive_sum()),
10682        );
10683
10684        // expected chain (matches the C-family else-if structure): each
10685        // `else if`/`else` adds +1 with NO nesting surcharge because
10686        // `is_else_if` recognises the else-clause-nested `if_statement` as
10687        // a branch extension — if(+1) + else-if(+1) + else-if(+1) +
10688        // else(+1) = 4. Were the predicate broken, the nested
10689        // `if_statement`s would accrue nesting (+2, +3) and the chain
10690        // would climb to 7. expected nested: if(+1) + if(+1+1) +
10691        // if(+1+2) = 6. The chain must remain strictly cheaper.
10692        assert_eq!(chain_sum.get(), 4, "else-if chain cognitive sum");
10693        assert_eq!(nested_sum.get(), 6, "triple-nested if cognitive sum");
10694        assert!(
10695            chain_sum.get() < nested_sum.get(),
10696            "else-if chain ({}) must score lower than triple-nested ifs ({})",
10697            chain_sum.get(),
10698            nested_sum.get(),
10699        );
10700    }
10701
10702    /// Pins that `function_depth` and `lambda` are distinguishable.
10703    ///
10704    /// They are summed symmetrically almost everywhere, so most inputs
10705    /// cannot tell them apart. The asymmetric operation is
10706    /// `enter_function_boundary`'s `lambda = 0`, which clears one field
10707    /// while `increment_function_depth` raises the other — since #1187
10708    /// that pair runs for every language, not only the JS macro.
10709    ///
10710    /// The doubled arrow is still load-bearing, for the reason it always
10711    /// was: a swap at the write site transposes the pair at every node
10712    /// on the way down, and the plain
10713    /// `arrow -> statement_block -> function_declaration` chain has odd
10714    /// parity and totals the same either way. A mutant writing
10715    /// `function_depth = 0` in place of `lambda = 0` leaves `lambda 2,
10716    /// function_depth 1` here and charges the `if` 4 rather than 2.
10717    #[test]
10718    fn javascript_function_depth_and_lambda_are_distinguishable() {
10719        // expected: `inner` takes the boundary, so `conditional` and
10720        // `lambda` both reset to 0. `ArrowFunction` joined the `stops`
10721        // list in #1187, so `inner` earns a function-depth surcharge of
10722        // 1 — `increment_function_depth` asks whether *any* ancestor is a
10723        // stop, not how many, so two arrows still give 1. The `if` costs
10724        // 1 base + 1 depth = 2, up from 1 before the arrow entered
10725        // `stops`.
10726        check_metrics::<JavascriptParser>(
10727            "const f = () => () => { function inner() { if (a) { } } };",
10728            "nest.js",
10729            |metric| {
10730                assert_eq!(metric.cognitive.cognitive_sum(), 2);
10731            },
10732        );
10733    }
10734
10735    /// The `ArrowFunction` arm's own `lambda += 1`, pinned separately:
10736    /// with no `function_declaration` between the arrow and the `if`,
10737    /// nothing resets lambda, so the arrow's level reaches the `if`.
10738    #[test]
10739    fn javascript_arrow_contributes_lambda_nesting() {
10740        // expected: 1 for the `if`, +1 for the enclosing arrow level.
10741        check_metrics::<JavascriptParser>(
10742            "const f = () => { if (a) { } };",
10743            "arrow.js",
10744            |metric| {
10745                assert_eq!(metric.cognitive.cognitive_sum(), 2);
10746            },
10747        );
10748    }
10749
10750    /// Nesting is still inherited correctly thousands of levels deep
10751    /// (#1062).
10752    ///
10753    /// `get_nesting_from_map` used to recover a node's inherited nesting
10754    /// via `node.parent()`, which is `O(depth)` — tree-sitter stores no
10755    /// parent pointer — making the metric `O(nodes × depth)`. The walker
10756    /// now seeds each child's slot from its parent's, so the lookup is
10757    /// `O(1)`.
10758    ///
10759    /// Both versions produce identical numbers, only at different
10760    /// speeds, so what this test pins is correctness at depth. The
10761    /// *cost* is pinned by the `cognitive/nested-while` probe in the
10762    /// benchmark harness (#1068), which asserts the complexity class —
10763    /// `cargo bench -p big-code-analysis-bench --bench scaling`.
10764    ///
10765    /// The wall-clock half used to live here and produced a false
10766    /// failure in four environments: `windows-latest` in CI (10.9 s
10767    /// against an 8 s absolute budget), a local `make pre-commit`
10768    /// running clippy and rustdoc alongside the suite (5.6x), the same
10769    /// host under heavy parallel load (3.9x), and `cargo llvm-cov`,
10770    /// whose instrumentation skewed even a best-of-three ratio to 3.5x.
10771    /// The `coverage` job runs in CI, so leaving it armed redded the
10772    /// build on a measurement artefact rather than on a regression. A
10773    /// ratio between two depths is host-independent but not
10774    /// load-independent, and the fix for that is interleaved
10775    /// measurement at three depths, which belongs in a bench target
10776    /// and not in the unit suite.
10777    ///
10778    /// Uses `while`, deliberately, **not** `if`: when this test was
10779    /// written `Checker::is_else_if` still called `node.parent()` for
10780    /// every `if_statement`, so nested `if`s were quadratic for reasons
10781    /// that fix did not touch. #1084 moved that predicate onto the
10782    /// walker's ancestor chain, and the harness now measures the `if`
10783    /// shape under the same linear bound as this one.
10784    #[test]
10785    fn cognitive_nesting_is_inherited_at_depth() {
10786        // Restricted to `Cognitive` — which pulls in `Nom` as a declared
10787        // dependency, so this narrows the work rather than isolating it.
10788        fn cognitive_of(source: &str) -> u64 {
10789            crate::test_support::metrics_verbatim(
10790                crate::LANG::C,
10791                source.as_bytes(),
10792                crate::MetricsOptions::default().with_only(&[crate::Metric::Cognitive]),
10793            )
10794            .cognitive
10795            .cognitive_sum()
10796        }
10797
10798        // Each level adds its own nesting penalty, so cognitive grows as
10799        // 1 + 2 + … + n. Asserting the closed form at depth is what pins
10800        // that nesting is still inherited rather than recomputed.
10801        let nested_whiles = |n: usize| -> String {
10802            format!(
10803                "int main(){{ {} 1; {} }}\n",
10804                "while (a) { ".repeat(n),
10805                "} ".repeat(n)
10806            )
10807        };
10808        let expected = |n: u64| n * (n + 1) / 2;
10809
10810        assert_eq!(cognitive_of(&nested_whiles(3)), expected(3), "1 + 2 + 3");
10811        assert_eq!(cognitive_of(&nested_whiles(2_000)), expected(2_000));
10812    }
10813
10814    /// Function-nesting depth is still counted correctly thousands of
10815    /// levels deep (#1062).
10816    ///
10817    /// `increment_function_depth` asks whether any ancestor of a
10818    /// function node is itself a function. It used to climb with
10819    /// `node.parent()`, which is `O(depth)` per step, so `Cognitive`
10820    /// stayed `O(depth²)` on nested definitions after the nesting-map
10821    /// half of #1062 was fixed. The scan now walks the ancestor chain
10822    /// the walker hands down.
10823    ///
10824    /// Both versions answer identically, so what this pins is the
10825    /// arithmetic at depth; the *cost* is pinned by the
10826    /// `cognitive/nested-fn` probe in the benchmark harness
10827    /// (`cargo bench -p big-code-analysis-bench --bench scaling`),
10828    /// which asserts the complexity class.
10829    #[test]
10830    fn cognitive_function_depth_is_inherited_at_depth() {
10831        fn cognitive_of(source: &str) -> u64 {
10832            crate::test_support::metrics_verbatim(
10833                crate::LANG::Rust,
10834                source.as_bytes(),
10835                crate::MetricsOptions::default().with_only(&[crate::Metric::Cognitive]),
10836            )
10837            .cognitive
10838            .cognitive_sum()
10839        }
10840
10841        // The function at level k has k enclosing functions, so its
10842        // `if` costs k + 1 and the file totals 1 + 2 + … + n. The `if`
10843        // is what makes the depth observable: a chain of bare functions
10844        // scores zero however the depth is computed.
10845        let nested_fns = |n: usize| -> String {
10846            format!(
10847                "{}let x = 1;{}\n",
10848                "fn f() { if a {} ".repeat(n),
10849                "} ".repeat(n)
10850            )
10851        };
10852        let expected = |n: u64| n * (n + 1) / 2;
10853
10854        assert_eq!(cognitive_of(&nested_fns(3)), expected(3), "1 + 2 + 3");
10855        // Half the depth of `cognitive_nesting_is_inherited_at_depth`
10856        // because each level here also opens a `FuncSpace`. The debug
10857        // build is no longer the reason: #1122 took the `Node::parent`
10858        // re-derivation out of `Ancestors::checked`, so an unoptimised
10859        // walk is linear like the release one and this case dropped from
10860        // ~1.0 s to ~0.02 s. `make chain-audit` puts the quadratic
10861        // assertion back deliberately.
10862        assert_eq!(cognitive_of(&nested_fns(1_000)), expected(1_000));
10863    }
10864
10865    /// A function nested inside another makes its `if` cost one more
10866    /// than the same `if` at the top level, in every language that can
10867    /// express the nesting (#1062).
10868    ///
10869    /// That surcharge has exactly one source: `increment_function_depth`,
10870    /// which asks whether any ancestor of a function node is itself a
10871    /// function. #1062 moved the scan off `Node::parent` — `O(depth)` per
10872    /// step, and so quadratic over a deeply nested file — and onto the
10873    /// ancestor chain the walker hands down. The flat source in each row
10874    /// is the control: the same `if` with nothing enclosing its function,
10875    /// which must stay at 1, so the pair measures the surcharge and not
10876    /// the body.
10877    ///
10878    /// These are the languages whose function-depth arm had no test of
10879    /// its own; C++, C#, Groovy, Java, Kotlin, Perl, PHP, Python, Rust
10880    /// and Tcl are covered by dedicated tests elsewhere in this module.
10881    /// Go is deliberately absent: its stop set is `function_declaration`
10882    /// / `method_declaration` and the grammar allows neither inside a
10883    /// function body, so the surcharge is unreachable there — a nested
10884    /// Go function is a `func_literal`, which takes the `lambda` path.
10885    #[test]
10886    fn function_depth_surcharge_holds_across_languages() {
10887        use crate::test_support::metrics_verbatim;
10888
10889        fn cognitive_of(lang: LANG, source: &str) -> u64 {
10890            metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
10891                .cognitive
10892                .cognitive_sum()
10893        }
10894
10895        // Every row must parse cleanly. Several of these snippets lean
10896        // on a grammar's less-travelled corners — GNU nested functions
10897        // in C, a `function` statement inside another in Lua, a `proc`
10898        // inside a `proc` in iRules — and a grammar bump that stopped
10899        // accepting one would leave that row measuring `tree_sitter`'s
10900        // error recovery while still reporting 1 and 2. Verified: with
10901        // trailing garbage appended to the C source, the costs below
10902        // are unmoved and the test stays green without this check.
10903        fn parses_cleanly(lang: LANG, source: &str) -> bool {
10904            crate::Ast::parse(crate::Source::new(lang, source.as_bytes()))
10905                .is_ok_and(|ast| !ast.as_tree_sitter().root_node().has_error())
10906        }
10907
10908        // C: a GNU nested function definition.
10909        const C_FLAT: &str = "void f(int a) { if (a) { } }\n";
10910        const C_NESTED: &str = "void f(int a) { void g(int b) { if (b) { } } }\n";
10911        // Objective-C reuses C's `function_definition` stop but adds
10912        // `method_definition`, which only a real `@implementation`
10913        // reaches — running the C source here would duplicate the row
10914        // above and leave that extra stop untested.
10915        const OBJC_FLAT: &str = "@implementation A\n- (void)m:(int)a { if (a) { } }\n@end\n";
10916        const OBJC_NESTED: &str =
10917            "@implementation A\n- (void)m:(int)a { void g(int b) { if (b) { } } }\n@end\n";
10918        // C++ has no nested function definitions; a method on a local
10919        // struct is the nesting the grammar does admit (see
10920        // `cpp_nested_function_resets_nesting_and_adds_depth`).
10921        const CPP_FLAT: &str = "struct S { void f(bool a) { if (a) { } } };\n";
10922        const CPP_NESTED: &str =
10923            "struct S { void f(bool a) { struct I { void g(bool b) { if (b) { } } }; } };\n";
10924        const JS_FLAT: &str = "function f(a) { if (a) { } }\n";
10925        const JS_NESTED: &str = "function f(a) { function g(b) { if (b) { } } }\n";
10926        const RUBY_FLAT: &str = "def f\nif a\nend\nend\n";
10927        const RUBY_NESTED: &str = "def f\ndef g\nif a\nend\nend\nend\n";
10928        const LUA_FLAT: &str = "function f() if a then end end\n";
10929        const LUA_NESTED: &str = "function f() function g() if a then end end end\n";
10930        const BASH_FLAT: &str = "f() {\nif [ -n \"$a\" ]; then :; fi\n}\n";
10931        const BASH_NESTED: &str = "f() {\ng() {\nif [ -n \"$a\" ]; then :; fi\n}\n}\n";
10932        const TCL_FLAT: &str = "proc outer {x} {\nif {$x > 0} {\nputs positive\n}\n}\n";
10933        const TCL_NESTED: &str =
10934            "proc outer {x} {\nproc inner {y} {\nif {$y > 0} {\nputs positive\n}\n}\n}\n";
10935
10936        let rows = [
10937            (LANG::C, C_FLAT, C_NESTED),
10938            (LANG::Objc, OBJC_FLAT, OBJC_NESTED),
10939            (LANG::Mozcpp, CPP_FLAT, CPP_NESTED),
10940            (LANG::Javascript, JS_FLAT, JS_NESTED),
10941            (LANG::Mozjs, JS_FLAT, JS_NESTED),
10942            (LANG::Typescript, JS_FLAT, JS_NESTED),
10943            (LANG::Tsx, JS_FLAT, JS_NESTED),
10944            (LANG::Ruby, RUBY_FLAT, RUBY_NESTED),
10945            (LANG::Lua, LUA_FLAT, LUA_NESTED),
10946            (LANG::Bash, BASH_FLAT, BASH_NESTED),
10947            (LANG::Irules, TCL_FLAT, TCL_NESTED),
10948        ];
10949
10950        // Whole vectors rather than a per-row `assert_eq!`: when this
10951        // shared walk breaks it breaks for every language at once, and
10952        // comparing the columns shows all of them instead of stopping
10953        // at the first. Hand-rolling that diagnostic with a `wrong`
10954        // accumulator would work too, but its `push` arm is a branch no
10955        // passing run ever takes — dead weight that reads as a coverage
10956        // hole and never gets exercised.
10957        let measured: Vec<(LANG, u64, u64)> = rows
10958            .iter()
10959            .map(|&(lang, flat, nested)| {
10960                for (label, source) in [("flat", flat), ("nested", nested)] {
10961                    assert!(
10962                        parses_cleanly(lang, source),
10963                        "{lang:?}: the {label} source must parse without an ERROR node:\n{source}",
10964                    );
10965                }
10966                (lang, cognitive_of(lang, flat), cognitive_of(lang, nested))
10967            })
10968            .collect();
10969        let expected: Vec<(LANG, u64, u64)> = rows.iter().map(|&(lang, ..)| (lang, 1, 2)).collect();
10970
10971        assert_eq!(
10972            measured, expected,
10973            "an `if` must cost 1 in a top-level function and 2 one function deeper",
10974        );
10975    }
10976
10977    /// Every [`Nesting`] channel contributes to `total()`.
10978    ///
10979    /// Distinct powers of two, so dropping a channel or summing one
10980    /// twice — the failure modes of the open-coded
10981    /// `conditional + function_depth + lambda` this method replaced at
10982    /// its two sites (#1086) — is distinguishable from the total alone
10983    /// rather than just reading as a bad number.
10984    #[test]
10985    fn nesting_total_sums_every_channel() {
10986        assert_eq!(
10987            Nesting {
10988                conditional: 1,
10989                function_depth: 2,
10990                lambda: 4,
10991            }
10992            .total(),
10993            7,
10994            "1/2/4 encoding: short by 1 means `conditional` was dropped, \
10995             by 2 `function_depth`, by 4 `lambda`; over by the same \
10996             amount means that channel was summed twice",
10997        );
10998        // Weak on its own — every field is zero, so this survives any
10999        // linear combination of them. It only rules out a `total()` that
11000        // returns a nonzero constant.
11001        assert_eq!(Nesting::default().total(), 0);
11002    }
11003
11004    /// `increase_nesting` charges the *summed* level but advances only
11005    /// the `conditional` channel.
11006    ///
11007    /// Before #1086 this helper took `&mut usize` for the conditional
11008    /// channel and `depth` / `lambda` as by-value non-`mut` params, so
11009    /// bumping the wrong one was *inert* rather than unrepresentable:
11010    /// `depth += 1` needed a `mut` added to compile, and then wrote to a
11011    /// copy nobody read. It now holds the whole struct, which makes
11012    /// `function_depth += 1` a one-character slip that persists —
11013    /// invisible in the *charge* itself, since `total()` is symmetric,
11014    /// and detectable only downstream where the channels are read apart.
11015    ///
11016    /// Perturbing the production line to `function_depth += 1` fails this
11017    /// test plus five nested-function tests (`java_nested_method_…`,
11018    /// `cpp_nested_function_…`, `groovy_…`, `php_…`,
11019    /// `csharp_local_function_in_if_…`) — those catch it only because a
11020    /// function boundary resets `conditional` alone, leaving the misplaced
11021    /// increment behind. This test pins it at the helper, where the
11022    /// mistake is, rather than five languages away from it.
11023    #[test]
11024    fn increase_nesting_charges_the_total_but_advances_only_conditional() {
11025        let mut nesting = Nesting {
11026            conditional: 1,
11027            function_depth: 2,
11028            lambda: 4,
11029        };
11030        // Both fields are seeded rather than defaulted. From
11031        // `Stats::default()` the `boolean_seq` assertion holds even with
11032        // `reset()` deleted, and the `structural` assertion cannot tell
11033        // `increment`'s `+=` from a plain `=`, since both start at zero.
11034        let mut stats = Stats {
11035            structural: 5,
11036            boolean_seq: BoolSequence {
11037                boolean_op: Some((1, 0)),
11038            },
11039            ..Stats::default()
11040        };
11041
11042        increase_nesting(&mut stats, &mut nesting);
11043
11044        // Charged at the inherited level (7), and `increment`
11045        // accumulates `nesting + 1` onto the seeded 5.
11046        assert_eq!(stats.nesting, 7);
11047        assert_eq!(stats.structural, 13);
11048        assert_eq!(stats.boolean_seq, BoolSequence::default());
11049        assert_eq!(
11050            nesting,
11051            Nesting {
11052                conditional: 2,
11053                function_depth: 2,
11054                lambda: 4,
11055            }
11056        );
11057    }
11058}
11059
11060/// The nameless constructs from #1184 are function boundaries, so a
11061/// deeply-nested one must score what the same body scores as an
11062/// ordinary method in the same position (#1184).
11063///
11064/// Each opens a `FuncSpace`, and without a cognitive boundary arm it
11065/// reached none and inherited the enclosing conditional nesting: a
11066/// Kotlin accessor nested two `if`s deep scored 7 where the method
11067/// beside it scored 5.
11068///
11069/// **Two levels of nesting are load-bearing.** At one level the fixture
11070/// reports the same number either way, which is the trap the issue's own
11071/// checklist warns about — a first draft of this test used one `if` and
11072/// could not discriminate the fix from its absence.
11073///
11074/// The comparison is against a *sibling method* rather than an absolute
11075/// number, so the assertion states the property (these are ordinary
11076/// function boundaries) rather than a value that moves with any
11077/// unrelated re-tuning. The absolute is pinned too, so a regression
11078/// moving both equally still fails.
11079#[cfg(test)]
11080mod nameless_construct_boundaries {
11081    use crate::test_support::space_verbatim;
11082    use crate::{FuncSpace, LANG, MetricsOptions};
11083
11084    fn score(lang: LANG, source: &str, name: &str) -> u64 {
11085        fn find(s: &FuncSpace, name: &str) -> Option<u64> {
11086            if s.name.as_deref() == Some(name) {
11087                return Some(s.metrics.cognitive.cognitive());
11088            }
11089            s.spaces.iter().find_map(|c| find(c, name))
11090        }
11091        let root = space_verbatim(lang, source.as_bytes(), MetricsOptions::default());
11092        find(&root, name)
11093            .unwrap_or_else(|| panic!("{lang:?}: no space named {name:?} in the fixture"))
11094    }
11095
11096    /// `(language, source, construct name, sibling method name)`. Each
11097    /// fixture nests a class two `if`s deep and gives it both the
11098    /// nameless construct and an ordinary method with a byte-identical
11099    /// body.
11100    fn cases() -> Vec<(LANG, &'static str, &'static str, &'static str)> {
11101        vec![
11102            (
11103                LANG::Kotlin,
11104                "fun outer(a: Boolean) { if (a) { if (a) { class D {\n\
11105                 \x20   var q: Int = 0\n\
11106                 \x20       get() { if (q > 0) { if (q > 1) { return 2 } }; return 0 }\n\
11107                 \x20   fun m(): Int { if (q > 0) { if (q > 1) { return 2 } }; return 0 }\n\
11108                 } } } }\n",
11109                "<get>",
11110                "m",
11111            ),
11112            (
11113                LANG::Java,
11114                "class K { void outer(boolean a) { if (a) { if (a) { class D {\n\
11115                 \x20   static int x;\n\
11116                 \x20   static { if (x > 0) { if (x > 1) { x = 2; } } }\n\
11117                 \x20   void m() { if (x > 0) { if (x > 1) { x = 2; } } }\n\
11118                 } } } } }\n",
11119                "<static-init>",
11120                "m",
11121            ),
11122            (
11123                LANG::Javascript,
11124                "function outer(a) { if (a) { if (a) { class D {\n\
11125                 \x20   static x;\n\
11126                 \x20   static { if (D.x > 0) { if (D.x > 1) { D.x = 2; } } }\n\
11127                 \x20   m() { if (D.x > 0) { if (D.x > 1) { D.x = 2; } } }\n\
11128                 } } } }\n",
11129                "<static-init>",
11130                "m",
11131            ),
11132        ]
11133    }
11134
11135    #[test]
11136    fn a_nested_nameless_construct_scores_like_a_sibling_method() {
11137        let mut checked = 0;
11138        for (lang, source, construct, method) in cases() {
11139            if !lang.is_enabled() {
11140                continue;
11141            }
11142            checked += 1;
11143            let (got, want) = (score(lang, source, construct), score(lang, source, method));
11144            assert_eq!(
11145                got, want,
11146                "{lang:?}: {construct} scored {got} where the sibling method scored {want}; \
11147                 the construct is inheriting the enclosing nesting",
11148            );
11149            // expected: two `if`s at +1 and +2 = 3, plus +1 each for the
11150            // function-depth surcharge from `outer` = 5. Pinned so a
11151            // regression that moved both sides equally still fails.
11152            assert_eq!(want, 5, "{lang:?}: the baseline itself moved");
11153        }
11154        assert!(
11155            checked > 0,
11156            "no language enabled; this test asserted nothing"
11157        );
11158    }
11159}