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