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