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