Skip to main content

big_code_analysis/metrics/
cognitive.rs

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