Skip to main content

big_code_analysis/metrics/
cognitive.rs

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