big_code_analysis/metrics/cognitive.rs
1// Per-language metric and AST modules deliberately consume the macro-
2// generated tree-sitter token enums via `use crate::*` and `use Foo::*`
3// inside match expressions — explicit imports would list dozens of
4// variants per arm and obscure the per-language token sets that are the
5// point of these files. Allowed at the module level rather than per
6// function so the per-language impl blocks stay readable.
7#![allow(
8 clippy::enum_glob_use,
9 clippy::match_same_arms,
10 clippy::needless_pass_by_value,
11 clippy::wildcard_imports
12)]
13// Metric counts (token, function, branch, argument, etc.) are stored as
14// `usize` and crossed with `f64` averages, ratios, and Halstead scores
15// across the cyclomatic / MI / Halstead computations. The `usize as f64`
16// and `f64 as usize` casts are intentional and snapshot-anchored — every
17// site is bounded by the count it came from. Allowing the lints at the
18// module level keeps the metric arithmetic legible.
19#![allow(
20 clippy::cast_precision_loss,
21 clippy::cast_possible_truncation,
22 clippy::cast_sign_loss
23)]
24
25use crate::spaces::{Nesting, NestingMap};
26
27use std::fmt;
28
29use crate::checker::Checker;
30use crate::lang_helpers::elixir::{elixir_call_keyword, elixir_is_method_macro};
31use crate::lang_helpers::python::python_is_lambda;
32use crate::lang_helpers::tcl::tcl_command_name;
33use crate::macros::implement_metric_trait;
34use crate::metrics::ConstraintReach;
35use crate::*;
36
37// TODO: Find a way to increment the cognitive complexity value
38// for recursive code. For some kind of languages, such as C++, it is pretty
39// hard to detect, just parsing the code, if a determined function is recursive
40// because the call graph of a function is solved at runtime.
41// So a possible solution could be searching for a crate which implements
42// a light language interpreter, computing the call graph, and then detecting
43// if there are cycles. At this point, it is possible to figure out if a
44// function is recursive or not.
45
46/// The `Cognitive Complexity` metric.
47#[derive(Debug, Clone, PartialEq)]
48#[non_exhaustive]
49pub struct Stats {
50 structural: usize,
51 structural_sum: usize,
52 structural_min: usize,
53 structural_max: usize,
54 nesting: usize,
55 total_space_functions: usize,
56 boolean_seq: BoolSequence,
57 constraint: ConstraintReach,
58 bash_runs: bash::ComparisonRuns,
59}
60
61impl Default for Stats {
62 fn default() -> Self {
63 Self {
64 structural: 0,
65 structural_sum: 0,
66 structural_min: usize::MAX,
67 structural_max: 0,
68 nesting: 0,
69 total_space_functions: 1,
70 boolean_seq: BoolSequence::default(),
71 constraint: ConstraintReach::default(),
72 bash_runs: bash::ComparisonRuns::default(),
73 }
74 }
75}
76
77impl fmt::Display for Stats {
78 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
79 write!(
80 f,
81 "sum: {}, average: {}, min:{}, max: {}",
82 self.cognitive(),
83 self.cognitive_average(),
84 self.cognitive_min(),
85 self.cognitive_max()
86 )
87 }
88}
89
90impl Stats {
91 /// Merges a second `Cognitive Complexity` metric into the first one
92 pub fn merge(&mut self, other: &Stats) {
93 self.structural_min = self.structural_min.min(other.structural_min);
94 self.structural_max = self.structural_max.max(other.structural_max);
95 self.structural_sum += other.structural_sum;
96 }
97
98 /// Returns the `Cognitive Complexity` metric value
99 #[must_use]
100 pub fn cognitive(&self) -> u64 {
101 self.structural as u64
102 }
103 /// Returns the `Cognitive Complexity` sum metric value
104 #[must_use]
105 pub fn cognitive_sum(&self) -> u64 {
106 self.structural_sum as u64
107 }
108
109 /// Returns the `Cognitive Complexity` minimum metric value.
110 ///
111 /// Collapses the `usize::MAX` sentinel that `Stats::default()` plants
112 /// into `structural_min` to `0`, so a never-observed space
113 /// serializes to a meaningful number rather than `1.8446744e19`.
114 #[must_use]
115 pub fn cognitive_min(&self) -> u64 {
116 if self.structural_min == usize::MAX {
117 0
118 } else {
119 self.structural_min as u64
120 }
121 }
122 /// Returns the `Cognitive Complexity` maximum metric value
123 #[must_use]
124 pub fn cognitive_max(&self) -> u64 {
125 self.structural_max as u64
126 }
127
128 /// Returns the `Cognitive Complexity` metric average value
129 ///
130 /// This value is computed dividing the `Cognitive Complexity` value
131 /// for the total number of functions/closures in a space.
132 ///
133 /// The per-function divisor (shared with `cyclomatic`/`exit`/`nargs`,
134 /// #512) is guarded with `.max(1)` via the shared `average` helper, so
135 /// a space with no counted functions (or one where `Nom` was not
136 /// selected) degrades to `sum / 1` instead of producing `inf`/`NaN`
137 /// (#428).
138 #[must_use]
139 pub fn cognitive_average(&self) -> f64 {
140 crate::metrics::average(self.cognitive_sum() as f64, self.total_space_functions)
141 }
142 #[inline]
143 pub(crate) fn compute_sum(&mut self) {
144 self.structural_sum += self.structural;
145 }
146 #[inline]
147 pub(crate) fn compute_minmax(&mut self) {
148 self.structural_min = self.structural_min.min(self.structural);
149 self.structural_max = self.structural_max.max(self.structural);
150 self.compute_sum();
151 }
152
153 pub(crate) fn finalize(&mut self, total_space_functions: usize) {
154 self.total_space_functions = total_space_functions;
155 }
156}
157
158#[doc(hidden)]
159/// Per-language computation of the cognitive complexity metric.
160pub(crate) trait Cognitive
161where
162 Self: Checker,
163{
164 /// Walk `node` and update `stats` with this metric for the language
165 /// implementing the trait.
166 ///
167 /// `code` is the source bytes underlying the parsed tree. Most
168 /// languages ignore it: their control-flow constructs surface as
169 /// distinct grammar productions (`IfStatement`, `WhileStatement`,
170 /// …) and a `kind_id()` match is enough. Elixir is the exception
171 /// — `if` / `unless` / `case` / `cond` / `for` / `while` / `with`
172 /// all surface as `Call` nodes whose keyword target lives only in
173 /// the source text (the `target` field is an `Identifier`). This
174 /// matches the `Cyclomatic` / `Halstead` / `Exit` pattern of
175 /// taking `code` so the same source-text dispatch can run here.
176 ///
177 /// `ancestors` is the chain the walker descended through. The
178 /// grammars that spell `else if` as a nested `if` need an ancestor
179 /// to recognise the continuation, and Python needs one to find the
180 /// outermost operator of a boolean chain; resolving either from the
181 /// node alone costs `O(depth)` per node (#1084).
182 fn compute<'a>(
183 node: &Node<'a>,
184 code: &'a [u8],
185 ancestors: Ancestors<'a, '_>,
186 stats: &mut Stats,
187 nesting_map: &mut NestingMap,
188 );
189}
190
191/// Walks `node.children()` and folds each child whose `kind_id`
192/// satisfies `is_op` into the boolean-sequence counter. The predicate
193/// is the only thing that differs across the per-language short-
194/// circuit helpers (`compute_*_booleans`); inlining the predicate as
195/// a `Fn` closure lets each language declare its operator set with a
196/// `matches!` pattern at the call site without duplicating the walk.
197fn compute_booleans_with<F: Fn(u16) -> bool>(node: &Node, stats: &mut Stats, is_op: F) {
198 compute_booleans_keyed(node, stats, |id| is_op(id).then_some(id));
199}
200
201/// [`compute_booleans_with`] for a language that spells one operator
202/// with more than one token kind. `sequence_key` maps each operator
203/// child to the id the boolean sequence compares, so two spellings of
204/// one operator share a key and continue each other's sequence — C++'s
205/// `a and b && c` is one sequence, exactly as `a && b && c` is.
206///
207/// SonarSource's rule charges one increment per sequence of *like*
208/// operators because switching between conjunction and disjunction is
209/// what a reader must stop and work through. "Like" is therefore the
210/// logical operation, not the token: a word form is keyed to its
211/// symbol even where the language gives it a lower precedence (Ruby,
212/// PHP, Perl) or strict-boolean operands (Elixir). Precedence only
213/// changes which operator nests under which, and the parse already
214/// carries that, so `a || b and c` is `(a || b) and c` and still
215/// switches operation. Operators that are a different operation
216/// (`??`, `//`) keep keys of their own (#1530); `xor`, like `^`, is no
217/// sequence operator at all (#1536).
218fn compute_booleans_keyed<F: Fn(u16) -> Option<u16>>(
219 node: &Node,
220 stats: &mut Stats,
221 sequence_key: F,
222) {
223 compute_booleans_by_node(node, node.end_byte(), stats, |child| {
224 sequence_key(child.kind_id())
225 });
226}
227
228/// [`compute_booleans_keyed`] for an operator whose kind alone does not
229/// say which operation it is — Bash's `-a` / `-o` share `test_operator`
230/// with every comparison, so the key reads the child itself — and for a
231/// sequence whose extent is not `node`'s own span. `enclosing_end` is
232/// where the sequence `node` belongs to ends: an operator inside it
233/// continues a like operator seen earlier in it.
234fn compute_booleans_by_node<F: Fn(&Node) -> Option<u16>>(
235 node: &Node,
236 enclosing_end: usize,
237 stats: &mut Stats,
238 sequence_key: F,
239) {
240 for child in node.children() {
241 if let Some(key) = sequence_key(&child) {
242 stats.structural =
243 stats
244 .boolean_seq
245 .eval_based_on_prev(key, enclosing_end, stats.structural);
246 }
247 }
248}
249
250/// Two-operator specialization. Most call sites match exactly two
251/// enum variants (`&&` + `||`, or `and` + `or`); this signature
252/// keeps those call sites as plain `(node, stats, A, B)` rather than
253/// forcing a closure.
254fn compute_booleans<T: PartialEq + From<u16>>(node: &Node, stats: &mut Stats, typs1: T, typs2: T) {
255 compute_booleans_with(node, stats, |id| {
256 let converted: T = id.into();
257 typs1 == converted || typs2 == converted
258 });
259}
260
261#[derive(Debug, Default, Clone, PartialEq)]
262struct BoolSequence {
263 boolean_op: Option<(u16, usize)>,
264}
265
266impl BoolSequence {
267 fn reset(&mut self) {
268 // Structural boundaries (new branches, nesting increments) end the current sequence.
269 self.boolean_op = None;
270 }
271
272 fn eval_based_on_prev(
273 &mut self,
274 bool_id: u16,
275 enclosing_end: usize,
276 structural: usize,
277 ) -> usize {
278 match self.boolean_op {
279 // Same operator type and enclosing_end fits inside the previously seen
280 // binary_expression span (pre-order: parent visited before child) →
281 // continuation of the same sequence, no extra cost.
282 Some((prev_id, prev_end)) if prev_id == bool_id && enclosing_end <= prev_end => {
283 structural
284 }
285 _ => {
286 self.boolean_op = Some((bool_id, enclosing_end));
287 structural + 1
288 }
289 }
290 }
291}
292
293#[inline]
294fn increment(stats: &mut Stats) {
295 stats.structural += stats.nesting + 1;
296}
297
298#[inline]
299fn increment_by_one(stats: &mut Stats) {
300 stats.structural += 1;
301}
302
303#[inline]
304fn increment_branch_extension(stats: &mut Stats) {
305 stats.structural += 1;
306 stats.boolean_seq.reset();
307}
308
309/// Returns the [`Nesting`] `node` inherits from its parent.
310///
311/// The map is keyed so that a node's own slot holds what it *inherits*:
312/// the walker seeds each child's slot from its parent's slot after the
313/// parent's `compute` has run (see `propagate_nesting_to_children` in
314/// `spaces::compute`). Reading `node.parent()` here instead would cost
315/// `O(depth)` per node — `Node::parent` walks down from the root — which
316/// made this metric quadratic in nesting depth (#1062).
317fn get_nesting_from_map(node: &Node, nesting_map: &NestingMap) -> Nesting {
318 nesting_map.get(&node.id()).copied().unwrap_or_default()
319}
320
321/// Adds one to `depth` when `node` is lexically nested inside another
322/// function, where `stops` lists the grammar kinds that count as a
323/// function for this language.
324///
325/// The scan reads the walker's ancestor chain. Climbing with
326/// [`Node::parent`] instead costs `O(depth)` per step — tree-sitter
327/// stores no parent pointer — which made `Cognitive` `O(depth²)` on
328/// nested function definitions (#1062, deferred out of #1084). A nested
329/// function's enclosing function is a couple of levels up, so the scan
330/// stops immediately there; a function with *no* enclosing function
331/// scans its whole chain, but each step is a slice index rather than a
332/// descent from the root.
333fn increment_function_depth<'a, T: PartialEq + From<u16>>(
334 depth: &mut usize,
335 node: &Node<'a>,
336 ancestors: Ancestors<'a, '_>,
337 stops: &[T],
338) {
339 if ancestors
340 .iter(node)
341 .any(|(ancestor, _)| stops.contains(&T::from(ancestor.kind_id())))
342 {
343 *depth += 1;
344 }
345}
346
347/// Applies the function-boundary rule at `node`, which every language
348/// with a syntactic function-definition kind shares (#696).
349///
350/// It moves all three of [`Nesting`]'s channels. Structural nesting and
351/// the lambda surcharge restart at zero, so control flow written inside
352/// this function is charged against its own depth rather than against
353/// whatever enclosed the definition; and the function-depth surcharge
354/// rises when this definition is itself lexically nested in one of
355/// `stops`. Byte-equivalent constructs therefore score the same across
356/// languages, which is the property the book's per-language deviations
357/// list states.
358///
359/// The lambda reset was the JS macro's alone until #1187. Every other
360/// language carried the enclosing closure's surcharge into a function
361/// *declared inside* it, so the same body scored 3 or 2 depending on
362/// whether something two levels up happened to be a closure — measured
363/// in Rust, Java, C++, PHP and C#, where a `LocalFunctionStatement`
364/// inside a lambda is idiomatic. A function declaration is a new lexical
365/// scope whatever encloses it, so the reset belongs to every boundary,
366/// and living here is what stops a language opting out by accident —
367/// which is how the gap arose.
368///
369/// The two statements were spelled out longhand in eighteen modules
370/// before #1103. One caller still spells them out: `elixir.rs` takes the
371/// resets and deliberately skips the depth bump, and says why at its own
372/// site.
373fn enter_function_boundary<'a, T: PartialEq + From<u16>>(
374 nesting: &mut Nesting,
375 node: &Node<'a>,
376 ancestors: Ancestors<'a, '_>,
377 stops: &[T],
378) {
379 nesting.conditional = 0;
380 nesting.lambda = 0;
381 increment_function_depth(&mut nesting.function_depth, node, ancestors, stops);
382}
383
384/// Charges `node`'s construct at the current nesting level and opens a
385/// new structural level for its children.
386///
387/// Takes the whole [`Nesting`] rather than its three same-typed fields
388/// positionally: the previous signature was
389/// `(stats, &mut nesting, depth, lambda)` at 43 call sites, where any
390/// two of the trailing arguments could be transposed silently (#1086).
391#[inline]
392fn increase_nesting(stats: &mut Stats, nesting: &mut Nesting) {
393 stats.nesting = nesting.total();
394 increment(stats);
395 nesting.conditional += 1;
396 stats.boolean_seq.reset();
397}
398
399macro_rules! js_cognitive {
400 ($lang:ident) => {
401 fn compute<'a>(
402 node: &Node<'a>,
403 _code: &'a [u8],
404 ancestors: Ancestors<'a, '_>,
405 stats: &mut Stats,
406 nesting_map: &mut NestingMap,
407 ) {
408 use $lang::*;
409 let mut nesting = get_nesting_from_map(node, nesting_map);
410
411 match node.kind_id().into() {
412 IfStatement if !Self::is_else_if(node, ancestors) => {
413 increase_nesting(stats, &mut nesting);
414 }
415 ForStatement | ForInStatement | WhileStatement | DoStatement | SwitchStatement
416 | CatchClause | TernaryExpression => {
417 increase_nesting(stats, &mut nesting);
418 }
419 // `Else` here is the `else` keyword token, which the
420 // grammar also emits for the `else` of an `else if` —
421 // so this arm covers both.
422 Else => {
423 increment_by_one(stats);
424 }
425 // Per SonarSource Cognitive Complexity §B2, a labeled
426 // `break LABEL` / `continue LABEL` is an unstructured jump
427 // and adds +1. The JS-family grammar exposes the label as a
428 // `StatementIdentifier` child (not the plain `Identifier`
429 // Java uses), so gate on that kind; plain `break;` /
430 // `continue;` have no such child and add +0.
431 BreakStatement | ContinueStatement if node.is_child(StatementIdentifier as u16) => {
432 increment_by_one(stats);
433 }
434 ExpressionStatement => {
435 // Reset the boolean sequence
436 stats.boolean_seq.reset();
437 }
438 BinaryExpression => {
439 // `??` (`QMARKQMARK`) short-circuits like `&&` /
440 // `||`, so a chain of `??` collapses to a single
441 // boolean-sequence increment under Sonar B1.
442 compute_booleans_with(node, stats, |id| {
443 matches!(id.into(), AMPAMP | PIPEPIPE | QMARKQMARK)
444 });
445 }
446 AugmentedAssignmentExpression => {
447 // Compound short-circuit assignments `&&=`, `||=`,
448 // `??=` are semantically `x = x op y` and each carries
449 // one boolean-sequence decision, parallel to the
450 // cyclomatic fix from #231. The operator token sits
451 // inside the augmented-assignment node rather than a
452 // `BinaryExpression`, so it needs its own arm (#236).
453 compute_booleans_with(node, stats, |id| {
454 matches!(id.into(), AMPAMPEQ | PIPEPIPEEQ | QMARKQMARKEQ)
455 });
456 }
457 FunctionDeclaration
458 | MethodDefinition
459 | FunctionExpression
460 | GeneratorFunctionDeclaration
461 | GeneratorFunction
462 if Self::is_func(node, ancestors) =>
463 {
464 // The kind set is `is_js_func!` minus `ArrowFunction`,
465 // and the `function_expression` half is re-derived by
466 // asking `Self::is_func` rather than copied flat, because
467 // `function_expression` covers both a function and a
468 // closure. `check_if_func!` is what separates them: its
469 // ancestor walk marks the expression a function when a
470 // binding frame (`var x = …`, `x = …`, `label:`, object
471 // `pair`) is reached before a positional one, and its
472 // `$extra` disjunct additionally marks any expression
473 // carrying its own `identifier` name child. So
474 // `const f = function () {}` and `run(function f () {})`
475 // are functions, while `run(function () {})` is a closure
476 // and must keep falling through to `_`. That
477 // classification is inherited from `is_func`, not
478 // endorsed here — it is also what makes `nom` call the
479 // same node a function or a closure, so cognitive
480 // disagreeing with it would be the larger bug.
481 // `ArrowFunction` stays out because it owns the lambda
482 // channel in the arm below. Listing `FunctionDeclaration`
483 // alone left a method or a bound function expression
484 // inheriting the enclosing conditional nesting (#1159).
485 //
486 // `stops` takes bare kinds, so re-applying that gate to
487 // an *ancestor* would mean changing
488 // `increment_function_depth`'s signature. Leaving it
489 // ungated is deliberate rather than a shortcut: an
490 // anonymous IIFE is a lexical function scope —
491 // `get_space_kind` maps every `function_expression` to
492 // `SpaceKind::Function` — so a `function` declared inside
493 // one really is nested in a function.
494 //
495 // `ArrowFunction` is in the list since #1187, which is
496 // what makes `(function () { function g() {…} })()` and
497 // `(() => { function g() {…} })()` both charge `g` a
498 // depth of 1; the arrow form charged 0 while the kind
499 // was absent. It cannot double-charge, because the sole
500 // caller resets `nesting.lambda` first.
501 //
502 // Both generator kinds are in the arm and in `stops`
503 // since #1186, which moved them from `is_js_closure!`
504 // to `is_js_func!`. The two halves are independent
505 // and had to move together: the arm decides whether
506 // `function* g()` resets its own inherited nesting,
507 // while `stops` decides whether a plain `function`
508 // nested *inside* a generator gets a depth surcharge.
509 // `GeneratorFunction` is gated by `Self::is_func` for
510 // the same reason `FunctionExpression` is — it has an
511 // optional name and covers both a function and a
512 // closure — while `GeneratorFunctionDeclaration`,
513 // like `FunctionDeclaration`, is unconditional.
514 enter_function_boundary(
515 &mut nesting,
516 node,
517 ancestors,
518 &[
519 FunctionDeclaration,
520 MethodDefinition,
521 FunctionExpression,
522 GeneratorFunctionDeclaration,
523 GeneratorFunction,
524 ArrowFunction,
525 ClassStaticBlock,
526 ],
527 );
528 }
529 // A class static block is a function boundary but is
530 // deliberately *not* in `is_func` (#1184), so it needs
531 // its own ungated arm rather than joining the gated one
532 // above — gated, it would never fire. It is in the
533 // `stops` list below for the same reason a
534 // `function_expression` is: a `function` declared inside
535 // a `static { … }` really is nested in one.
536 ClassStaticBlock => {
537 enter_function_boundary(
538 &mut nesting,
539 node,
540 ancestors,
541 &[
542 FunctionDeclaration,
543 MethodDefinition,
544 FunctionExpression,
545 GeneratorFunctionDeclaration,
546 GeneratorFunction,
547 ArrowFunction,
548 ClassStaticBlock,
549 ],
550 );
551 }
552 ArrowFunction => {
553 nesting.lambda += 1;
554 }
555 _ => {}
556 }
557 nesting_map.insert(node.id(), nesting);
558 }
559 };
560}
561
562// Per-language `Cognitive` impls live in sibling modules. The `mod`
563// declarations sit after the local `macro_rules! js_cognitive!` so
564// textual macro scoping reaches the JS-family child files (mirrors
565// `getter.rs`, `metrics::npm`, `metrics::cyclomatic`).
566mod bash;
567mod c;
568mod cpp;
569mod csharp;
570mod elixir;
571mod go;
572mod groovy;
573mod irules;
574mod java;
575mod javascript;
576mod kotlin;
577mod lua;
578mod mozcpp;
579mod mozjs;
580mod objc;
581mod perl;
582mod php;
583mod python;
584mod ruby;
585mod rust;
586mod tcl;
587mod tsx;
588mod typescript;
589#[cfg(test)]
590#[cfg(any(
591 feature = "elixir",
592 feature = "irules",
593 feature = "perl",
594 feature = "php",
595 feature = "ruby"
596))]
597mod word_operator_tests;
598
599// Tcl's `switch` is a generic `command` (no dedicated kind_id, unlike
600// `if`/`while`/`foreach`/`catch`), so the kind-dispatch in the Cognitive
601// and Cyclomatic impls never sees it (issue #467, lesson 19). Both metrics
602// reach it through `tcl_command_name` and then through this pair: the arm
603// list locates the supported form, and `tcl_switch_decision_arms` counts
604// it. Cognitive needs only the former — it charges one structure whatever
605// the arm count is — so the count stays out of its path.
606//
607// Grammar shape (tree-sitter-tcl 0.x), canonical brace-list form:
608//
609// (command name: (simple_word "switch")
610// (word_list <options…> <value> (braced_word (command (simple_word PAT) …) …)))
611//
612// The arm list is the sole `braced_word` argument inside the command's
613// `word_list`, which makes the lookup robust to leading options (`-exact`,
614// `-glob`, `-regexp`, `-nocase`, `--`) and the matched value, all of which
615// precede it and never parse as `braced_word`. The rarer split form
616// (`switch $x a {b} c {d}` — arms as separate `word_list` arguments rather
617// than wrapped in one `braced_word`) produces *several* sibling
618// `braced_word`s, one per arm body, so requiring exactly one distinguishes
619// the brace-list form and excludes the split form, where the last
620// `braced_word` is merely a body rather than the full arm list. The split
621// form is intentionally NOT counted: its body braces are sibling
622// arguments, not nested commands, so there is no reliable arm node to
623// count. Idiomatic Tcl uses the brace-list form, so this scoping
624// under-counts only the uncommon style.
625//
626// **Precondition**: `node` is a `command` whose leading word resolved to
627// `"switch"` — the callers' `tcl_command_name` dispatch establishes that,
628// and re-checking it here would resolve the name a second time on every
629// switch.
630pub(crate) fn tcl_switch_arm_list<'a>(node: &Node<'a>) -> Option<Node<'a>> {
631 let word_list = node
632 .children()
633 .find(|child| child.kind_id() == Tcl::WordList as u16)?;
634 let mut braced_words = word_list
635 .children()
636 .filter(|child| child.kind_id() == Tcl::BracedWord as u16);
637 let arm_list = braced_words.next()?;
638 braced_words.next().is_none().then_some(arm_list)
639}
640
641// The number of *decision* arms of a `switch` — every arm but `default`,
642// which is the fallback and does not contribute a decision point, matching
643// the C-family `default:` convention (lesson 11). Each arm is a nested
644// `command` whose leading word is the pattern.
645//
646// Carries [`tcl_switch_arm_list`]'s precondition, and returns `None` for
647// the same unsupported split form, so callers leave it untouched exactly
648// as they leave a non-switch command.
649pub(crate) fn tcl_switch_decision_arms(node: &Node, code: &[u8]) -> Option<usize> {
650 let decision_arms = tcl_switch_arm_list(node)?
651 .children()
652 .filter(|arm| arm.kind_id() == Tcl::Command as u16)
653 .filter(|arm| {
654 arm.child_by_field_name("name")
655 .and_then(|pat| pat.utf8_text(code))
656 != Some("default")
657 })
658 .count();
659 Some(decision_arms)
660}
661
662// Tcl's `try` has a dedicated kind (unlike `switch`/`for`), but its error
663// handler is a flat run of sibling tokens rather than a wrapper node
664// (issue #1266):
665//
666// (try "try" <body> ["on" "error" (arguments) <body>] [(finally)])
667//
668// The vendored grammar permits at most one `on error` handler and has no
669// `trap` rule at all — Tcl 8.6's multi-handler and `trap` forms degrade to
670// an `ERROR` node, a grammar limitation like the `for` condition in #1264.
671// The scan still tolerates repetition so a grammar bump that adds those
672// forms keeps counting without an edit here.
673//
674// Each handler body is located by structural position, never fixed index
675// (grammar-dispatch §3): it is the child directly following the handler's
676// `arguments` node. That makes the scan pairwise — every child paired with
677// its predecessor, yielding the successor of each `arguments` — rather
678// than a state machine over the `on` / `error` / `arguments` run. The
679// grammar's extras are whitespace-only, so nothing can interpose between
680// the `arguments` node and the body it introduces, and `arguments` appears
681// among a `try`'s direct children only in handler position. Repetition
682// costs nothing here: a grammar bump that admits several handlers (or
683// `trap`) is counted without an edit, because each `arguments` yields its
684// own successor. The `finally` clause is a wrapper kind (`Tcl::Finally`)
685// and is never yielded — unconditional cleanup costs nothing, matching the
686// cross-language `finally` convention (#416).
687//
688// Shared by Cognitive (which also seeds each body's nesting slot) and
689// Cyclomatic (which counts the bodies); both call it from their `Tcl::Try`
690// dispatch arm.
691pub(crate) fn tcl_try_handler_bodies<'a>(node: &Node<'a>) -> impl Iterator<Item = Node<'a>> {
692 node.children()
693 .zip(node.children().skip(1))
694 .filter(|(previous, _)| previous.kind_id() == Tcl::Arguments as u16)
695 .map(|(_, body)| body)
696}
697
698// iRules counterpart to [`tcl_switch_decision_arms`]. Unlike Tcl, the iRules
699// grammar models `switch` as a dedicated node with `switch_arm` children, so
700// the arms are read off the tree directly instead of re-parsing a generic
701// command. Returns the number of non-`default` arms (each a decision point in
702// standard CCN); `None` when `node` is not a `switch`. The `default` arm is the
703// fallback and does not contribute a branch (the Java/C-family wildcard
704// convention — lesson 11, #106).
705pub(crate) fn irules_switch_decision_arms(node: &Node, code: &[u8]) -> Option<usize> {
706 if node.kind_id() != Irules::Switch as u16 {
707 return None;
708 }
709 let decision_arms = node
710 .children()
711 .filter(|arm| arm.kind_id() == Irules::SwitchArm as u16)
712 .filter(|arm| {
713 arm.child_by_field_name("pattern")
714 .and_then(|pat| pat.utf8_text(code))
715 != Some("default")
716 })
717 .count();
718 Some(decision_arms)
719}
720
721implement_metric_trait!(Cognitive, PreprocCode, CcommentCode);
722
723#[cfg(test)]
724#[allow(
725 clippy::float_cmp,
726 clippy::cast_precision_loss,
727 clippy::cast_possible_truncation,
728 clippy::cast_sign_loss,
729 clippy::similar_names,
730 clippy::doc_markdown,
731 clippy::needless_raw_string_hashes,
732 clippy::too_many_lines
733)]
734mod tests {
735 use crate::test_support::{
736 check_func_space_only_shim, check_metrics_only_shim, child_space, function_space,
737 };
738
739 use super::*;
740
741 // Cognitive's dependency closure adds Nom, the divisor behind
742 // `cognitive_average`.
743 check_metrics_only_shim!(check_metrics, Cognitive);
744 check_func_space_only_shim!(check_func_space, Cognitive);
745 // The Python-comprehension tests (#417/#421) assert the cyclomatic
746 // count alongside the cognitive one, to show where the two metrics
747 // agree and where nesting makes them diverge. They are the only
748 // cross-metric assertions here, so they get their own shim rather
749 // than widening the module-wide selection.
750 check_metrics_only_shim!(check_cognitive_and_cyclomatic, Cognitive, Cyclomatic);
751
752 /// The walker must hand `is_else_if` the node's own parent at every
753 /// AST depth.
754 ///
755 /// A bare `{ … }` block carries no cognitive weight in either
756 /// language, so burying an `if / else if / else if` chain under more
757 /// of them cannot move the score — unless the chain of ancestors the
758 /// walker propagates (#1084) drifts. Then the inner `if`s stop
759 /// reading as continuations of the branch above, each pays a fresh
760 /// nesting penalty, and the total climbs with depth.
761 ///
762 /// Both `is_else_if` shapes are covered: C resolves the enclosing
763 /// `else_clause` through the parent, Java through the preceding
764 /// `else` token, which the chain answers by scanning the parent's
765 /// children.
766 #[cfg(all(feature = "c", feature = "java"))]
767 #[test]
768 fn else_if_is_recognised_at_every_nesting_depth() {
769 use crate::test_support::metrics_verbatim;
770
771 // 1 for the `if`, plus 1 for each `else if` as a branch
772 // extension. No nesting penalty: an `else if` continues the
773 // chain rather than nesting inside it.
774 #[cfg(any(feature = "c", feature = "java"))]
775 const CHAIN_COGNITIVE: u64 = 3;
776
777 let chain = "if (a) { } else if (b) { } else if (c) { }";
778 for depth in 0..=6 {
779 let (open, close) = ("{ ".repeat(depth), " }".repeat(depth));
780 for (lang, source) in [
781 (LANG::C, format!("void f() {{ {open}{chain}{close} }}\n")),
782 (
783 LANG::Java,
784 format!("class A {{ void m() {{ {open}{chain}{close} }} }}\n"),
785 ),
786 ] {
787 let metrics = metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default());
788 assert_eq!(
789 metrics.cognitive.cognitive_sum(),
790 CHAIN_COGNITIVE,
791 "{lang:?}: `else if` chain under {depth} plain blocks scored \
792 {} instead of {CHAIN_COGNITIVE}",
793 metrics.cognitive.cognitive_sum(),
794 );
795 }
796 }
797 }
798
799 /// A `Stats::default()` that never sees an
800 /// observation must not leak the `usize::MAX` sentinel for
801 /// `structural_min`. The getter collapses the sentinel to `0.0`
802 /// so JSON never emits `1.8446744e19`.
803 #[test]
804 fn cognitive_empty_file_min_is_zero() {
805 let stats = Stats::default();
806 assert_eq!(stats.cognitive_min(), 0);
807 }
808
809 #[cfg(feature = "python")]
810 #[test]
811 fn python_no_cognitive() {
812 check_metrics::<PythonParser>("a = 42", "foo.py", |metric| {
813 insta::assert_json_snapshot!(
814 metric.cognitive,
815 @r#"
816 {
817 "sum": 0,
818 "value": 0,
819 "average": 0.0,
820 "min": 0,
821 "max": 0
822 }
823 "#
824 );
825 });
826 }
827
828 #[cfg(feature = "rust")]
829 #[test]
830 fn rust_no_cognitive() {
831 check_metrics::<RustParser>("let a = 42;", "foo.rs", |metric| {
832 insta::assert_json_snapshot!(
833 metric.cognitive,
834 @r#"
835 {
836 "sum": 0,
837 "value": 0,
838 "average": 0.0,
839 "min": 0,
840 "max": 0
841 }
842 "#
843 );
844 });
845 }
846
847 #[cfg(feature = "c")]
848 #[test]
849 fn c_no_cognitive() {
850 check_metrics::<CParser>("int a = 42;", "foo.c", |metric| {
851 insta::assert_json_snapshot!(
852 metric.cognitive,
853 @r#"
854 {
855 "sum": 0,
856 "value": 0,
857 "average": 0.0,
858 "min": 0,
859 "max": 0
860 }
861 "#
862 );
863 });
864 }
865
866 #[cfg(feature = "mozjs")]
867 #[test]
868 fn mozjs_no_cognitive() {
869 check_metrics::<MozjsParser>("var a = 42;", "foo.js", |metric| {
870 insta::assert_json_snapshot!(
871 metric.cognitive,
872 @r#"
873 {
874 "sum": 0,
875 "value": 0,
876 "average": 0.0,
877 "min": 0,
878 "max": 0
879 }
880 "#
881 );
882 });
883 }
884
885 #[cfg(feature = "javascript")]
886 #[test]
887 fn javascript_no_cognitive() {
888 check_metrics::<JavascriptParser>("var a = 42;", "foo.js", |metric| {
889 insta::assert_json_snapshot!(
890 metric.cognitive,
891 @r#"
892 {
893 "sum": 0,
894 "value": 0,
895 "average": 0.0,
896 "min": 0,
897 "max": 0
898 }
899 "#
900 );
901 });
902 }
903
904 #[cfg(feature = "python")]
905 #[test]
906 fn python_simple_function() {
907 check_metrics::<PythonParser>(
908 "def f(a, b):
909 if a and b: # +2 (+1 and)
910 return 1
911 if c and d: # +2 (+1 and)
912 return 1",
913 "foo.py",
914 |metric| {
915 insta::assert_json_snapshot!(
916 metric.cognitive,
917 @r#"
918 {
919 "sum": 4,
920 "value": 0,
921 "average": 4.0,
922 "min": 0,
923 "max": 4
924 }
925 "#
926 );
927 },
928 );
929 }
930
931 /// Python `match`/`case` (PEP 634, 3.10+) opens cognitive nesting
932 /// the same way Rust's `match_expression` and the C-family
933 /// `switch_statement` do. A 2-arm match with one explicit arm
934 /// plus a wildcard contributes one cognitive decision point.
935 /// Regression test for #212.
936 #[cfg(feature = "python")]
937 #[test]
938 fn python_match_two_arm_wildcard() {
939 check_metrics::<PythonParser>(
940 "def f(x):
941 match x:
942 case 1:
943 return 'one'
944 case _:
945 return 'other'
946",
947 "foo.py",
948 |metric| {
949 // The `match_statement` contributes one decision point;
950 // case arms inside add no extra nesting (mirrors Rust /
951 // C-family switch). cognitive_max = 1.
952 insta::assert_json_snapshot!(
953 metric.cognitive,
954 @r#"
955 {
956 "sum": 1,
957 "value": 0,
958 "average": 1.0,
959 "min": 0,
960 "max": 1
961 }
962 "#
963 );
964 },
965 );
966 }
967
968 #[cfg(feature = "python")]
969 #[test]
970 fn python_expression_statement() {
971 // Boolean expressions containing `And` and `Or` operators were not
972 // considered in assignments
973 check_metrics::<PythonParser>(
974 "def f(a, b):
975 c = True and True",
976 "foo.py",
977 |metric| {
978 insta::assert_json_snapshot!(
979 metric.cognitive,
980 @r#"
981 {
982 "sum": 1,
983 "value": 0,
984 "average": 1.0,
985 "min": 0,
986 "max": 1
987 }
988 "#
989 );
990 },
991 );
992 }
993
994 #[cfg(feature = "python")]
995 #[test]
996 fn python_tuple() {
997 // Boolean expressions containing `And` and `Or` operators were not
998 // considered inside tuples
999 check_metrics::<PythonParser>(
1000 "def f(a, b):
1001 return \"%s%s\" % (a and \"Get\" or \"Set\", b)",
1002 "foo.py",
1003 |metric| {
1004 insta::assert_json_snapshot!(
1005 metric.cognitive,
1006 @r#"
1007 {
1008 "sum": 2,
1009 "value": 0,
1010 "average": 2.0,
1011 "min": 0,
1012 "max": 2
1013 }
1014 "#
1015 );
1016 },
1017 );
1018 }
1019
1020 #[cfg(feature = "python")]
1021 #[test]
1022 fn python_elif_function() {
1023 // Boolean expressions containing `And` and `Or` operators were not
1024 // considered in `elif` statements
1025 check_metrics::<PythonParser>(
1026 "def f(a, b):
1027 if a and b: # +2 (+1 and)
1028 return 1
1029 elif c and d: # +2 (+1 and)
1030 return 1",
1031 "foo.py",
1032 |metric| {
1033 insta::assert_json_snapshot!(
1034 metric.cognitive,
1035 @r#"
1036 {
1037 "sum": 4,
1038 "value": 0,
1039 "average": 4.0,
1040 "min": 0,
1041 "max": 4
1042 }
1043 "#
1044 );
1045 },
1046 );
1047 }
1048
1049 #[cfg(feature = "python")]
1050 #[test]
1051 fn python_more_elifs_function() {
1052 // Boolean expressions containing `And` and `Or` operators were not
1053 // considered when there were more `elif` statements
1054 check_metrics::<PythonParser>(
1055 "def f(a, b):
1056 if a and b: # +2 (+1 and)
1057 return 1
1058 elif c and d: # +2 (+1 and)
1059 return 1
1060 elif e and f: # +2 (+1 and)
1061 return 1",
1062 "foo.py",
1063 |metric| {
1064 insta::assert_json_snapshot!(
1065 metric.cognitive,
1066 @r#"
1067 {
1068 "sum": 6,
1069 "value": 0,
1070 "average": 6.0,
1071 "min": 0,
1072 "max": 6
1073 }
1074 "#
1075 );
1076 },
1077 );
1078 }
1079
1080 #[cfg(feature = "python")]
1081 #[test]
1082 fn python_if_elif_elif_else_chain() {
1083 // Regression for #274: `if/elif/elif/else` must score as a flat
1084 // branch chain (each continuation contributes +1 with no extra
1085 // nesting). `ElifClause` is a dedicated node handled directly
1086 // by the cognitive dispatch as a branch extension, and the
1087 // generic `count_specific_ancestors` nesting walk does not
1088 // include `ElifClause` in its kind sets, so no ancestor-side
1089 // suppression via `is_else_if` is required.
1090 // expected: outer if +1, elif +1, elif +1, else +1 = 4.
1091 check_metrics::<PythonParser>(
1092 "def f(a, b, c, d):
1093 if a:
1094 return 1
1095 elif b:
1096 return 2
1097 elif c:
1098 return 3
1099 else:
1100 return 4",
1101 "foo.py",
1102 |metric| {
1103 assert_eq!(metric.cognitive.cognitive_sum(), 4);
1104 insta::assert_json_snapshot!(
1105 metric.cognitive,
1106 @r#"
1107 {
1108 "sum": 4,
1109 "value": 0,
1110 "average": 4.0,
1111 "min": 0,
1112 "max": 4
1113 }
1114 "#
1115 );
1116 },
1117 );
1118 }
1119
1120 #[cfg(feature = "python")]
1121 #[test]
1122 fn python_else_if_chain_matches_elif() {
1123 // Regression for #276: `else: if x:` (no `elif`) is semantically
1124 // an else-if chain and must score the same as the `elif`
1125 // equivalent. Before the fix, the inner `if_statement` was
1126 // double-counted (nesting +2 instead of +1), inflating the
1127 // cognitive score linearly with chain length.
1128 // expected: outer if +1, boolean `and` +1, else_clause +1,
1129 // inner if suppressed by is_else_if, inner boolean `and` +1
1130 // = 4 — matching the `elif` form above (python_elif_function).
1131 check_metrics::<PythonParser>(
1132 "def f(a, b, c, d):
1133 if a and b:
1134 return 1
1135 else:
1136 if c and d:
1137 return 1",
1138 "foo.py",
1139 |metric| {
1140 assert_eq!(metric.cognitive.cognitive_sum(), 4);
1141 insta::assert_json_snapshot!(
1142 metric.cognitive,
1143 @r#"
1144 {
1145 "sum": 4,
1146 "value": 0,
1147 "average": 4.0,
1148 "min": 0,
1149 "max": 4
1150 }
1151 "#
1152 );
1153 },
1154 );
1155 }
1156
1157 #[cfg(feature = "python")]
1158 #[test]
1159 fn python_try_except_finally_finally_is_free() {
1160 // Regression for #416: a `finally` clause is structured cleanup that
1161 // always runs and must add 0 per the SonarSource Cognitive Complexity
1162 // spec. try/except/finally must score the same as try/except.
1163 // expected: except +1, finally +0 = 1.
1164 check_metrics::<PythonParser>(
1165 "def f():
1166 try:
1167 x = risky()
1168 except ValueError:
1169 x = 1
1170 finally:
1171 cleanup()
1172 return x",
1173 "foo.py",
1174 |metric| {
1175 assert_eq!(metric.cognitive.cognitive_sum(), 1);
1176 insta::assert_json_snapshot!(
1177 metric.cognitive,
1178 @r#"
1179 {
1180 "sum": 1,
1181 "value": 0,
1182 "average": 1.0,
1183 "min": 0,
1184 "max": 1
1185 }
1186 "#
1187 );
1188 },
1189 );
1190 }
1191
1192 #[cfg(feature = "python")]
1193 #[test]
1194 fn python_try_except_matches_try_except_finally() {
1195 // Companion to #416: try/except (no finally) scores the same as the
1196 // try/except/finally form above, proving `finally` is free.
1197 // expected: except +1 = 1.
1198 check_metrics::<PythonParser>(
1199 "def f():
1200 try:
1201 x = risky()
1202 except ValueError:
1203 x = 1
1204 return x",
1205 "foo.py",
1206 |metric| {
1207 assert_eq!(metric.cognitive.cognitive_sum(), 1);
1208 insta::assert_json_snapshot!(
1209 metric.cognitive,
1210 @r#"
1211 {
1212 "sum": 1,
1213 "value": 0,
1214 "average": 1.0,
1215 "min": 0,
1216 "max": 1
1217 }
1218 "#
1219 );
1220 },
1221 );
1222 }
1223
1224 #[cfg(feature = "python")]
1225 #[test]
1226 fn python_comprehension_matches_explicit_loop() {
1227 // Regression for #417: a list comprehension's `for`/`if` clauses must
1228 // carry the same cognitive load as the explicit loop+condition they
1229 // desugar to. `[x for x in xs if x > 0]` was scoring 0 while the
1230 // equivalent explicit `for`/`if` scored 3.
1231 // expected: for_in_clause +1 (nesting 0), if_clause +2 (1 base +
1232 // 1 nesting under the for) = 3 — equal to the explicit form below.
1233 check_cognitive_and_cyclomatic::<PythonParser>(
1234 "def f(xs):
1235 return [x for x in xs if x > 0]",
1236 "foo.py",
1237 |metric| {
1238 // cyclomatic 4 = unit base 1 + for 1 + if 1 + function base 1.
1239 assert_eq!(metric.cognitive.cognitive_sum(), 3);
1240 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
1241 },
1242 );
1243 check_cognitive_and_cyclomatic::<PythonParser>(
1244 "def g(xs):
1245 out = []
1246 for x in xs:
1247 if x > 0:
1248 out.append(x)
1249 return out",
1250 "foo.py",
1251 |metric| {
1252 // The explicit loop+if form the comprehension above desugars
1253 // to: for +1, nested if +2 = 3 (cognitive), matching f.
1254 // cyclomatic 4 matches f as well, confirming agreement.
1255 assert_eq!(metric.cognitive.cognitive_sum(), 3);
1256 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
1257 },
1258 );
1259 }
1260
1261 #[cfg(feature = "python")]
1262 #[test]
1263 fn python_comprehension_plain_no_filter() {
1264 // A comprehension with no `if` filter scores just the loop.
1265 // expected: for_in_clause +1 = 1.
1266 check_cognitive_and_cyclomatic::<PythonParser>(
1267 "def f(xs):
1268 return [x for x in xs]",
1269 "foo.py",
1270 |metric| {
1271 assert_eq!(metric.cognitive.cognitive_sum(), 1);
1272 // cyclomatic 3 = unit base 1 + for 1 + function base 1.
1273 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
1274 },
1275 );
1276 }
1277
1278 #[cfg(feature = "python")]
1279 #[test]
1280 fn python_comprehension_nested_for() {
1281 // Two `for` clauses are nested loops: the second nests under the
1282 // first, mirroring explicit nested `for` statements.
1283 // expected: for #1 +1 (nesting 0), for #2 +2 (1 base + 1 nesting) = 3.
1284 check_cognitive_and_cyclomatic::<PythonParser>(
1285 "def f(xs, ys):
1286 return [a for a in xs for b in ys]",
1287 "foo.py",
1288 |metric| {
1289 assert_eq!(metric.cognitive.cognitive_sum(), 3);
1290 // cyclomatic 4 = unit base 1 + for 1 + for 1 + function base 1.
1291 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
1292 },
1293 );
1294 }
1295
1296 #[cfg(feature = "python")]
1297 #[test]
1298 fn python_comprehension_multiple_filters() {
1299 // Each `if` filter is an independent condition nested under the for.
1300 // Cognitive penalizes the nesting, so it exceeds cyclomatic here; the
1301 // two metrics legitimately diverge once filters multiply.
1302 // expected cognitive: for +1, if #1 +2, if #2 +2 = 5.
1303 check_cognitive_and_cyclomatic::<PythonParser>(
1304 "def f(xs):
1305 return [x for x in xs if a if b]",
1306 "foo.py",
1307 |metric| {
1308 assert_eq!(metric.cognitive.cognitive_sum(), 5);
1309 // cyclomatic 5 = unit base 1 + for 1 + if 1 + if 1 + fn base 1.
1310 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
1311 },
1312 );
1313 }
1314
1315 #[cfg(feature = "python")]
1316 #[test]
1317 fn python_comprehension_variants_consistent() {
1318 // dict / set / generator comprehensions reuse the same for_in_clause /
1319 // if_clause node kinds as the list form, so all must score identically
1320 // to `[x for x in xs if x > 0]` (cognitive 3).
1321 // expected: for +1, if +2 = 3 for each variant.
1322 for body in [
1323 "{x: y for x, y in xs if x > 0}",
1324 "{x for x in xs if x > 0}",
1325 "(x for x in xs if x > 0)",
1326 ] {
1327 check_cognitive_and_cyclomatic::<PythonParser>(
1328 &format!("def f(xs):\n return {body}"),
1329 "foo.py",
1330 |metric| {
1331 assert_eq!(metric.cognitive.cognitive_sum(), 3);
1332 // cyclomatic 4 = unit base 1 + for 1 + if 1 + fn base 1,
1333 // identical to the list form, for every variant.
1334 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
1335 },
1336 );
1337 }
1338 }
1339
1340 #[cfg(feature = "python")]
1341 #[test]
1342 fn python_comprehension_nested_in_element() {
1343 // Regression for #421: a comprehension in another comprehension's
1344 // element position must carry the full nesting of the outer loop+
1345 // filter, not the shallow depth the #417 sibling write-back left it
1346 // with (it under-counted at 6). The element is traversed before the
1347 // outer clauses, so the depth is established on the comprehension node
1348 // itself, independent of sibling traversal order.
1349 // expected cognitive: outer for +1 (nesting 0), outer if +2
1350 // (nesting 1), inner for +3 (nesting 2), inner if +4 (nesting 3) = 10.
1351 check_metrics::<PythonParser>(
1352 "def f(xs):
1353 return [[y for y in x if y] for x in xs if x]",
1354 "foo.py",
1355 |metric| {
1356 assert_eq!(metric.cognitive.cognitive_sum(), 10);
1357 },
1358 );
1359 // The explicit doubly-nested loop+if form it desugars to: for +1,
1360 // if +2, for +3, if +4 = 10, matching the comprehension above.
1361 check_metrics::<PythonParser>(
1362 "def g(xs):
1363 out = []
1364 for x in xs:
1365 if x:
1366 for y in x:
1367 if y:
1368 out.append(y)
1369 return out",
1370 "foo.py",
1371 |metric| {
1372 assert_eq!(metric.cognitive.cognitive_sum(), 10);
1373 },
1374 );
1375 }
1376
1377 #[cfg(feature = "python")]
1378 #[test]
1379 fn python_comprehension_three_levels_nested() {
1380 // Three comprehensions nested through each other's element positions
1381 // must equal their explicit triply-nested loop+if form at every depth.
1382 // expected cognitive: for/if pairs at nesting 0..5 =
1383 // 1+2+3+4+5+6 = 21.
1384 check_metrics::<PythonParser>(
1385 "def f(xss):
1386 return [[[z for z in y if z] for y in x if y] for x in xss if x]",
1387 "foo.py",
1388 |metric| {
1389 assert_eq!(metric.cognitive.cognitive_sum(), 21);
1390 },
1391 );
1392 check_metrics::<PythonParser>(
1393 "def g(xss):
1394 out = []
1395 for x in xss:
1396 if x:
1397 for y in x:
1398 if y:
1399 for z in y:
1400 if z:
1401 out.append(z)
1402 return out",
1403 "foo.py",
1404 |metric| {
1405 assert_eq!(metric.cognitive.cognitive_sum(), 21);
1406 },
1407 );
1408 }
1409
1410 #[cfg(feature = "python")]
1411 #[test]
1412 fn python_generator_in_comprehension_element() {
1413 // #421 edge case: a generator passed to a call (`sum(...)`) in a
1414 // comprehension's element still inherits the outer loop+filter depth
1415 // through the intervening call/argument_list nodes.
1416 // expected cognitive: outer for +1, outer if +2, inner for +3,
1417 // inner if +4 = 10.
1418 check_metrics::<PythonParser>(
1419 "def f(xs):
1420 return [sum(y for y in x if y) for x in xs if x]",
1421 "foo.py",
1422 |metric| {
1423 assert_eq!(metric.cognitive.cognitive_sum(), 10);
1424 },
1425 );
1426 check_metrics::<PythonParser>(
1427 "def g(xs):
1428 out = []
1429 for x in xs:
1430 if x:
1431 out.append(sum(y for y in x if y))
1432 return out",
1433 "foo.py",
1434 |metric| {
1435 assert_eq!(metric.cognitive.cognitive_sum(), 10);
1436 },
1437 );
1438 }
1439
1440 #[cfg(feature = "python")]
1441 #[test]
1442 fn python_try_finally_no_except_is_free() {
1443 // #416: try/finally with no except clause scores 0 — neither the try
1444 // body nor the finally cleanup carries any cognitive cost on its own.
1445 // expected: 0.
1446 check_metrics::<PythonParser>(
1447 "def f():
1448 try:
1449 x = risky()
1450 finally:
1451 cleanup()
1452 return x",
1453 "foo.py",
1454 |metric| {
1455 assert_eq!(metric.cognitive.cognitive_sum(), 0);
1456 insta::assert_json_snapshot!(
1457 metric.cognitive,
1458 @r#"
1459 {
1460 "sum": 0,
1461 "value": 0,
1462 "average": 0.0,
1463 "min": 0,
1464 "max": 0
1465 }
1466 "#
1467 );
1468 },
1469 );
1470 }
1471
1472 #[cfg(feature = "python")]
1473 #[test]
1474 fn python_constructs_inside_finally_still_count() {
1475 // #416 guard: making `finally` free must not make its body invisible.
1476 // The finally clause itself carries no nesting increment (it never
1477 // called `increase_nesting`), so an `if` directly inside it is at
1478 // nesting depth 0 and contributes its +1 base cost.
1479 // expected: if inside finally = +1.
1480 check_metrics::<PythonParser>(
1481 "def f():
1482 try:
1483 x = risky()
1484 finally:
1485 if x:
1486 cleanup()",
1487 "foo.py",
1488 |metric| {
1489 assert_eq!(metric.cognitive.cognitive_sum(), 1);
1490 insta::assert_json_snapshot!(
1491 metric.cognitive,
1492 @r#"
1493 {
1494 "sum": 1,
1495 "value": 0,
1496 "average": 1.0,
1497 "min": 0,
1498 "max": 1
1499 }
1500 "#
1501 );
1502 },
1503 );
1504 }
1505
1506 #[cfg(feature = "rust")]
1507 #[test]
1508 fn rust_simple_function() {
1509 check_metrics::<RustParser>(
1510 "fn f() {
1511 if a && b { // +2 (+1 &&)
1512 println!(\"test\");
1513 }
1514 if c && d { // +2 (+1 &&)
1515 println!(\"test\");
1516 }
1517 }",
1518 "foo.rs",
1519 |metric| {
1520 insta::assert_json_snapshot!(
1521 metric.cognitive,
1522 @r#"
1523 {
1524 "sum": 4,
1525 "value": 0,
1526 "average": 4.0,
1527 "min": 0,
1528 "max": 4
1529 }
1530 "#
1531 );
1532 },
1533 );
1534 }
1535
1536 #[cfg(feature = "c")]
1537 #[test]
1538 fn c_simple_function() {
1539 check_metrics::<CParser>(
1540 "void f() {
1541 if (a && b) { // +2 (+1 &&)
1542 printf(\"test\");
1543 }
1544 if (c && d) { // +2 (+1 &&)
1545 printf(\"test\");
1546 }
1547 }",
1548 "foo.c",
1549 |metric| {
1550 insta::assert_json_snapshot!(
1551 metric.cognitive,
1552 @r#"
1553 {
1554 "sum": 4,
1555 "value": 0,
1556 "average": 4.0,
1557 "min": 0,
1558 "max": 4
1559 }
1560 "#
1561 );
1562 },
1563 );
1564 }
1565
1566 #[cfg(feature = "mozjs")]
1567 #[test]
1568 fn mozjs_simple_function() {
1569 check_metrics::<MozjsParser>(
1570 "function f() {
1571 if (a && b) { // +2 (+1 &&)
1572 window.print(\"test\");
1573 }
1574 if (c && d) { // +2 (+1 &&)
1575 window.print(\"test\");
1576 }
1577 }",
1578 "foo.js",
1579 |metric| {
1580 insta::assert_json_snapshot!(
1581 metric.cognitive,
1582 @r#"
1583 {
1584 "sum": 4,
1585 "value": 0,
1586 "average": 4.0,
1587 "min": 0,
1588 "max": 4
1589 }
1590 "#
1591 );
1592 },
1593 );
1594 }
1595
1596 #[cfg(feature = "javascript")]
1597 #[test]
1598 fn javascript_simple_function() {
1599 check_metrics::<JavascriptParser>(
1600 "function f() {
1601 if (a && b) { // +2 (+1 &&)
1602 console.log(\"test\");
1603 }
1604 if (c || d) { // +2 (+1 ||)
1605 console.log(\"test\");
1606 }
1607 }",
1608 "foo.js",
1609 |metric| {
1610 insta::assert_json_snapshot!(
1611 metric.cognitive,
1612 @r#"
1613 {
1614 "sum": 4,
1615 "value": 0,
1616 "average": 4.0,
1617 "min": 0,
1618 "max": 4
1619 }
1620 "#
1621 );
1622 },
1623 );
1624 }
1625
1626 #[cfg(feature = "python")]
1627 #[test]
1628 fn python_sequence_same_booleans() {
1629 check_metrics::<PythonParser>(
1630 "def f(a, b):
1631 if a and b and True: # +2 (+1 sequence of and)
1632 return 1",
1633 "foo.py",
1634 |metric| {
1635 insta::assert_json_snapshot!(
1636 metric.cognitive,
1637 @r#"
1638 {
1639 "sum": 2,
1640 "value": 0,
1641 "average": 2.0,
1642 "min": 0,
1643 "max": 2
1644 }
1645 "#
1646 );
1647 },
1648 );
1649 }
1650
1651 #[cfg(feature = "rust")]
1652 #[test]
1653 fn rust_sequence_same_booleans() {
1654 check_metrics::<RustParser>(
1655 "fn f() {
1656 if a && b && true { // +2 (+1 sequence of &&)
1657 println!(\"test\");
1658 }
1659 }",
1660 "foo.rs",
1661 |metric| {
1662 insta::assert_json_snapshot!(
1663 metric.cognitive,
1664 @r#"
1665 {
1666 "sum": 2,
1667 "value": 0,
1668 "average": 2.0,
1669 "min": 0,
1670 "max": 2
1671 }
1672 "#
1673 );
1674 },
1675 );
1676
1677 check_metrics::<RustParser>(
1678 "fn f() {
1679 if a || b || c || d { // +2 (+1 sequence of ||)
1680 println!(\"test\");
1681 }
1682 }",
1683 "foo.rs",
1684 |metric| {
1685 insta::assert_json_snapshot!(
1686 metric.cognitive,
1687 @r#"
1688 {
1689 "sum": 2,
1690 "value": 0,
1691 "average": 2.0,
1692 "min": 0,
1693 "max": 2
1694 }
1695 "#
1696 );
1697 },
1698 );
1699 }
1700
1701 // Regression for issue #396: in Rust 2024 let-chains, the `&&`
1702 // tokens are direct children of the `_let_chain` / `let_chain`
1703 // node rather than a `BinaryExpression`. Before #396 these
1704 // tokens were invisible to the cognitive boolean-sequence
1705 // counter (cyclomatic already counted them via AMPAMP).
1706 #[cfg(feature = "rust")]
1707 #[test]
1708 fn rust_let_chain_sequence_booleans() {
1709 // expected: +1 for the `if`, +1 for the chain of two `&&`
1710 // tokens (sequence of same operator collapses to one).
1711 // Equivalent shape to `if a && b && true { ... }` above,
1712 // which scores 2.0.
1713 check_metrics::<RustParser>(
1714 "fn f(a: Option<i32>, b: Option<i32>) {
1715 if let Some(x) = a && let Some(y) = b && x > y { // +2 (+1 sequence of &&)
1716 println!(\"both\");
1717 }
1718 }",
1719 "foo.rs",
1720 |metric| {
1721 assert_eq!(metric.cognitive.cognitive_sum() as u32, 2);
1722 insta::assert_json_snapshot!(
1723 metric.cognitive,
1724 @r#"
1725 {
1726 "sum": 2,
1727 "value": 0,
1728 "average": 2.0,
1729 "min": 0,
1730 "max": 2
1731 }
1732 "#
1733 );
1734 },
1735 );
1736 }
1737
1738 #[cfg(feature = "rust")]
1739 #[test]
1740 fn rust_let_chain_vs_nested_if_let() {
1741 // Companion to `rust_let_chain_sequence_booleans`. The nested
1742 // `if let` form has no `&&` and so is unaffected by the #396
1743 // LetChain dispatch; this test pins that the pre-existing
1744 // nesting scoring (+1 outer `if`, +2 nested `if` at nesting=1)
1745 // still yields 3 and that the LetChain arm did not alter it.
1746 check_metrics::<RustParser>(
1747 "fn f(a: Option<i32>, b: Option<i32>) {
1748 if let Some(x) = a { // +1
1749 if let Some(y) = b { // +2 (nesting=1)
1750 println!(\"{} {}\", x, y);
1751 }
1752 }
1753 }",
1754 "foo.rs",
1755 |metric| {
1756 assert_eq!(metric.cognitive.cognitive_sum() as u32, 3);
1757 insta::assert_json_snapshot!(
1758 metric.cognitive,
1759 @r#"
1760 {
1761 "sum": 3,
1762 "value": 0,
1763 "average": 3.0,
1764 "min": 0,
1765 "max": 3
1766 }
1767 "#
1768 );
1769 },
1770 );
1771 }
1772
1773 #[cfg(all(feature = "c", feature = "cpp"))]
1774 #[test]
1775 fn c_sequence_same_booleans() {
1776 check_metrics::<CParser>(
1777 "void f() {
1778 if (a && b && 1 == 1) { // +2 (+1 sequence of &&)
1779 printf(\"test\");
1780 }
1781 }",
1782 "foo.c",
1783 |metric| {
1784 insta::assert_json_snapshot!(
1785 metric.cognitive,
1786 @r#"
1787 {
1788 "sum": 2,
1789 "value": 0,
1790 "average": 2.0,
1791 "min": 0,
1792 "max": 2
1793 }
1794 "#
1795 );
1796 },
1797 );
1798
1799 check_metrics::<CppParser>(
1800 "void f() {
1801 if (a || b || c || d) { // +2 (+1 sequence of ||)
1802 printf(\"test\");
1803 }
1804 }",
1805 "foo.c",
1806 |metric| {
1807 insta::assert_json_snapshot!(
1808 metric.cognitive,
1809 @r#"
1810 {
1811 "sum": 2,
1812 "value": 0,
1813 "average": 2.0,
1814 "min": 0,
1815 "max": 2
1816 }
1817 "#
1818 );
1819 },
1820 );
1821 }
1822
1823 #[cfg(feature = "mozjs")]
1824 #[test]
1825 fn mozjs_sequence_same_booleans() {
1826 check_metrics::<MozjsParser>(
1827 "function f() {
1828 if (a && b && 1 == 1) { // +2 (+1 sequence of &&)
1829 window.print(\"test\");
1830 }
1831 }",
1832 "foo.js",
1833 |metric| {
1834 insta::assert_json_snapshot!(
1835 metric.cognitive,
1836 @r#"
1837 {
1838 "sum": 2,
1839 "value": 0,
1840 "average": 2.0,
1841 "min": 0,
1842 "max": 2
1843 }
1844 "#
1845 );
1846 },
1847 );
1848
1849 check_metrics::<MozjsParser>(
1850 "function f() {
1851 if (a || b || c || d) { // +2 (+1 sequence of ||)
1852 window.print(\"test\");
1853 }
1854 }",
1855 "foo.js",
1856 |metric| {
1857 insta::assert_json_snapshot!(
1858 metric.cognitive,
1859 @r#"
1860 {
1861 "sum": 2,
1862 "value": 0,
1863 "average": 2.0,
1864 "min": 0,
1865 "max": 2
1866 }
1867 "#
1868 );
1869 },
1870 );
1871 }
1872
1873 #[cfg(feature = "rust")]
1874 #[test]
1875 fn rust_not_booleans() {
1876 check_metrics::<RustParser>(
1877 "fn f() {
1878 if !a && !b { // +2 (+1 &&)
1879 println!(\"test\");
1880 }
1881 }",
1882 "foo.rs",
1883 |metric| {
1884 insta::assert_json_snapshot!(
1885 metric.cognitive,
1886 @r#"
1887 {
1888 "sum": 2,
1889 "value": 0,
1890 "average": 2.0,
1891 "min": 0,
1892 "max": 2
1893 }
1894 "#
1895 );
1896 },
1897 );
1898
1899 check_metrics::<RustParser>(
1900 // `!` does not break boolean sequences (issue #392): the
1901 // outer and inner `&&`s are folded into a single sequence
1902 // because pre-order visits the outer BinaryExpression first
1903 // (recording `&&` at its end_byte) and the inner `&&` lies
1904 // within that span. The `!` arm was dead anyway — it fired
1905 // after both BinaryExpressions had already been counted.
1906 "fn f() {
1907 if a && !(b && c) { // +2 (+1 if, +1 outer &&; inner && continues)
1908 println!(\"test\");
1909 }
1910 }",
1911 "foo.rs",
1912 |metric| {
1913 insta::assert_json_snapshot!(
1914 metric.cognitive,
1915 @r#"
1916 {
1917 "sum": 2,
1918 "value": 0,
1919 "average": 2.0,
1920 "min": 0,
1921 "max": 2
1922 }
1923 "#
1924 );
1925 },
1926 );
1927
1928 check_metrics::<RustParser>(
1929 "fn f() {
1930 if !(a || b) && !(c || d) { // +4 (+1 ||, +1 &&, +1 ||)
1931 println!(\"test\");
1932 }
1933 }",
1934 "foo.rs",
1935 |metric| {
1936 insta::assert_json_snapshot!(
1937 metric.cognitive,
1938 @r#"
1939 {
1940 "sum": 4,
1941 "value": 0,
1942 "average": 4.0,
1943 "min": 0,
1944 "max": 4
1945 }
1946 "#
1947 );
1948 },
1949 );
1950 }
1951
1952 #[cfg(feature = "rust")]
1953 #[test]
1954 fn rust_not_does_not_affect_boolean_sequence_392() {
1955 // Regression test for issue #392: `!` does not affect cognitive
1956 // scoring for a same-operator boolean sequence. `!a && !b && !c`
1957 // must score identically to `a && b && c` — both are a single
1958 // `&&` chain under SonarSource's rule B1 (only operator switches
1959 // start a new sequence). The previously dead `UnaryExpression`
1960 // arm could not have affected this case either way (pre-order
1961 // visits the BinaryExpressions before the UnaryExpressions), so
1962 // this asserts the new and old behaviour agree where it matters.
1963 // if(+1) + && sequence(+1) = 2; the two trailing `&&`s are
1964 // continuations because all three share the outer pre-order
1965 // parent's end_byte.
1966 check_metrics::<RustParser>(
1967 "fn f() {
1968 if !a && !b && !c {
1969 println!(\"test\");
1970 }
1971 }",
1972 "foo.rs",
1973 |metric| {
1974 assert_eq!(metric.cognitive.cognitive_sum(), 2);
1975 insta::assert_json_snapshot!(
1976 metric.cognitive,
1977 @r#"
1978 {
1979 "sum": 2,
1980 "value": 0,
1981 "average": 2.0,
1982 "min": 0,
1983 "max": 2
1984 }
1985 "#
1986 );
1987 },
1988 );
1989 check_metrics::<RustParser>(
1990 "fn f() {
1991 if a && b && c {
1992 println!(\"test\");
1993 }
1994 }",
1995 "foo.rs",
1996 |metric| {
1997 // Same sum as the negated form above: `!` is not a
1998 // boolean-sequence boundary.
1999 assert_eq!(metric.cognitive.cognitive_sum(), 2);
2000 insta::assert_json_snapshot!(
2001 metric.cognitive,
2002 @r#"
2003 {
2004 "sum": 2,
2005 "value": 0,
2006 "average": 2.0,
2007 "min": 0,
2008 "max": 2
2009 }
2010 "#
2011 );
2012 },
2013 );
2014 }
2015
2016 #[cfg(all(feature = "c", feature = "cpp"))]
2017 #[test]
2018 fn c_not_booleans() {
2019 // `!` does not break boolean sequences (issue #392): the inner
2020 // `&&` is folded into the outer `&&`'s span because pre-order
2021 // visits the outer `binary_expression` first.
2022 check_metrics::<CParser>(
2023 "void f() {
2024 if (a && !(b && c)) { // +2 (+1 if, +1 outer &&; inner && continues)
2025 printf(\"test\");
2026 }
2027 }",
2028 "foo.c",
2029 |metric| {
2030 insta::assert_json_snapshot!(
2031 metric.cognitive,
2032 @r#"
2033 {
2034 "sum": 2,
2035 "value": 0,
2036 "average": 2.0,
2037 "min": 0,
2038 "max": 2
2039 }
2040 "#
2041 );
2042 },
2043 );
2044
2045 check_metrics::<CppParser>(
2046 "void f() {
2047 if (!(a || b) && !(c || d)) { // +4 (+1 ||, +1 &&, +1 ||)
2048 printf(\"test\");
2049 }
2050 }",
2051 "foo.c",
2052 |metric| {
2053 insta::assert_json_snapshot!(
2054 metric.cognitive,
2055 @r#"
2056 {
2057 "sum": 4,
2058 "value": 0,
2059 "average": 4.0,
2060 "min": 0,
2061 "max": 4
2062 }
2063 "#
2064 );
2065 },
2066 );
2067 }
2068
2069 #[cfg(feature = "mozjs")]
2070 #[test]
2071 fn mozjs_not_booleans() {
2072 // `!` does not break boolean sequences (issue #392): inner `&&`
2073 // continues the outer `&&` sequence (pre-order visits the outer
2074 // BinaryExpression first, so its end_byte already covers the
2075 // inner one).
2076 check_metrics::<MozjsParser>(
2077 "function f() {
2078 if (a && !(b && c)) { // +2 (+1 if, +1 outer &&; inner && continues)
2079 window.print(\"test\");
2080 }
2081 }",
2082 "foo.js",
2083 |metric| {
2084 insta::assert_json_snapshot!(
2085 metric.cognitive,
2086 @r#"
2087 {
2088 "sum": 2,
2089 "value": 0,
2090 "average": 2.0,
2091 "min": 0,
2092 "max": 2
2093 }
2094 "#
2095 );
2096 },
2097 );
2098
2099 check_metrics::<MozjsParser>(
2100 "function f() {
2101 if (!(a || b) && !(c || d)) { // +4 (+1 ||, +1 &&, +1 ||)
2102 window.print(\"test\");
2103 }
2104 }",
2105 "foo.js",
2106 |metric| {
2107 insta::assert_json_snapshot!(
2108 metric.cognitive,
2109 @r#"
2110 {
2111 "sum": 4,
2112 "value": 0,
2113 "average": 4.0,
2114 "min": 0,
2115 "max": 4
2116 }
2117 "#
2118 );
2119 },
2120 );
2121 }
2122
2123 #[cfg(feature = "python")]
2124 #[test]
2125 fn python_sequence_different_booleans() {
2126 check_metrics::<PythonParser>(
2127 "def f(a, b):
2128 if a and b or True: # +3 (+1 and, +1 or)
2129 return 1",
2130 "foo.py",
2131 |metric| {
2132 insta::assert_json_snapshot!(
2133 metric.cognitive,
2134 @r#"
2135 {
2136 "sum": 3,
2137 "value": 0,
2138 "average": 3.0,
2139 "min": 0,
2140 "max": 3
2141 }
2142 "#
2143 );
2144 },
2145 );
2146 }
2147
2148 #[cfg(feature = "rust")]
2149 #[test]
2150 fn rust_sequence_different_booleans() {
2151 check_metrics::<RustParser>(
2152 "fn f() {
2153 if a && b || true { // +3 (+1 &&, +1 ||)
2154 println!(\"test\");
2155 }
2156 }",
2157 "foo.rs",
2158 |metric| {
2159 insta::assert_json_snapshot!(
2160 metric.cognitive,
2161 @r#"
2162 {
2163 "sum": 3,
2164 "value": 0,
2165 "average": 3.0,
2166 "min": 0,
2167 "max": 3
2168 }
2169 "#
2170 );
2171 },
2172 );
2173 }
2174
2175 #[cfg(feature = "c")]
2176 #[test]
2177 fn c_sequence_different_booleans() {
2178 check_metrics::<CParser>(
2179 "void f() {
2180 if (a && b || 1 == 1) { // +3 (+1 &&, +1 ||)
2181 printf(\"test\");
2182 }
2183 }",
2184 "foo.c",
2185 |metric| {
2186 insta::assert_json_snapshot!(
2187 metric.cognitive,
2188 @r#"
2189 {
2190 "sum": 3,
2191 "value": 0,
2192 "average": 3.0,
2193 "min": 0,
2194 "max": 3
2195 }
2196 "#
2197 );
2198 },
2199 );
2200 }
2201
2202 #[cfg(feature = "mozjs")]
2203 #[test]
2204 fn mozjs_sequence_different_booleans() {
2205 check_metrics::<MozjsParser>(
2206 "function f() {
2207 if (a && b || 1 == 1) { // +3 (+1 &&, +1 ||)
2208 window.print(\"test\");
2209 }
2210 }",
2211 "foo.js",
2212 |metric| {
2213 insta::assert_json_snapshot!(
2214 metric.cognitive,
2215 @r#"
2216 {
2217 "sum": 3,
2218 "value": 0,
2219 "average": 3.0,
2220 "min": 0,
2221 "max": 3
2222 }
2223 "#
2224 );
2225 },
2226 );
2227 }
2228
2229 #[cfg(feature = "python")]
2230 #[test]
2231 fn python_formatted_sequence_different_booleans() {
2232 check_metrics::<PythonParser>(
2233 "def f(a, b):
2234 if ( # +1
2235 a and b and # +1
2236 (c or d) # +1
2237 ):
2238 return 1",
2239 "foo.py",
2240 |metric| {
2241 insta::assert_json_snapshot!(
2242 metric.cognitive,
2243 @r#"
2244 {
2245 "sum": 3,
2246 "value": 0,
2247 "average": 3.0,
2248 "min": 0,
2249 "max": 3
2250 }
2251 "#
2252 );
2253 },
2254 );
2255 }
2256
2257 #[cfg(feature = "python")]
2258 #[test]
2259 fn python_1_level_nesting() {
2260 check_metrics::<PythonParser>(
2261 "def f(a, b):
2262 if a: # +1
2263 for i in range(b): # +2
2264 return 1",
2265 "foo.py",
2266 |metric| {
2267 insta::assert_json_snapshot!(
2268 metric.cognitive,
2269 @r#"
2270 {
2271 "sum": 3,
2272 "value": 0,
2273 "average": 3.0,
2274 "min": 0,
2275 "max": 3
2276 }
2277 "#
2278 );
2279 },
2280 );
2281 }
2282
2283 #[cfg(feature = "rust")]
2284 #[test]
2285 fn rust_1_level_nesting() {
2286 check_metrics::<RustParser>(
2287 "fn f() {
2288 if true { // +1
2289 if true { // +2 (nesting = 1)
2290 println!(\"test\");
2291 } else if 1 == 1 { // +1
2292 if true { // +3 (nesting = 2)
2293 println!(\"test\");
2294 }
2295 } else { // +1
2296 if true { // +3 (nesting = 2)
2297 println!(\"test\");
2298 }
2299 }
2300 }
2301 }",
2302 "foo.rs",
2303 |metric| {
2304 insta::assert_json_snapshot!(
2305 metric.cognitive,
2306 @r#"
2307 {
2308 "sum": 11,
2309 "value": 0,
2310 "average": 11.0,
2311 "min": 0,
2312 "max": 11
2313 }
2314 "#
2315 );
2316 },
2317 );
2318
2319 check_metrics::<RustParser>(
2320 "fn f() {
2321 if true { // +1
2322 match true { // +2 (nesting = 1)
2323 true => println!(\"test\"),
2324 false => println!(\"test\"),
2325 }
2326 }
2327 }",
2328 "foo.rs",
2329 |metric| {
2330 insta::assert_json_snapshot!(
2331 metric.cognitive,
2332 @r#"
2333 {
2334 "sum": 3,
2335 "value": 0,
2336 "average": 3.0,
2337 "min": 0,
2338 "max": 3
2339 }
2340 "#
2341 );
2342 },
2343 );
2344 }
2345
2346 #[cfg(feature = "c")]
2347 #[test]
2348 fn c_1_level_nesting() {
2349 check_metrics::<CParser>(
2350 "void f() {
2351 if (1 == 1) { // +1
2352 if (1 == 1) { // +2 (nesting = 1)
2353 printf(\"test\");
2354 } else if (1 == 1) { // +1
2355 if (1 == 1) { // +3 (nesting = 2)
2356 printf(\"test\");
2357 }
2358 } else { // +1
2359 if (1 == 1) { // +3 (nesting = 2)
2360 printf(\"test\");
2361 }
2362 }
2363 }
2364 }",
2365 "foo.c",
2366 |metric| {
2367 insta::assert_json_snapshot!(
2368 metric.cognitive,
2369 @r#"
2370 {
2371 "sum": 11,
2372 "value": 0,
2373 "average": 11.0,
2374 "min": 0,
2375 "max": 11
2376 }
2377 "#
2378 );
2379 },
2380 );
2381 }
2382
2383 #[cfg(feature = "mozjs")]
2384 #[test]
2385 fn mozjs_1_level_nesting() {
2386 check_metrics::<MozjsParser>(
2387 "function f() {
2388 if (1 == 1) { // +1
2389 if (1 == 1) { // +2 (nesting = 1)
2390 window.print(\"test\");
2391 } else if (1 == 1) { // +1
2392 if (1 == 1) { // +3 (nesting = 2)
2393 window.print(\"test\");
2394 }
2395 } else { // +1
2396 if (1 == 1) { // +3 (nesting = 2)
2397 window.print(\"test\");
2398 }
2399 }
2400 }
2401 }",
2402 "foo.js",
2403 |metric| {
2404 insta::assert_json_snapshot!(
2405 metric.cognitive,
2406 @r#"
2407 {
2408 "sum": 11,
2409 "value": 0,
2410 "average": 11.0,
2411 "min": 0,
2412 "max": 11
2413 }
2414 "#
2415 );
2416 },
2417 );
2418 }
2419
2420 #[cfg(feature = "javascript")]
2421 #[test]
2422 fn javascript_nesting() {
2423 check_metrics::<JavascriptParser>(
2424 "function f() {
2425 if (a) { // +1
2426 for (let i = 0; i < 10; i++) { // +2 (nesting = 1)
2427 while (b) { // +3 (nesting = 2)
2428 console.log(\"test\");
2429 }
2430 }
2431 }
2432 }",
2433 "foo.js",
2434 |metric| {
2435 insta::assert_json_snapshot!(
2436 metric.cognitive,
2437 @r#"
2438 {
2439 "sum": 6,
2440 "value": 0,
2441 "average": 6.0,
2442 "min": 0,
2443 "max": 6
2444 }
2445 "#
2446 );
2447 },
2448 );
2449 }
2450
2451 #[cfg(feature = "python")]
2452 #[test]
2453 fn python_2_level_nesting() {
2454 check_metrics::<PythonParser>(
2455 "def f(a, b):
2456 if a: # +1
2457 for i in range(b): # +2
2458 if b: # +3
2459 return 1",
2460 "foo.py",
2461 |metric| {
2462 insta::assert_json_snapshot!(
2463 metric.cognitive,
2464 @r#"
2465 {
2466 "sum": 6,
2467 "value": 0,
2468 "average": 6.0,
2469 "min": 0,
2470 "max": 6
2471 }
2472 "#
2473 );
2474 },
2475 );
2476 }
2477
2478 #[cfg(feature = "rust")]
2479 #[test]
2480 fn rust_2_level_nesting() {
2481 check_metrics::<RustParser>(
2482 "fn f() {
2483 if true { // +1
2484 for i in 0..4 { // +2 (nesting = 1)
2485 match true { // +3 (nesting = 2)
2486 true => println!(\"test\"),
2487 false => println!(\"test\"),
2488 }
2489 }
2490 }
2491 }",
2492 "foo.rs",
2493 |metric| {
2494 insta::assert_json_snapshot!(
2495 metric.cognitive,
2496 @r#"
2497 {
2498 "sum": 6,
2499 "value": 0,
2500 "average": 6.0,
2501 "min": 0,
2502 "max": 6
2503 }
2504 "#
2505 );
2506 },
2507 );
2508 }
2509
2510 #[cfg(feature = "python")]
2511 #[test]
2512 fn python_try_construct() {
2513 check_metrics::<PythonParser>(
2514 "def f(a, b):
2515 try:
2516 for foo in bar: # +1
2517 return a
2518 except Exception: # +1
2519 if a < 0: # +2
2520 return a",
2521 "foo.py",
2522 |metric| {
2523 insta::assert_json_snapshot!(
2524 metric.cognitive,
2525 @r#"
2526 {
2527 "sum": 4,
2528 "value": 0,
2529 "average": 4.0,
2530 "min": 0,
2531 "max": 4
2532 }
2533 "#
2534 );
2535 },
2536 );
2537 }
2538
2539 #[cfg(feature = "python")]
2540 #[test]
2541 fn python_flat_try_except() {
2542 // Regression for #242: flat try/except at function top level
2543 // must still score +1 for the except clause (no enclosing
2544 // control-flow nesting). Before the fix this happened to be
2545 // correct because `stats.nesting` was zero; after the fix the
2546 // value is the same — `increase_nesting` records nesting=0 and
2547 // bumps structural by 0+1.
2548 check_metrics::<PythonParser>(
2549 "def f():
2550 try:
2551 pass
2552 except Exception: # +1
2553 pass",
2554 "foo.py",
2555 |metric| {
2556 // expected: only the except clause contributes (+1).
2557 assert_eq!(metric.cognitive.cognitive_sum() as u32, 1);
2558 insta::assert_json_snapshot!(
2559 metric.cognitive,
2560 @r#"
2561 {
2562 "sum": 1,
2563 "value": 0,
2564 "average": 1.0,
2565 "min": 0,
2566 "max": 1
2567 }
2568 "#
2569 );
2570 },
2571 );
2572 }
2573
2574 #[cfg(feature = "python")]
2575 #[test]
2576 fn python_except_inside_if() {
2577 // Regression for #242: try/except nested inside an `if` must
2578 // apply a nesting penalty to the except clause. Before the
2579 // fix, the except contributed +1 because `stats.nesting` was
2580 // stale (0 from the previous `increase_nesting` call on the
2581 // if). After the fix the except sees nesting=1 and contributes
2582 // +2.
2583 check_metrics::<PythonParser>(
2584 "def f(x):
2585 if x: # +1
2586 try:
2587 pass
2588 except Exception: # +2 (nesting = 1)
2589 pass",
2590 "foo.py",
2591 |metric| {
2592 // expected: if (+1) + except inside if (+2) = 3
2593 assert_eq!(metric.cognitive.cognitive_sum() as u32, 3);
2594 insta::assert_json_snapshot!(
2595 metric.cognitive,
2596 @r#"
2597 {
2598 "sum": 3,
2599 "value": 0,
2600 "average": 3.0,
2601 "min": 0,
2602 "max": 3
2603 }
2604 "#
2605 );
2606 },
2607 );
2608 }
2609
2610 #[cfg(feature = "python")]
2611 #[test]
2612 fn python_except_inside_for() {
2613 // Regression for #242: try/except nested inside a `for` must
2614 // apply the for's nesting penalty to the except clause.
2615 check_metrics::<PythonParser>(
2616 "def f(xs):
2617 for x in xs: # +1
2618 try:
2619 pass
2620 except Exception: # +2 (nesting = 1)
2621 pass",
2622 "foo.py",
2623 |metric| {
2624 // expected: for (+1) + except inside for (+2) = 3
2625 assert_eq!(metric.cognitive.cognitive_sum() as u32, 3);
2626 insta::assert_json_snapshot!(
2627 metric.cognitive,
2628 @r#"
2629 {
2630 "sum": 3,
2631 "value": 0,
2632 "average": 3.0,
2633 "min": 0,
2634 "max": 3
2635 }
2636 "#
2637 );
2638 },
2639 );
2640 }
2641
2642 #[cfg(feature = "python")]
2643 #[test]
2644 fn python_multi_except_inside_if() {
2645 // Regression for #242: every clause in a multi-except chain
2646 // nested inside an `if` must reflect the nesting penalty.
2647 // Before the fix, all three except clauses contributed +1;
2648 // after the fix each contributes +2 (nesting = 1 from the
2649 // enclosing if).
2650 check_metrics::<PythonParser>(
2651 "def f(x):
2652 if x: # +1
2653 try:
2654 pass
2655 except ValueError: # +2
2656 pass
2657 except TypeError: # +2
2658 pass
2659 except Exception: # +2
2660 pass",
2661 "foo.py",
2662 |metric| {
2663 // expected: if (+1) + 3 * except inside if (+2 each) = 7
2664 assert_eq!(metric.cognitive.cognitive_sum() as u32, 7);
2665 insta::assert_json_snapshot!(
2666 metric.cognitive,
2667 @r#"
2668 {
2669 "sum": 7,
2670 "value": 0,
2671 "average": 7.0,
2672 "min": 0,
2673 "max": 7
2674 }
2675 "#
2676 );
2677 },
2678 );
2679 }
2680
2681 #[cfg(feature = "mozjs")]
2682 #[test]
2683 fn mozjs_try_construct() {
2684 check_metrics::<MozjsParser>(
2685 "function asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
2686 for (const collector of this.collectors) {
2687 try {
2688 collector._onChannelRedirect(oldChannel, newChannel, flags);
2689 } catch (ex) {
2690 console.error(
2691 \"StackTraceCollector.onChannelRedirect threw an exception\",
2692 ex
2693 );
2694 }
2695 }
2696 callback.onRedirectVerifyCallback(Cr.NS_OK);
2697 }",
2698 "foo.js",
2699 |metric| {
2700 insta::assert_json_snapshot!(
2701 metric.cognitive,
2702 @r#"
2703 {
2704 "sum": 3,
2705 "value": 0,
2706 "average": 3.0,
2707 "min": 0,
2708 "max": 3
2709 }
2710 "#
2711 );
2712 },
2713 );
2714 }
2715
2716 #[cfg(feature = "javascript")]
2717 #[test]
2718 fn javascript_try_construct() {
2719 check_metrics::<JavascriptParser>(
2720 "function f() {
2721 for (let i = 0; i < 10; i++) { // +1
2722 try {
2723 doSomething(i);
2724 } catch (ex) { // +2 (nesting = 1)
2725 if (ex instanceof TypeError) { // +3 (nesting = 2)
2726 console.error(\"type error\");
2727 }
2728 } finally {
2729 cleanup();
2730 }
2731 }
2732 }",
2733 "foo.js",
2734 |metric| {
2735 insta::assert_json_snapshot!(
2736 metric.cognitive,
2737 @r#"
2738 {
2739 "sum": 6,
2740 "value": 0,
2741 "average": 6.0,
2742 "min": 0,
2743 "max": 6
2744 }
2745 "#
2746 );
2747 },
2748 );
2749 }
2750
2751 // The tree-sitter-javascript / -typescript grammars fold both
2752 // `for...in` and `for...of` into the same `for_in_statement` node
2753 // (only the keyword token differs). The four regression tests below
2754 // lock that in across every JS-family parser, so any future grammar
2755 // bump that splits `for...of` into its own node kind would surface
2756 // here rather than silently scoring `for...of` loops as 0 cognitive.
2757
2758 #[cfg(feature = "javascript")]
2759 #[test]
2760 fn javascript_for_of_loop() {
2761 check_metrics::<JavascriptParser>(
2762 "function f(xs) {
2763 let s = 0;
2764 for (const x of xs) { // +1
2765 s += x;
2766 }
2767 return s;
2768 }",
2769 "foo.js",
2770 |metric| {
2771 assert_eq!(metric.cognitive.cognitive_sum(), 1);
2772 assert_eq!(metric.cognitive.cognitive_max(), 1);
2773 insta::assert_json_snapshot!(
2774 metric.cognitive,
2775 @r#"
2776 {
2777 "sum": 1,
2778 "value": 0,
2779 "average": 1.0,
2780 "min": 0,
2781 "max": 1
2782 }
2783 "#
2784 );
2785 },
2786 );
2787 }
2788
2789 #[cfg(feature = "mozjs")]
2790 #[test]
2791 fn mozjs_for_of_loop() {
2792 check_metrics::<MozjsParser>(
2793 "function f(xs) {
2794 let s = 0;
2795 for (const x of xs) { // +1
2796 s += x;
2797 }
2798 return s;
2799 }",
2800 "foo.js",
2801 |metric| {
2802 assert_eq!(metric.cognitive.cognitive_sum(), 1);
2803 assert_eq!(metric.cognitive.cognitive_max(), 1);
2804 insta::assert_json_snapshot!(
2805 metric.cognitive,
2806 @r#"
2807 {
2808 "sum": 1,
2809 "value": 0,
2810 "average": 1.0,
2811 "min": 0,
2812 "max": 1
2813 }
2814 "#
2815 );
2816 },
2817 );
2818 }
2819
2820 #[cfg(feature = "typescript")]
2821 #[test]
2822 fn typescript_for_of_loop() {
2823 check_metrics::<TypescriptParser>(
2824 "function f(xs: number[]): number {
2825 let s = 0;
2826 for (const x of xs) { // +1
2827 s += x;
2828 }
2829 return s;
2830 }",
2831 "foo.ts",
2832 |metric| {
2833 assert_eq!(metric.cognitive.cognitive_sum(), 1);
2834 assert_eq!(metric.cognitive.cognitive_max(), 1);
2835 insta::assert_json_snapshot!(
2836 metric.cognitive,
2837 @r#"
2838 {
2839 "sum": 1,
2840 "value": 0,
2841 "average": 1.0,
2842 "min": 0,
2843 "max": 1
2844 }
2845 "#
2846 );
2847 },
2848 );
2849 }
2850
2851 #[cfg(feature = "typescript")]
2852 #[test]
2853 fn tsx_for_of_loop() {
2854 check_metrics::<TsxParser>(
2855 "function f(xs: number[]): number {
2856 let s = 0;
2857 for (const x of xs) { // +1
2858 s += x;
2859 }
2860 return s;
2861 }",
2862 "foo.tsx",
2863 |metric| {
2864 assert_eq!(metric.cognitive.cognitive_sum(), 1);
2865 assert_eq!(metric.cognitive.cognitive_max(), 1);
2866 insta::assert_json_snapshot!(
2867 metric.cognitive,
2868 @r#"
2869 {
2870 "sum": 1,
2871 "value": 0,
2872 "average": 1.0,
2873 "min": 0,
2874 "max": 1
2875 }
2876 "#
2877 );
2878 },
2879 );
2880 }
2881
2882 #[cfg(feature = "rust")]
2883 #[test]
2884 fn rust_break_continue() {
2885 // Only labeled break and continue statements are considered
2886 check_metrics::<RustParser>(
2887 "fn f() {
2888 'tens: for ten in 0..3 { // +1
2889 '_units: for unit in 0..=9 { // +2 (nesting = 1)
2890 if unit % 2 == 0 { // +3 (nesting = 2)
2891 continue;
2892 } else if unit == 5 { // +1
2893 continue 'tens; // +1
2894 } else if unit == 6 { // +1
2895 break;
2896 } else { // +1
2897 break 'tens; // +1
2898 }
2899 }
2900 }
2901 }",
2902 "foo.rs",
2903 |metric| {
2904 insta::assert_json_snapshot!(
2905 metric.cognitive,
2906 @r#"
2907 {
2908 "sum": 11,
2909 "value": 0,
2910 "average": 11.0,
2911 "min": 0,
2912 "max": 11
2913 }
2914 "#
2915 );
2916 },
2917 );
2918 }
2919
2920 // Regression for #389: Rust's `loop {}` has a dedicated grammar node
2921 // (LoopExpression) distinct from WhileExpression. The cognitive nesting
2922 // arm previously matched only For/While/Match, so `loop {}` silently
2923 // contributed neither a structural +1 nor a nesting bump.
2924 #[cfg(feature = "rust")]
2925 #[test]
2926 fn rust_loop_single() {
2927 check_metrics::<RustParser>(
2928 "fn f() {
2929 loop { // +1
2930 if true { // +2 (nesting = 1)
2931 break;
2932 }
2933 }
2934 }",
2935 "foo.rs",
2936 |metric| {
2937 // expected: loop=+1, nested if=+2 (1 + nesting depth 1) = 3
2938 assert_eq!(metric.cognitive.cognitive_sum() as u32, 3);
2939 insta::assert_json_snapshot!(
2940 metric.cognitive,
2941 @r#"
2942 {
2943 "sum": 3,
2944 "value": 0,
2945 "average": 3.0,
2946 "min": 0,
2947 "max": 3
2948 }
2949 "#
2950 );
2951 },
2952 );
2953 }
2954
2955 // Regression for #389: nested `loop` blocks must accrue nesting just
2956 // like nested `while`/`for` would.
2957 #[cfg(feature = "rust")]
2958 #[test]
2959 fn rust_loop_nested() {
2960 check_metrics::<RustParser>(
2961 "fn f() {
2962 loop { // +1
2963 loop { // +2 (nesting = 1)
2964 if true { // +3 (nesting = 2)
2965 break;
2966 }
2967 }
2968 }
2969 }",
2970 "foo.rs",
2971 |metric| {
2972 // expected: outer loop=+1, inner loop=+2, inner if=+3 = 6
2973 assert_eq!(metric.cognitive.cognitive_sum() as u32, 6);
2974 insta::assert_json_snapshot!(
2975 metric.cognitive,
2976 @r#"
2977 {
2978 "sum": 6,
2979 "value": 0,
2980 "average": 6.0,
2981 "min": 0,
2982 "max": 6
2983 }
2984 "#
2985 );
2986 },
2987 );
2988 }
2989
2990 #[cfg(feature = "cpp")]
2991 #[test]
2992 fn cpp_nested_function_resets_nesting_and_adds_depth() {
2993 // Regression for #696: a method defined on a local struct declared
2994 // two `if`s deep inside an outer method must reset nesting to 0 and
2995 // gain a function-depth surcharge — not inherit the enclosing
2996 // nesting.
2997 //
2998 // expected: outer `if` (+1, nesting=0) + inner `if` (+2, nesting=1)
2999 // + Inner::g's `if` (+1 base + 1 depth = +2, nesting=0, depth=1) = 5.
3000 // Before the fix, `g` inherited nesting=2 from the two enclosing
3001 // `if`s, scoring its inner `if` at nesting 2 (+3) for a sum of 6.
3002 // The two-deep nesting is load-bearing: one level deep, the
3003 // inherited nesting (1) coincidentally equals the depth bump (1).
3004 check_metrics::<CppParser>(
3005 "struct S {
3006 void outer(bool a) {
3007 if (a) {
3008 if (a) {
3009 struct Inner {
3010 void g(bool b) {
3011 if (b) { h(); }
3012 }
3013 };
3014 }
3015 }
3016 }
3017 };",
3018 "foo.cpp",
3019 |metric| {
3020 assert_eq!(metric.cognitive.cognitive_sum(), 5);
3021 assert_eq!(metric.cognitive.cognitive_max(), 3);
3022 },
3023 );
3024 }
3025
3026 #[cfg(feature = "c")]
3027 #[test]
3028 fn c_goto() {
3029 check_metrics::<CParser>(
3030 "void f() {
3031 OUT: for (int i = 1; i <= max; ++i) { // +1
3032 for (int j = 2; j < i; ++j) { // +2 (nesting = 1)
3033 if (i % j == 0) { // +3 (nesting = 2)
3034 goto OUT; // +1
3035 }
3036 }
3037 }
3038 }",
3039 "foo.c",
3040 |metric| {
3041 insta::assert_json_snapshot!(
3042 metric.cognitive,
3043 @r#"
3044 {
3045 "sum": 7,
3046 "value": 0,
3047 "average": 7.0,
3048 "min": 0,
3049 "max": 7
3050 }
3051 "#
3052 );
3053 },
3054 );
3055 }
3056
3057 #[cfg(feature = "c")]
3058 #[test]
3059 fn c_switch() {
3060 check_metrics::<CParser>(
3061 "void f() {
3062 switch (1) { // +1
3063 case 1:
3064 printf(\"one\");
3065 break;
3066 case 2:
3067 printf(\"two\");
3068 break;
3069 case 3:
3070 printf(\"three\");
3071 break;
3072 default:
3073 printf(\"all\");
3074 break;
3075 }
3076 }",
3077 "foo.c",
3078 |metric| {
3079 insta::assert_json_snapshot!(
3080 metric.cognitive,
3081 @r#"
3082 {
3083 "sum": 1,
3084 "value": 0,
3085 "average": 1.0,
3086 "min": 0,
3087 "max": 1
3088 }
3089 "#
3090 );
3091 },
3092 );
3093 }
3094
3095 #[cfg(feature = "c")]
3096 #[test]
3097 fn c_ternary() {
3098 // Sonar's rule scores the ternary `?:` as +1 (and +nesting), matching
3099 // the JS/Java/Python/Rust families. The cognitive walker matches the
3100 // `conditional_expression` node, so the operator participates in nesting
3101 // like any other conditional construct.
3102 check_metrics::<CParser>(
3103 "int f(int a) {
3104 if (a) { // +1
3105 return a > 0 ? 1 : -1; // +2 (1 + nesting 1)
3106 }
3107 return a > 0 ? 0 : -1; // +1
3108 }",
3109 "foo.c",
3110 // expected: 1 (if) + 2 (nested ternary, nesting=1) + 1 (top-level
3111 // ternary) = 4. max is 4 for the only function.
3112 |metric| {
3113 assert_eq!(metric.cognitive.cognitive_sum(), 4);
3114 assert_eq!(metric.cognitive.cognitive_max(), 4);
3115 insta::assert_json_snapshot!(
3116 metric.cognitive,
3117 @r#"
3118 {
3119 "sum": 4,
3120 "value": 0,
3121 "average": 4.0,
3122 "min": 0,
3123 "max": 4
3124 }
3125 "#
3126 );
3127 },
3128 );
3129 }
3130
3131 #[cfg(feature = "cpp")]
3132 #[test]
3133 fn cpp_try_catch_single() {
3134 check_metrics::<CppParser>(
3135 "void f() {
3136 try {
3137 g();
3138 } catch (const std::exception& e) { // +1
3139 h();
3140 }
3141 }",
3142 "foo.cpp",
3143 |metric| {
3144 // Single catch clause +1.
3145 assert_eq!(metric.cognitive.cognitive_sum(), 1);
3146 assert_eq!(metric.cognitive.cognitive_max(), 1);
3147 insta::assert_json_snapshot!(
3148 metric.cognitive,
3149 @r#"
3150 {
3151 "sum": 1,
3152 "value": 0,
3153 "average": 1.0,
3154 "min": 0,
3155 "max": 1
3156 }
3157 "#
3158 );
3159 },
3160 );
3161 }
3162
3163 #[cfg(feature = "cpp")]
3164 #[test]
3165 fn cpp_try_multiple_catches() {
3166 check_metrics::<CppParser>(
3167 "void f() {
3168 try {
3169 g();
3170 } catch (const std::runtime_error& e) { // +1
3171 h();
3172 } catch (const std::logic_error& e) { // +1
3173 i();
3174 } catch (...) { // +1
3175 j();
3176 }
3177 }",
3178 "foo.cpp",
3179 |metric| {
3180 // Three catch clauses, each +1 at nesting 0 → 3.
3181 assert_eq!(metric.cognitive.cognitive_sum(), 3);
3182 assert_eq!(metric.cognitive.cognitive_max(), 3);
3183 insta::assert_json_snapshot!(
3184 metric.cognitive,
3185 @r#"
3186 {
3187 "sum": 3,
3188 "value": 0,
3189 "average": 3.0,
3190 "min": 0,
3191 "max": 3
3192 }
3193 "#
3194 );
3195 },
3196 );
3197 }
3198
3199 #[cfg(feature = "cpp")]
3200 #[test]
3201 fn cpp_try_catch_in_loop() {
3202 check_metrics::<CppParser>(
3203 "void f() {
3204 for (int i = 0; i < 10; ++i) { // +1
3205 try {
3206 g();
3207 } catch (const std::exception& e) { // +2 (nesting = 1)
3208 h();
3209 }
3210 }
3211 }",
3212 "foo.cpp",
3213 |metric| {
3214 // for +1, catch +2 (nesting = 1) → 3.
3215 assert_eq!(metric.cognitive.cognitive_sum(), 3);
3216 assert_eq!(metric.cognitive.cognitive_max(), 3);
3217 insta::assert_json_snapshot!(
3218 metric.cognitive,
3219 @r#"
3220 {
3221 "sum": 3,
3222 "value": 0,
3223 "average": 3.0,
3224 "min": 0,
3225 "max": 3
3226 }
3227 "#
3228 );
3229 },
3230 );
3231 }
3232
3233 #[cfg(feature = "cpp")]
3234 #[test]
3235 fn cpp_range_based_for() {
3236 check_metrics::<CppParser>(
3237 "int sum(const std::vector<int>& v) {
3238 int s = 0;
3239 for (int x : v) { // +1
3240 s += x;
3241 }
3242 return s;
3243 }",
3244 "foo.cpp",
3245 |metric| {
3246 // C++11 range-based `for (auto x : v)` parses as
3247 // `for_range_loop`; it is a control-flow construct and
3248 // counts the same as a classic `for_statement` → +1.
3249 assert_eq!(metric.cognitive.cognitive_sum(), 1);
3250 assert_eq!(metric.cognitive.cognitive_max(), 1);
3251 insta::assert_json_snapshot!(
3252 metric.cognitive,
3253 @r#"
3254 {
3255 "sum": 1,
3256 "value": 0,
3257 "average": 1.0,
3258 "min": 0,
3259 "max": 1
3260 }
3261 "#
3262 );
3263 },
3264 );
3265 }
3266
3267 #[cfg(feature = "cpp")]
3268 #[test]
3269 fn cpp_nested_range_based_for() {
3270 check_metrics::<CppParser>(
3271 "void f(const std::vector<std::vector<int>>& vv) {
3272 for (const auto& row : vv) { // +1
3273 for (int x : row) { // +2 (nesting = 1)
3274 g(x);
3275 }
3276 }
3277 }",
3278 "foo.cpp",
3279 |metric| {
3280 // Nested range-fors compound by nesting, matching the
3281 // behaviour of nested classic `for` loops: 1 + 2 = 3.
3282 assert_eq!(metric.cognitive.cognitive_sum(), 3);
3283 assert_eq!(metric.cognitive.cognitive_max(), 3);
3284 insta::assert_json_snapshot!(
3285 metric.cognitive,
3286 @r#"
3287 {
3288 "sum": 3,
3289 "value": 0,
3290 "average": 3.0,
3291 "min": 0,
3292 "max": 3
3293 }
3294 "#
3295 );
3296 },
3297 );
3298 }
3299
3300 #[cfg(feature = "c")]
3301 #[test]
3302 fn c_nested_for() {
3303 check_metrics::<CParser>(
3304 "void f(int n, int m) {
3305 for (int i = 0; i < n; ++i) { // +1
3306 for (int j = 0; j < m; ++j) { // +2 (nesting = 1)
3307 for (int k = 0; k < 4; ++k) { // +3 (nesting = 2)
3308 g(i, j, k);
3309 }
3310 }
3311 }
3312 }",
3313 "foo.c",
3314 |metric| {
3315 // Three nested `for` loops → 1 + 2 + 3 = 6.
3316 assert_eq!(metric.cognitive.cognitive_sum(), 6);
3317 assert_eq!(metric.cognitive.cognitive_max(), 6);
3318 insta::assert_json_snapshot!(
3319 metric.cognitive,
3320 @r#"
3321 {
3322 "sum": 6,
3323 "value": 0,
3324 "average": 6.0,
3325 "min": 0,
3326 "max": 6
3327 }
3328 "#
3329 );
3330 },
3331 );
3332 }
3333
3334 #[cfg(feature = "c")]
3335 #[test]
3336 fn c_nested_while() {
3337 check_metrics::<CParser>(
3338 "void f(int n) {
3339 while (n > 0) { // +1
3340 while (n % 2 == 0) { // +2 (nesting = 1)
3341 n /= 2;
3342 }
3343 n -= 1;
3344 }
3345 }",
3346 "foo.c",
3347 |metric| {
3348 // Two nested `while` loops → 1 + 2 = 3.
3349 assert_eq!(metric.cognitive.cognitive_sum(), 3);
3350 assert_eq!(metric.cognitive.cognitive_max(), 3);
3351 insta::assert_json_snapshot!(
3352 metric.cognitive,
3353 @r#"
3354 {
3355 "sum": 3,
3356 "value": 0,
3357 "average": 3.0,
3358 "min": 0,
3359 "max": 3
3360 }
3361 "#
3362 );
3363 },
3364 );
3365 }
3366
3367 #[cfg(feature = "c")]
3368 #[test]
3369 fn c_recursion() {
3370 // Sonar's rule scores each recursive call to the enclosing function
3371 // as +1, but the file-level comment in `cognitive.rs` documents that
3372 // recursion is not tracked for C/C++ because the call graph is only
3373 // resolvable at run time. The body of `fact` therefore costs only
3374 // the explicit `if`.
3375 check_metrics::<CParser>(
3376 "int fact(int n) {
3377 if (n <= 1) { // +1
3378 return 1;
3379 }
3380 return n * fact(n - 1); // recursion: currently not counted
3381 }",
3382 "foo.c",
3383 |metric| {
3384 // Only the `if` contributes; recursion is a documented gap.
3385 assert_eq!(metric.cognitive.cognitive_sum(), 1);
3386 assert_eq!(metric.cognitive.cognitive_max(), 1);
3387 insta::assert_json_snapshot!(
3388 metric.cognitive,
3389 @r#"
3390 {
3391 "sum": 1,
3392 "value": 0,
3393 "average": 1.0,
3394 "min": 0,
3395 "max": 1
3396 }
3397 "#
3398 );
3399 },
3400 );
3401 }
3402
3403 #[cfg(feature = "c")]
3404 #[test]
3405 fn c_goto_sibling_jump() {
3406 check_metrics::<CParser>(
3407 "void f(int n) {
3408 if (n < 0) { // +1
3409 goto err; // +1
3410 }
3411 if (n > 100) { // +1
3412 goto err; // +1
3413 }
3414 return;
3415 err:
3416 abort();
3417 }",
3418 "foo.c",
3419 |metric| {
3420 // Two `if` (+1 each) and two `goto` (+1 each) at nesting 0
3421 // (the `goto` cost is flat, not multiplied by nesting) → 4.
3422 assert_eq!(metric.cognitive.cognitive_sum(), 4);
3423 assert_eq!(metric.cognitive.cognitive_max(), 4);
3424 insta::assert_json_snapshot!(
3425 metric.cognitive,
3426 @r#"
3427 {
3428 "sum": 4,
3429 "value": 0,
3430 "average": 4.0,
3431 "min": 0,
3432 "max": 4
3433 }
3434 "#
3435 );
3436 },
3437 );
3438 }
3439
3440 #[cfg(feature = "cpp")]
3441 #[test]
3442 fn cpp_lambda_inside_function() {
3443 // Per `increase_nesting`, entering a lambda bumps the effective nesting
3444 // by one — so an `if` directly inside a top-level lambda is +2 charged
3445 // to the enclosing function (Cpp lambdas are not split into a separate
3446 // FuncSpace by `getter.rs`, so the `if` is not double-counted).
3447 // The lambda *is* counted as a closure by NoM, so the cognitive
3448 // average is sum / (1 function + 1 closure) = 2 / 2 = 1.0.
3449 check_metrics::<CppParser>(
3450 "int f(const std::vector<int>& v) {
3451 auto pred = [](int x) {
3452 if (x > 0) { // +2 (lambda nesting = 1)
3453 return true;
3454 }
3455 return false;
3456 };
3457 return std::count_if(v.begin(), v.end(), pred);
3458 }",
3459 "foo.cpp",
3460 |metric| {
3461 // Single `if` inside lambda at lambda-nesting 1 → +2.
3462 assert_eq!(metric.cognitive.cognitive_sum(), 2);
3463 assert_eq!(metric.cognitive.cognitive_max(), 2);
3464 insta::assert_json_snapshot!(
3465 metric.cognitive,
3466 @r#"
3467 {
3468 "sum": 2,
3469 "value": 0,
3470 "average": 1.0,
3471 "min": 0,
3472 "max": 2
3473 }
3474 "#
3475 );
3476 },
3477 );
3478 }
3479
3480 /// The `mozcpp` fork must charge a lambda the same nesting surcharge
3481 /// as upstream C++ — the same source through `CppParser`
3482 /// (`cpp_lambda_inside_function` above) scores identically.
3483 ///
3484 /// `mozcpp`'s `LambdaExpression` arm had no cognitive test before
3485 /// this, so the whole arm measured zero-coverage even though the
3486 /// fork is expected to stay metric-equivalent to `cpp`.
3487 #[cfg(feature = "mozcpp")]
3488 #[test]
3489 fn mozcpp_lambda_inside_function() {
3490 check_metrics::<MozcppParser>(
3491 "int f(const std::vector<int>& v) {
3492 auto pred = [](int x) {
3493 if (x > 0) { // +2 (lambda nesting = 1)
3494 return true;
3495 }
3496 return false;
3497 };
3498 return std::count_if(v.begin(), v.end(), pred);
3499 }",
3500 "foo.cpp",
3501 |metric| {
3502 assert_eq!(metric.cognitive.cognitive_sum(), 2);
3503 assert_eq!(metric.cognitive.cognitive_max(), 2);
3504 },
3505 );
3506 }
3507
3508 #[cfg(feature = "c")]
3509 #[test]
3510 fn c_switch_fall_through() {
3511 // A `case` without `break` (fall-through) does not add cognitive cost
3512 // beyond the enclosing `switch` itself: only `switch` is in the match
3513 // arm. Same accounting as `c_switch` above — switch +1 only.
3514 check_metrics::<CParser>(
3515 "void f(int n) {
3516 switch (n) { // +1
3517 case 1:
3518 case 2:
3519 g();
3520 // fall-through
3521 case 3:
3522 h();
3523 break;
3524 default:
3525 i();
3526 break;
3527 }
3528 }",
3529 "foo.c",
3530 |metric| {
3531 assert_eq!(metric.cognitive.cognitive_sum(), 1);
3532 assert_eq!(metric.cognitive.cognitive_max(), 1);
3533 insta::assert_json_snapshot!(
3534 metric.cognitive,
3535 @r#"
3536 {
3537 "sum": 1,
3538 "value": 0,
3539 "average": 1.0,
3540 "min": 0,
3541 "max": 1
3542 }
3543 "#
3544 );
3545 },
3546 );
3547 }
3548
3549 #[cfg(feature = "c")]
3550 #[test]
3551 fn c_switch_in_loop() {
3552 check_metrics::<CParser>(
3553 "void f(int n) {
3554 for (int i = 0; i < n; ++i) { // +1
3555 switch (i % 3) { // +2 (nesting = 1)
3556 case 0:
3557 a();
3558 break;
3559 case 1:
3560 b();
3561 break;
3562 default:
3563 c();
3564 break;
3565 }
3566 }
3567 }",
3568 "foo.c",
3569 |metric| {
3570 // for +1, switch +2 (nesting = 1) → 3.
3571 assert_eq!(metric.cognitive.cognitive_sum(), 3);
3572 assert_eq!(metric.cognitive.cognitive_max(), 3);
3573 insta::assert_json_snapshot!(
3574 metric.cognitive,
3575 @r#"
3576 {
3577 "sum": 3,
3578 "value": 0,
3579 "average": 3.0,
3580 "min": 0,
3581 "max": 3
3582 }
3583 "#
3584 );
3585 },
3586 );
3587 }
3588
3589 #[cfg(feature = "c")]
3590 #[test]
3591 fn c_macro_expanded_control_flow() {
3592 // Per the file-level comment in `cognitive.rs`, macro expansion is not
3593 // tracked for C/C++ — macros are treated as opaque tokens. This is the
3594 // defensive case: a control-flow-bearing macro contributes nothing on
3595 // its own; only the explicit `if` in the function body is counted.
3596 check_metrics::<CParser>(
3597 "#define CHECK(x) do { if (!(x)) return; } while (0)
3598 void f(int a, int b) {
3599 CHECK(a); // expansion is opaque: 0
3600 if (b < 0) { // +1
3601 return;
3602 }
3603 }",
3604 "foo.c",
3605 |metric| {
3606 // Only the explicit `if` contributes.
3607 assert_eq!(metric.cognitive.cognitive_sum(), 1);
3608 assert_eq!(metric.cognitive.cognitive_max(), 1);
3609 insta::assert_json_snapshot!(
3610 metric.cognitive,
3611 @r#"
3612 {
3613 "sum": 1,
3614 "value": 0,
3615 "average": 1.0,
3616 "min": 0,
3617 "max": 1
3618 }
3619 "#
3620 );
3621 },
3622 );
3623 }
3624
3625 #[cfg(feature = "mozjs")]
3626 #[test]
3627 fn mozjs_switch() {
3628 check_metrics::<MozjsParser>(
3629 "function f() {
3630 switch (1) { // +1
3631 case 1:
3632 window.print(\"one\");
3633 break;
3634 case 2:
3635 window.print(\"two\");
3636 break;
3637 case 3:
3638 window.print(\"three\");
3639 break;
3640 default:
3641 window.print(\"all\");
3642 break;
3643 }
3644 }",
3645 "foo.js",
3646 |metric| {
3647 insta::assert_json_snapshot!(
3648 metric.cognitive,
3649 @r#"
3650 {
3651 "sum": 1,
3652 "value": 0,
3653 "average": 1.0,
3654 "min": 0,
3655 "max": 1
3656 }
3657 "#
3658 );
3659 },
3660 );
3661 }
3662
3663 #[cfg(feature = "javascript")]
3664 #[test]
3665 fn javascript_switch() {
3666 check_metrics::<JavascriptParser>(
3667 "function f() {
3668 switch (x) { // +1
3669 case 1:
3670 console.log(\"one\");
3671 break;
3672 case 2:
3673 console.log(\"two\");
3674 break;
3675 default:
3676 console.log(\"other\");
3677 break;
3678 }
3679 }",
3680 "foo.js",
3681 |metric| {
3682 insta::assert_json_snapshot!(
3683 metric.cognitive,
3684 @r#"
3685 {
3686 "sum": 1,
3687 "value": 0,
3688 "average": 1.0,
3689 "min": 0,
3690 "max": 1
3691 }
3692 "#
3693 );
3694 },
3695 );
3696 }
3697
3698 #[cfg(feature = "python")]
3699 #[test]
3700 fn python_ternary_operator() {
3701 check_metrics::<PythonParser>(
3702 "def f(a, b):
3703 if a % 2: # +1
3704 return 'c' if a else 'd' # +2
3705 return 'a' if a else 'b' # +1",
3706 "foo.py",
3707 |metric| {
3708 insta::assert_json_snapshot!(
3709 metric.cognitive,
3710 @r#"
3711 {
3712 "sum": 4,
3713 "value": 0,
3714 "average": 4.0,
3715 "min": 0,
3716 "max": 4
3717 }
3718 "#
3719 );
3720 },
3721 );
3722 }
3723
3724 /// Cognitive cost of a boolean sequence inside a `lambda`, under
3725 /// each statement kind that can enclose one.
3726 ///
3727 /// This pins the scores, not the stop set.
3728 /// `python_apply_boolean_operator`'s enclosing-lambda walk stops at
3729 /// `ExpressionList | IfStatement | ForStatement | WhileStatement`,
3730 /// and no fixture below discriminates any of those arms: none has a
3731 /// `lambda` above the stop node, so halting there and running to the
3732 /// module root give the same count. Do not "strengthen" this test by
3733 /// asserting on the arms — the one arm that can differ,
3734 /// `ExpressionList`, is discriminated by
3735 /// `python_boolean_in_expression_list_under_lambda` (#1090).
3736 #[cfg(feature = "python")]
3737 #[test]
3738 fn python_boolean_in_lambda_scores_under_each_enclosing_statement() {
3739 use crate::test_support::metrics_verbatim;
3740
3741 let cognitive_sum = |source: &str| {
3742 metrics_verbatim(
3743 crate::LANG::Python,
3744 source.as_bytes(),
3745 MetricsOptions::default(),
3746 )
3747 .cognitive
3748 .cognitive_sum()
3749 };
3750
3751 // No enclosing branch statement: +1 boolean sequence, +1 for the
3752 // one enclosing lambda = 2.
3753 assert_eq!(cognitive_sum("y = (lambda x: x and x)(1)\n"), 2);
3754
3755 // Each branch statement adds its own +1 nesting on top of that
3756 // same 2.
3757 for (label, source) in [
3758 ("if", "if (lambda x: x and x)(1):\n pass\n"),
3759 ("for", "for i in (lambda x: x and x)(1):\n pass\n"),
3760 ("while", "while (lambda x: x and x)(1):\n break\n"),
3761 (
3762 "for over a comma list",
3763 "for i in (lambda x: x and x)(1), 2:\n pass\n",
3764 ),
3765 ] {
3766 assert_eq!(
3767 cognitive_sum(source),
3768 3,
3769 "{label}: +1 statement nesting, +1 lambda, +1 boolean sequence"
3770 );
3771 }
3772
3773 // A second enclosing lambda adds one more, which is what the
3774 // enclosing-lambda walk is actually for.
3775 assert_eq!(
3776 cognitive_sum("f = lambda a: ((lambda x: x and x)(1), 2)\n"),
3777 3
3778 );
3779 }
3780
3781 /// The `ExpressionList` arm of `python_apply_boolean_operator`'s
3782 /// stop set — the only one of its four arms that can change a score.
3783 ///
3784 /// Two grammar productions can put an `expression_list` under a
3785 /// `lambda`: a parenthesised `yield`, and an f-string interpolation
3786 /// (`_f_expression`). Every other site tree-sitter-python spells
3787 /// `expression_list` at is either a statement (`return`, `del`,
3788 /// `raise`, `for … in`) or an assignment right-hand side, and a
3789 /// lambda body is a single expression, so it can contain none of
3790 /// them. In both fixtures the `expression_list` sits directly above
3791 /// the `boolean_operator` and stops the enclosing-lambda walk before
3792 /// the `lambda` is counted, leaving the +1 boolean sequence alone.
3793 ///
3794 /// Measured, not derived: deleting only the `ExpressionList` arm
3795 /// takes both fixtures from 1 to 2, while the doubly-nested lambda
3796 /// in `python_boolean_in_lambda_scores_under_each_enclosing_statement`
3797 /// stays at 3 (#1090). Whether 1 or 2 is the *right* score is a
3798 /// separate question — this pins current behaviour, and the
3799 /// per-lambda surcharge itself is under review in #1150.
3800 #[cfg(feature = "python")]
3801 #[test]
3802 fn python_boolean_in_expression_list_under_lambda() {
3803 use crate::test_support::metrics_verbatim;
3804
3805 for (route, source) in [
3806 ("parenthesised yield", "k = lambda q: (yield a and b, c)\n"),
3807 (
3808 "f-string interpolation",
3809 "m = lambda q: f\"{a and b, c}\"\n",
3810 ),
3811 ] {
3812 let metrics = metrics_verbatim(
3813 crate::LANG::Python,
3814 source.as_bytes(),
3815 MetricsOptions::default(),
3816 );
3817
3818 assert_eq!(
3819 metrics.cognitive.cognitive_sum(),
3820 1,
3821 "{route}: +1 boolean sequence only — the `expression_list` \
3822 stops the enclosing-lambda walk before it reaches the `lambda`"
3823 );
3824 }
3825 }
3826
3827 #[cfg(feature = "python")]
3828 #[test]
3829 fn python_nested_functions_lambdas() {
3830 check_metrics::<PythonParser>(
3831 "def f(a, b):
3832 def foo(a):
3833 if a: # +2 (+1 nesting)
3834 return 1
3835 # +3 (+1 for boolean sequence +2 for lambda nesting)
3836 bar = lambda a: lambda b: b or True or True
3837 return bar(foo(a))(a)",
3838 "foo.py",
3839 |metric| {
3840 // 2 functions + 2 lambdas = 4
3841 insta::assert_json_snapshot!(
3842 metric.cognitive,
3843 @r#"
3844 {
3845 "sum": 5,
3846 "value": 0,
3847 "average": 1.25,
3848 "min": 0,
3849 "max": 3
3850 }
3851 "#
3852 );
3853 },
3854 );
3855 }
3856
3857 /// #1149: a `def` nested inside a conditional is scored against its
3858 /// own depth, not the enclosing function's.
3859 ///
3860 /// Python was the only language with a syntactic function-definition
3861 /// node that never reset `nesting.conditional` at the boundary, so
3862 /// `inner` charged base(1) + inherited-conditional(1) +
3863 /// function-depth(1) = 3 where every sibling charges base(1) +
3864 /// function-depth(1) = 2. `python_nested_functions_lambdas` missed it
3865 /// because its nested `def` sits at function top level, where
3866 /// `conditional` is already 0.
3867 ///
3868 /// The Java companion is the byte-equivalent construct — Java has no
3869 /// local function, so a method reaches the inside of an `if` only
3870 /// through a class body declared there — and pins the book's
3871 /// "byte-equivalent constructs therefore score identically across
3872 /// languages" claim with a test rather than prose.
3873 ///
3874 /// Both fixtures nest the definition **two** conditionals deep, not
3875 /// one. At one level the Java assertion cannot discriminate: reset +
3876 /// depth-surcharge and no-reset + no-surcharge both yield 2, so
3877 /// deleting both lines from `cognitive/java.rs` leaves it green. At
3878 /// two levels the correct answer stays 2 while an unreset
3879 /// implementation gives 4 (Python, which also bumps depth) or 3
3880 /// (depth dropped as well).
3881 #[cfg(all(feature = "java", feature = "python"))]
3882 #[test]
3883 fn python_nested_def_inside_conditional_scores_like_java() {
3884 #[cfg(any(feature = "java", feature = "python"))]
3885 fn cognitive_of(space: &FuncSpace, name: &str) -> u64 {
3886 function_space(space, name).metrics.cognitive.cognitive()
3887 }
3888
3889 check_func_space::<PythonParser, _>(
3890 "def outer(a, b, c):
3891 if a: # +1
3892 if b: # +2 (+1 nesting)
3893 def inner(c):
3894 if c: # +1 base, +1 function depth, +0 inherited
3895 return 1
3896 return inner",
3897 "nested.py",
3898 |space| {
3899 assert_eq!(cognitive_of(&space, "outer"), 3, "python outer");
3900 assert_eq!(cognitive_of(&space, "inner"), 2, "python inner");
3901 },
3902 );
3903
3904 check_func_space::<JavaParser, _>(
3905 "class N {
3906 int outer(boolean a, boolean b, boolean c) {
3907 if (a) { // +1
3908 if (b) { // +2 (+1 nesting)
3909 class I {
3910 int inner(boolean c) {
3911 if (c) { // +1 base, +1 function depth
3912 return 1;
3913 }
3914 return 0;
3915 }
3916 }
3917 }
3918 }
3919 return 0;
3920 }
3921 }",
3922 "N.java",
3923 |space| {
3924 assert_eq!(cognitive_of(&space, "outer"), 3, "java outer");
3925 assert_eq!(cognitive_of(&space, "inner"), 2, "java inner");
3926 },
3927 );
3928 }
3929
3930 #[cfg(feature = "python")]
3931 #[test]
3932 fn python_real_function() {
3933 check_metrics::<PythonParser>(
3934 "def process_raw_constant(constant, min_word_length):
3935 processed_words = []
3936 raw_camelcase_words = []
3937 for raw_word in re.findall(r'[a-z]+', constant): # +1
3938 word = raw_word.strip()
3939 if ( # +2 (+1 if and +1 nesting)
3940 len(word) >= min_word_length
3941 and not (word.startswith('-') or word.endswith('-')) # +2 operators
3942 ):
3943 if is_camel_case_word(word): # +3 (+1 if and +2 nesting)
3944 raw_camelcase_words.append(word)
3945 else: # +1 else
3946 processed_words.append(word.lower())
3947 return processed_words, raw_camelcase_words",
3948 "foo.py",
3949 |metric| {
3950 insta::assert_json_snapshot!(
3951 metric.cognitive,
3952 @r#"
3953 {
3954 "sum": 9,
3955 "value": 0,
3956 "average": 9.0,
3957 "min": 0,
3958 "max": 9
3959 }
3960 "#
3961 );
3962 },
3963 );
3964 }
3965
3966 #[cfg(feature = "rust")]
3967 #[test]
3968 fn rust_if_let_else_if_else() {
3969 check_metrics::<RustParser>(
3970 "pub fn create_usage_no_title(p: &Parser, used: &[&str]) -> String {
3971 debugln!(\"usage::create_usage_no_title;\");
3972 if let Some(u) = p.meta.usage_str { // +1
3973 String::from(&*u)
3974 } else if used.is_empty() { // +1
3975 create_help_usage(p, true)
3976 } else { // +1
3977 create_smart_usage(p, used)
3978 }
3979 }",
3980 "foo.rs",
3981 |metric| {
3982 insta::assert_json_snapshot!(
3983 metric.cognitive,
3984 @r#"
3985 {
3986 "sum": 3,
3987 "value": 0,
3988 "average": 3.0,
3989 "min": 0,
3990 "max": 3
3991 }
3992 "#
3993 );
3994 },
3995 );
3996 }
3997
3998 // A `let`-`else` is an `if let` that diverges instead of nesting the
3999 // happy path, so it scores what its `if let` twin scores: +1 plus the
4000 // current nesting, with a level for everything under it (#1548).
4001 //
4002 // Each `le_*` fn is paired with an `il_*` twin that spells the same
4003 // control flow with `if let`, and the expected values were measured
4004 // on the twins *before* the fix (the twins do not move). Before it,
4005 // the let-else paid a flat +1 through the shared `else` token arm:
4006 // `le_lp` 2, `le_d2` 4, `le_d3` 7, `le_two` 3, the closure 1 and
4007 // `le_in` 2. `le_in` pins that the `else` block is nested like the
4008 // twin's body (its inner `if` pays 2), `ie` that an `if … else` still
4009 // pays its `else`, and the closure rows that the lambda level reaches
4010 // the let-else. `value` is the fn's own score and `sum` folds in the
4011 // closure space it encloses.
4012 #[cfg(feature = "rust")]
4013 #[test]
4014 fn rust_let_else_scores_like_its_if_let_twin_1548() {
4015 let src = "fn le_lp(xs: &[Option<i32>]) {
4016 for y in xs {
4017 let Some(a) = y else { continue };
4018 k(a);
4019 }
4020}
4021fn il_lp(xs: &[Option<i32>]) {
4022 for y in xs {
4023 if let Some(a) = y { k(a); }
4024 }
4025}
4026fn le_top(x: i32) -> i32 {
4027 let Ok(a) = g(x) else { return 0 };
4028 a
4029}
4030fn il_top(x: i32) -> i32 {
4031 if let Ok(a) = g(x) { return a; }
4032 0
4033}
4034fn le_d2(xs: &[Vec<Option<i32>>]) {
4035 for y in xs {
4036 for z in y {
4037 let Some(a) = z else { continue };
4038 k(a);
4039 }
4040 }
4041}
4042fn il_d2(xs: &[Vec<Option<i32>>]) {
4043 for y in xs {
4044 for z in y {
4045 if let Some(a) = z { k(a); }
4046 }
4047 }
4048}
4049fn le_d3(xs: &[Vec<Option<i32>>], c: bool) {
4050 for y in xs {
4051 while c {
4052 for z in y {
4053 let Some(a) = z else { continue };
4054 k(a);
4055 }
4056 }
4057 }
4058}
4059fn il_d3(xs: &[Vec<Option<i32>>], c: bool) {
4060 for y in xs {
4061 while c {
4062 for z in y {
4063 if let Some(a) = z { k(a); }
4064 }
4065 }
4066 }
4067}
4068fn le_two(xs: &[Option<i32>]) {
4069 for y in xs {
4070 let Some(a) = y else { continue };
4071 let Ok(b) = g(a) else { continue };
4072 k(b);
4073 }
4074}
4075fn il_two(xs: &[Option<i32>]) {
4076 for y in xs {
4077 if let Some(a) = y { k(a); }
4078 if let Ok(b) = g(*y) { k(b); }
4079 }
4080}
4081fn le_cl() {
4082 let c = |y: Option<i32>| {
4083 let Some(a) = y else { return 0 };
4084 a
4085 };
4086 k(c);
4087}
4088fn il_cl() {
4089 let c = |y: Option<i32>| {
4090 if let Some(a) = y { return a; }
4091 0
4092 };
4093 k(c);
4094}
4095fn le_in(x: Option<i32>, c: bool) -> i32 {
4096 let Some(a) = x else {
4097 if c { return 1; }
4098 return 0;
4099 };
4100 a
4101}
4102fn il_in(x: Option<i32>, c: bool) -> i32 {
4103 if let Some(a) = x {
4104 if c { return a; }
4105 }
4106 0
4107}
4108fn ie(x: bool) -> i32 {
4109 if x { 1 } else { 0 }
4110}
4111";
4112 // (let-else fn, if-let twin, value, sum)
4113 let expected: &[(&str, &str, u64, u64)] = &[
4114 ("le_lp", "il_lp", 3, 3),
4115 ("le_top", "il_top", 1, 1),
4116 ("le_d2", "il_d2", 6, 6),
4117 ("le_d3", "il_d3", 10, 10),
4118 ("le_two", "il_two", 5, 5),
4119 ("le_cl", "il_cl", 0, 2),
4120 ("le_in", "il_in", 3, 3),
4121 ("ie", "ie", 2, 2),
4122 ];
4123 check_func_space::<RustParser, _>(src, "foo.rs", |space| {
4124 assert_eq!(space.spaces.len(), 15, "fixture members");
4125 let measured = |n: &str| {
4126 let c = &child_space(&space, n).metrics.cognitive;
4127 (c.cognitive(), c.cognitive_sum())
4128 };
4129 for &(name, twin, value, sum) in expected {
4130 assert_eq!(measured(twin), (value, sum), "{twin}");
4131 assert_eq!(measured(name), (value, sum), "{name}");
4132 }
4133 for name in ["le_cl", "il_cl"] {
4134 let closure = &child_space(&space, name).spaces;
4135 assert_eq!(closure.len(), 1, "{name}'s closure space");
4136 assert_eq!(
4137 closure[0].metrics.cognitive.cognitive(),
4138 2,
4139 "{name} closure"
4140 );
4141 }
4142 });
4143 }
4144
4145 #[cfg(feature = "typescript")]
4146 #[test]
4147 fn typescript_if_else_if_else() {
4148 check_metrics::<TypescriptParser>(
4149 "function foo() {
4150 if (this._closed) return Promise.resolve(); // +1
4151 if (this._tempDirectory) { // +1
4152 this.kill();
4153 } else if (this.connection) { // +1
4154 this.kill();
4155 } else { // +1
4156 throw new Error(`Error`);
4157 }
4158 helper.removeEventListeners(this._listeners);
4159 return this._processClosing;
4160 }",
4161 "foo.ts",
4162 |metric| {
4163 insta::assert_json_snapshot!(
4164 metric.cognitive,
4165 @r#"
4166 {
4167 "sum": 4,
4168 "value": 0,
4169 "average": 4.0,
4170 "min": 0,
4171 "max": 4
4172 }
4173 "#
4174 );
4175 },
4176 );
4177 }
4178
4179 #[cfg(feature = "java")]
4180 #[test]
4181 fn java_no_cognitive() {
4182 check_metrics::<JavaParser>("int a = 42;", "foo.java", |metric| {
4183 insta::assert_json_snapshot!(
4184 metric.cognitive,
4185 @r#"
4186 {
4187 "sum": 0,
4188 "value": 0,
4189 "average": 0.0,
4190 "min": 0,
4191 "max": 0
4192 }
4193 "#
4194 );
4195 });
4196 }
4197
4198 #[cfg(feature = "java")]
4199 #[test]
4200 fn java_single_branch_function() {
4201 check_metrics::<JavaParser>(
4202 "class X {
4203 public static void print(boolean a){
4204 if(a){ // +1
4205 System.out.println(\"test1\");
4206 }
4207 }
4208 }",
4209 "foo.java",
4210 |metric| {
4211 insta::assert_json_snapshot!(
4212 metric.cognitive,
4213 @r#"
4214 {
4215 "sum": 1,
4216 "value": 0,
4217 "average": 1.0,
4218 "min": 0,
4219 "max": 1
4220 }
4221 "#
4222 );
4223 },
4224 );
4225 }
4226
4227 #[cfg(feature = "java")]
4228 #[test]
4229 fn java_multiple_branch_function() {
4230 check_metrics::<JavaParser>(
4231 "class X {
4232 public static void print(boolean a, boolean b){
4233 if(a){ // +1
4234 System.out.println(\"test1\");
4235 }
4236 if(b){ // +1
4237 System.out.println(\"test2\");
4238 }
4239 else { // +1
4240 System.out.println(\"test3\");
4241 }
4242 }
4243 }",
4244 "foo.java",
4245 |metric| {
4246 insta::assert_json_snapshot!(
4247 metric.cognitive,
4248 @r#"
4249 {
4250 "sum": 3,
4251 "value": 0,
4252 "average": 3.0,
4253 "min": 0,
4254 "max": 3
4255 }
4256 "#
4257 );
4258 },
4259 );
4260 }
4261
4262 #[cfg(feature = "java")]
4263 #[test]
4264 fn java_compound_conditions() {
4265 check_metrics::<JavaParser>(
4266 "class X {
4267 public static void print(boolean a, boolean b, boolean c, boolean d){
4268 if(a && b){ // +2 (+1 &&)
4269 System.out.println(\"test1\");
4270 }
4271 if(c && d){ // +2 (+1 &&)
4272 System.out.println(\"test2\");
4273 }
4274 }
4275 }",
4276 "foo.java",
4277 |metric| {
4278 insta::assert_json_snapshot!(
4279 metric.cognitive,
4280 @r#"
4281 {
4282 "sum": 4,
4283 "value": 0,
4284 "average": 4.0,
4285 "min": 0,
4286 "max": 4
4287 }
4288 "#
4289 );
4290 },
4291 );
4292 }
4293
4294 #[cfg(feature = "java")]
4295 #[test]
4296 fn java_switch_statement() {
4297 check_metrics::<JavaParser>(
4298 "class X {
4299 public static void print(boolean a, boolean b, boolean c, boolean d){
4300 switch(expr){ //+1
4301 case 1:
4302 System.out.println(\"test1\");
4303 break;
4304 case 2:
4305 System.out.println(\"test2\");
4306 break;
4307 default:
4308 System.out.println(\"test\");
4309 }
4310 }
4311 }",
4312 "foo.java",
4313 |metric| {
4314 insta::assert_json_snapshot!(
4315 metric.cognitive,
4316 @r#"
4317 {
4318 "sum": 1,
4319 "value": 0,
4320 "average": 1.0,
4321 "min": 0,
4322 "max": 1
4323 }
4324 "#
4325 );
4326 },
4327 );
4328 }
4329
4330 #[cfg(feature = "java")]
4331 #[test]
4332 fn java_switch_expression() {
4333 check_metrics::<JavaParser>(
4334 "class X {
4335 public static void print(boolean a, boolean b, boolean c, boolean d){
4336 switch(expr){ // +1
4337 case 1 -> System.out.println(\"test1\");
4338 case 2 -> System.out.println(\"test2\");
4339 default -> System.out.println(\"test\");
4340 }
4341 }
4342 }",
4343 "foo.java",
4344 |metric| {
4345 insta::assert_json_snapshot!(
4346 metric.cognitive,
4347 @r#"
4348 {
4349 "sum": 1,
4350 "value": 0,
4351 "average": 1.0,
4352 "min": 0,
4353 "max": 1
4354 }
4355 "#
4356 );
4357 },
4358 );
4359 }
4360
4361 #[cfg(feature = "java")]
4362 #[test]
4363 fn java_not_booleans() {
4364 // `!` does not break boolean sequences (issue #392): pre-order
4365 // visits the outer `&&` BinaryExpression first; the inner `&&`
4366 // lies within that span and is a continuation, not a new
4367 // sequence.
4368 check_metrics::<JavaParser>(
4369 "class X {
4370 public static void print(boolean a, boolean b, boolean c, boolean d){
4371 if (a && !(b && c)) { // +2 (+1 if, +1 outer &&; inner && continues)
4372 printf(\"test\");
4373 }
4374 }
4375 }",
4376 "foo.java",
4377 |metric| {
4378 insta::assert_json_snapshot!(
4379 metric.cognitive,
4380 @r#"
4381 {
4382 "sum": 2,
4383 "value": 0,
4384 "average": 2.0,
4385 "min": 0,
4386 "max": 2
4387 }
4388 "#
4389 );
4390 },
4391 );
4392 }
4393
4394 #[cfg(feature = "java")]
4395 #[test]
4396 fn java_enhanced_for_statement() {
4397 check_metrics::<JavaParser>(
4398 "class X {
4399 public static int sum(int[] xs) {
4400 int s = 0;
4401 for (int x : xs) { // +1
4402 s += x;
4403 }
4404 return s;
4405 }
4406 }",
4407 "foo.java",
4408 |metric| {
4409 // Java's enhanced-for `for (T x : c)` parses as
4410 // `enhanced_for_statement`; it is a control-flow construct
4411 // and counts the same as a classic `for_statement` → +1.
4412 assert_eq!(metric.cognitive.cognitive_sum(), 1);
4413 assert_eq!(metric.cognitive.cognitive_max(), 1);
4414 insta::assert_json_snapshot!(
4415 metric.cognitive,
4416 @r#"
4417 {
4418 "sum": 1,
4419 "value": 0,
4420 "average": 1.0,
4421 "min": 0,
4422 "max": 1
4423 }
4424 "#
4425 );
4426 },
4427 );
4428 }
4429
4430 #[cfg(feature = "java")]
4431 #[test]
4432 fn java_nested_enhanced_for_statement() {
4433 check_metrics::<JavaParser>(
4434 "class X {
4435 public static void f(int[][] xss) {
4436 for (int[] xs : xss) { // +1
4437 for (int x : xs) { // +2 (nesting = 1)
4438 g(x);
4439 }
4440 }
4441 }
4442 }",
4443 "foo.java",
4444 |metric| {
4445 // Nested enhanced-fors compound by nesting, matching the
4446 // behaviour of nested classic `for` loops: 1 + 2 = 3.
4447 assert_eq!(metric.cognitive.cognitive_sum(), 3);
4448 assert_eq!(metric.cognitive.cognitive_max(), 3);
4449 insta::assert_json_snapshot!(
4450 metric.cognitive,
4451 @r#"
4452 {
4453 "sum": 3,
4454 "value": 0,
4455 "average": 3.0,
4456 "min": 0,
4457 "max": 3
4458 }
4459 "#
4460 );
4461 },
4462 );
4463 }
4464
4465 #[cfg(feature = "java")]
4466 #[test]
4467 fn java_ternary() {
4468 // Java's ternary `?:` (grammar `ternary_expression`) is a
4469 // conditional construct: +1 base + nesting, matching the
4470 // SonarSource Cognitive Complexity §2 rule and the C++/JS
4471 // siblings.
4472 check_metrics::<JavaParser>(
4473 "class X {
4474 public static boolean check(int a) {
4475 return a > 0 ? true : false; // +1
4476 }
4477 }",
4478 "foo.java",
4479 |metric| {
4480 assert_eq!(metric.cognitive.cognitive_sum(), 1);
4481 assert_eq!(metric.cognitive.cognitive_max(), 1);
4482 insta::assert_json_snapshot!(
4483 metric.cognitive,
4484 @r#"
4485 {
4486 "sum": 1,
4487 "value": 0,
4488 "average": 1.0,
4489 "min": 0,
4490 "max": 1
4491 }
4492 "#
4493 );
4494 },
4495 );
4496 }
4497
4498 #[cfg(feature = "java")]
4499 #[test]
4500 fn java_nested_ternary() {
4501 // Nested ternaries inside an `if` block compound by nesting,
4502 // matching the C++ regression test for issue #172.
4503 // expected: if (+1, nesting=0) + outer ternary (+1+1=+2,
4504 // nesting=1) + inner ternary (+1+2=+3, nesting=2) = 6.
4505 check_metrics::<JavaParser>(
4506 "class X {
4507 public static String classify(int a, int b) {
4508 if (a > 0) { // +1
4509 return b > 0 ? (b > 10 ? \"big\" : \"small\") : \"neg\"; // +2, +3
4510 }
4511 return \"zero\";
4512 }
4513 }",
4514 "foo.java",
4515 |metric| {
4516 assert_eq!(metric.cognitive.cognitive_sum(), 6);
4517 assert_eq!(metric.cognitive.cognitive_max(), 6);
4518 insta::assert_json_snapshot!(
4519 metric.cognitive,
4520 @r#"
4521 {
4522 "sum": 6,
4523 "value": 0,
4524 "average": 6.0,
4525 "min": 0,
4526 "max": 6
4527 }
4528 "#
4529 );
4530 },
4531 );
4532 }
4533
4534 #[cfg(feature = "java")]
4535 #[test]
4536 fn java_nested_method_resets_nesting_and_adds_depth() {
4537 // Regression for #696: a local-class method declared two `if`s deep
4538 // inside an outer method must NOT inherit the enclosing nesting. The
4539 // method-declaration boundary resets nesting to 0 and bumps the
4540 // function-depth surcharge by 1 (it is nested inside `outer`).
4541 //
4542 // expected: outer `if` (+1, nesting=0) + inner `if` (+2, nesting=1)
4543 // + Local.f's `if` (+1 base + 1 depth = +2, nesting=0, depth=1) = 5.
4544 // Before the fix, `f` inherited nesting=2 from the two enclosing
4545 // `if`s, scoring its inner `if` at nesting 2 (+3) for a sum of 6.
4546 // The two-deep nesting is load-bearing: at one level deep the
4547 // inherited nesting (1) coincidentally equals the depth bump (1) so
4548 // the bug is invisible.
4549 check_metrics::<JavaParser>(
4550 "class Outer {
4551 void outer(boolean a) {
4552 if (a) {
4553 if (a) {
4554 class Local {
4555 void f(boolean b) {
4556 if (b) { g(); }
4557 }
4558 }
4559 }
4560 }
4561 }
4562 }",
4563 "foo.java",
4564 |metric| {
4565 assert_eq!(metric.cognitive.cognitive_sum(), 5);
4566 assert_eq!(metric.cognitive.cognitive_max(), 3);
4567 },
4568 );
4569 }
4570
4571 /// Regression for #1160: a record's compact constructor is its own
4572 /// grammar kind (`compact_constructor_declaration`), which was absent
4573 /// from `is_func`, `get_space_kind`, and the boundary arm in
4574 /// `cognitive/java.rs`. It therefore opened no function space and its
4575 /// control flow was charged to the enclosing class, so `bca check`
4576 /// could never flag one however complex it got.
4577 ///
4578 /// expected: each `if` is +1 at nesting 0, so `function R` scores 2
4579 /// while `class R` scores 0 of its own. Both halves are asserted:
4580 /// checking only the new space would still pass if the class kept a
4581 /// duplicate count of the same two branches.
4582 #[cfg(feature = "java")]
4583 #[test]
4584 fn java_record_compact_constructor_opens_function_space() {
4585 check_func_space::<JavaParser, _>(
4586 "record R(int a, int b) {
4587 R {
4588 if (a < 0) { throw new IllegalArgumentException(); }
4589 if (b < 0) { throw new IllegalArgumentException(); }
4590 }
4591 int sum() { return a + b; }
4592 }",
4593 "R.java",
4594 |space| {
4595 let class = child_space(&space, "R");
4596 assert_eq!(class.kind, SpaceKind::Class, "record opens a class space");
4597 assert_eq!(class.metrics.cognitive.cognitive(), 0, "class R own score");
4598 // Also pins the space name: the compact form carries a
4599 // `name` field holding the record's simple name, so the
4600 // default `get_func_space_name` reports `R` rather than
4601 // `<anonymous>`.
4602 assert_eq!(
4603 function_space(&space, "R").metrics.cognitive.cognitive(),
4604 2,
4605 "compact constructor own score",
4606 );
4607 },
4608 );
4609 }
4610
4611 /// The compact constructor is a *function boundary*, not merely a
4612 /// space: #1160 added it to the arm that resets structural nesting and
4613 /// to the `stops` set behind the function-depth surcharge. The
4614 /// reproducer above cannot see either — at nesting 0 with no enclosing
4615 /// function both lines are no-ops — so each gets a fixture that only
4616 /// it can satisfy.
4617 ///
4618 /// The first nests a *local record* (Java 16+) two conditionals deep,
4619 /// per the two-level rule in
4620 /// `java_nested_method_resets_nesting_and_adds_depth`: at one level
4621 /// the reset and the surcharge cancel out.
4622 /// expected: outer `if` +1, inner `if` +2, the compact constructor's
4623 /// `if` +1 base +1 depth (it is lexically inside `m`) = 5. Without the
4624 /// reset the last `if` inherits nesting 2 and scores +3, for 6.
4625 ///
4626 /// The second inverts the nesting: a local class method inside a
4627 /// compact constructor. Only the `stops` entry makes the constructor
4628 /// count as `f`'s enclosing function.
4629 /// expected: `f`'s `if` is +1 base +1 depth = 2. Without the `stops`
4630 /// entry the surcharge is 0 and it scores 1.
4631 #[cfg(feature = "java")]
4632 #[test]
4633 fn java_record_compact_constructor_is_a_function_boundary() {
4634 check_func_space::<JavaParser, _>(
4635 "class C {
4636 void m(boolean f) {
4637 if (f) {
4638 if (f) {
4639 record R(int a) {
4640 R {
4641 if (a < 0) { throw new IllegalArgumentException(); }
4642 }
4643 }
4644 }
4645 }
4646 }
4647 }",
4648 "C.java",
4649 |space| {
4650 assert_eq!(
4651 space.metrics.cognitive.cognitive_sum(),
4652 5,
4653 "local record's compact constructor restarts nesting",
4654 );
4655 assert_eq!(
4656 function_space(&space, "R").metrics.cognitive.cognitive(),
4657 2,
4658 "compact constructor: +1 base, +1 function depth",
4659 );
4660 },
4661 );
4662
4663 check_func_space::<JavaParser, _>(
4664 "record R(int a) {
4665 R {
4666 class L {
4667 void f(boolean b) {
4668 if (b) { g(); }
4669 }
4670 }
4671 }
4672 }",
4673 "R.java",
4674 |space| {
4675 assert_eq!(
4676 function_space(&space, "f").metrics.cognitive.cognitive(),
4677 2,
4678 "a compact constructor is `f`'s enclosing function",
4679 );
4680 },
4681 );
4682 }
4683
4684 #[cfg(feature = "java")]
4685 #[test]
4686 fn java_labeled_break_continue() {
4687 // Per SonarSource Cognitive Complexity §B2 (issue #225), labeled
4688 // `break LABEL` and `continue LABEL` each add +1 because they break
4689 // structured control flow. Mirrors `go_labeled_break_continue` and
4690 // `rust_break_continue_labeled`.
4691 // expected: outer for (+1, nesting=0) + inner for (+2, nesting=1)
4692 // + if (+3, nesting=2) + continue outer (+1)
4693 // + if (+3, nesting=2) + break outer (+1) = 11.
4694 check_metrics::<JavaParser>(
4695 "class X {
4696 void scan(int[][] m) {
4697 outer:
4698 for (int i = 0; i < m.length; i++) { // +1
4699 for (int j = 0; j < m[i].length; j++) { // +2
4700 if (m[i][j] < 0) continue outer; // +3, +1
4701 if (m[i][j] > 100) break outer; // +3, +1
4702 }
4703 }
4704 }
4705 }",
4706 "foo.java",
4707 |metric| {
4708 assert_eq!(metric.cognitive.cognitive_sum(), 11);
4709 assert_eq!(metric.cognitive.cognitive_max(), 11);
4710 insta::assert_json_snapshot!(
4711 metric.cognitive,
4712 @r#"
4713 {
4714 "sum": 11,
4715 "value": 0,
4716 "average": 11.0,
4717 "min": 0,
4718 "max": 11
4719 }
4720 "#
4721 );
4722 },
4723 );
4724 }
4725
4726 #[cfg(feature = "java")]
4727 #[test]
4728 fn java_unlabeled_break_continue_not_counted() {
4729 // Negative test for issue #225: plain `break;` / `continue;` are
4730 // *not* unstructured jumps under SonarSource Cognitive Complexity
4731 // §B2 and must add 0. Only the surrounding `for` + `if` contribute.
4732 // expected: for (+1) + if (+2) + if (+2) = 5.
4733 check_metrics::<JavaParser>(
4734 "class X {
4735 void scan(int[] m) {
4736 for (int i = 0; i < m.length; i++) { // +1
4737 if (m[i] < 0) continue; // +2, +0
4738 if (m[i] > 100) break; // +2, +0
4739 }
4740 }
4741 }",
4742 "foo.java",
4743 |metric| {
4744 assert_eq!(metric.cognitive.cognitive_sum(), 5);
4745 assert_eq!(metric.cognitive.cognitive_max(), 5);
4746 insta::assert_json_snapshot!(
4747 metric.cognitive,
4748 @r#"
4749 {
4750 "sum": 5,
4751 "value": 0,
4752 "average": 5.0,
4753 "min": 0,
4754 "max": 5
4755 }
4756 "#
4757 );
4758 },
4759 );
4760 }
4761
4762 #[cfg(feature = "csharp")]
4763 #[test]
4764 fn csharp_no_cognitive() {
4765 check_metrics::<CsharpParser>("int a = 42;", "foo.cs", |metric| {
4766 insta::assert_json_snapshot!(
4767 metric.cognitive,
4768 @r#"
4769 {
4770 "sum": 0,
4771 "value": 0,
4772 "average": 0.0,
4773 "min": 0,
4774 "max": 0
4775 }
4776 "#
4777 );
4778 });
4779 }
4780
4781 #[cfg(feature = "csharp")]
4782 #[test]
4783 fn csharp_single_branch_function() {
4784 check_metrics::<CsharpParser>(
4785 "class X {
4786 public static void Print(bool a) {
4787 if (a) {
4788 System.Console.WriteLine(\"test1\");
4789 }
4790 }
4791 }",
4792 "foo.cs",
4793 |metric| {
4794 // Single `if` at nesting 0 → +1.
4795 assert_eq!(metric.cognitive.cognitive_sum(), 1);
4796 assert_eq!(metric.cognitive.cognitive_max(), 1);
4797 insta::assert_json_snapshot!(metric.cognitive);
4798 },
4799 );
4800 }
4801
4802 #[cfg(feature = "csharp")]
4803 #[test]
4804 fn csharp_multiple_branch_function() {
4805 check_metrics::<CsharpParser>(
4806 "class X {
4807 public static void Print(bool a, bool b) {
4808 if (a) {
4809 System.Console.WriteLine(\"test1\");
4810 }
4811 if (b) {
4812 System.Console.WriteLine(\"test2\");
4813 } else {
4814 System.Console.WriteLine(\"test3\");
4815 }
4816 }
4817 }",
4818 "foo.cs",
4819 |metric| {
4820 // First `if` +1, second `if` +1, `else` +1 → 3.
4821 assert_eq!(metric.cognitive.cognitive_sum(), 3);
4822 assert_eq!(metric.cognitive.cognitive_max(), 3);
4823 insta::assert_json_snapshot!(metric.cognitive);
4824 },
4825 );
4826 }
4827
4828 #[cfg(feature = "csharp")]
4829 #[test]
4830 fn csharp_compound_conditions() {
4831 check_metrics::<CsharpParser>(
4832 "class X {
4833 public static void Print(bool a, bool b, bool c, bool d) {
4834 if (a && b) {
4835 System.Console.WriteLine(\"test1\");
4836 }
4837 if (c && d) {
4838 System.Console.WriteLine(\"test2\");
4839 }
4840 }
4841 }",
4842 "foo.cs",
4843 |metric| {
4844 // Two ifs (+1 each) + two `&&` (+1 each, fresh chain per if) = 4.
4845 assert_eq!(metric.cognitive.cognitive_sum(), 4);
4846 assert_eq!(metric.cognitive.cognitive_max(), 4);
4847 insta::assert_json_snapshot!(metric.cognitive);
4848 },
4849 );
4850 }
4851
4852 #[cfg(feature = "csharp")]
4853 #[test]
4854 fn csharp_switch_statement() {
4855 check_metrics::<CsharpParser>(
4856 "class X {
4857 public static void Print(int expr) {
4858 switch (expr) {
4859 case 1:
4860 System.Console.WriteLine(\"test1\");
4861 break;
4862 case 2:
4863 System.Console.WriteLine(\"test2\");
4864 break;
4865 default:
4866 System.Console.WriteLine(\"test\");
4867 break;
4868 }
4869 }
4870 }",
4871 "foo.cs",
4872 |metric| {
4873 // Single `switch` +1; cases / default do not increment.
4874 assert_eq!(metric.cognitive.cognitive_sum(), 1);
4875 assert_eq!(metric.cognitive.cognitive_max(), 1);
4876 insta::assert_json_snapshot!(metric.cognitive);
4877 },
4878 );
4879 }
4880
4881 #[cfg(feature = "csharp")]
4882 #[test]
4883 fn csharp_switch_expression() {
4884 check_metrics::<CsharpParser>(
4885 "class X {
4886 public static string Name(int expr) =>
4887 expr switch {
4888 1 => \"one\",
4889 2 => \"two\",
4890 _ => \"other\"
4891 };
4892 }",
4893 "foo.cs",
4894 |metric| {
4895 // `switch` expression +1; arms do not increment.
4896 assert_eq!(metric.cognitive.cognitive_sum(), 1);
4897 assert_eq!(metric.cognitive.cognitive_max(), 1);
4898 insta::assert_json_snapshot!(metric.cognitive);
4899 },
4900 );
4901 }
4902
4903 #[cfg(feature = "csharp")]
4904 #[test]
4905 fn csharp_not_booleans() {
4906 // `!` does not break boolean sequences (issue #392): pre-order
4907 // visits the outer `&&` BinaryExpression first, so the inner
4908 // `&&` lies within its span and is a continuation.
4909 check_metrics::<CsharpParser>(
4910 "class X {
4911 public static void Print(bool a, bool b, bool c) {
4912 if (a && !(b && c)) {
4913 System.Console.WriteLine(\"test\");
4914 }
4915 }
4916 }",
4917 "foo.cs",
4918 |metric| {
4919 // `if` +1, outer `&&` +1, inner `&&` continues outer span → 2.
4920 assert_eq!(metric.cognitive.cognitive_sum(), 2);
4921 assert_eq!(metric.cognitive.cognitive_max(), 2);
4922 insta::assert_json_snapshot!(metric.cognitive);
4923 },
4924 );
4925 }
4926
4927 #[cfg(feature = "csharp")]
4928 #[test]
4929 fn csharp_ternary() {
4930 // C#'s ternary `?:` (grammar `conditional_expression`) is a
4931 // conditional construct: +1 base + nesting. Regression test for
4932 // issue #224.
4933 check_metrics::<CsharpParser>(
4934 "class X {
4935 public static bool Check(int a) {
4936 return a > 0 ? true : false; // +1
4937 }
4938 }",
4939 "foo.cs",
4940 |metric| {
4941 assert_eq!(metric.cognitive.cognitive_sum(), 1);
4942 assert_eq!(metric.cognitive.cognitive_max(), 1);
4943 insta::assert_json_snapshot!(
4944 metric.cognitive,
4945 @r#"
4946 {
4947 "sum": 1,
4948 "value": 0,
4949 "average": 1.0,
4950 "min": 0,
4951 "max": 1
4952 }
4953 "#
4954 );
4955 },
4956 );
4957 }
4958
4959 #[cfg(feature = "csharp")]
4960 #[test]
4961 fn csharp_nested_ternary() {
4962 // Nested ternaries inside an `if` compound by nesting (mirrors
4963 // the C++ regression test for #172).
4964 // expected: if (+1) + outer ternary (+2, nesting=1) + inner
4965 // ternary (+3, nesting=2) = 6.
4966 check_metrics::<CsharpParser>(
4967 "class X {
4968 public static string Classify(int a, int b) {
4969 if (a > 0) { // +1
4970 return b > 0 ? (b > 10 ? \"big\" : \"small\") : \"neg\"; // +2, +3
4971 }
4972 return \"zero\";
4973 }
4974 }",
4975 "foo.cs",
4976 |metric| {
4977 assert_eq!(metric.cognitive.cognitive_sum(), 6);
4978 assert_eq!(metric.cognitive.cognitive_max(), 6);
4979 insta::assert_json_snapshot!(
4980 metric.cognitive,
4981 @r#"
4982 {
4983 "sum": 6,
4984 "value": 0,
4985 "average": 6.0,
4986 "min": 0,
4987 "max": 6
4988 }
4989 "#
4990 );
4991 },
4992 );
4993 }
4994
4995 #[cfg(feature = "csharp")]
4996 #[test]
4997 fn csharp_local_function_in_if_does_not_inherit_nesting() {
4998 // Regression for #696 (the acute C# case): a `local_function_statement`
4999 // declared two `if`s deep must reset nesting to 0 and gain a
5000 // function-depth surcharge — not inherit `nesting = 2` from the
5001 // enclosing `if`s. C# has dedicated `LocalFunctionStatement(342)` /
5002 // `LocalFunctionDeclaration(343)` nodes that previously went
5003 // unhandled by the cognitive walker.
5004 //
5005 // expected: outer `if` (+1, nesting=0) + inner `if` (+2, nesting=1)
5006 // + Local's `if` (+1 base + 1 depth = +2, nesting=0, depth=1) = 5.
5007 // Before the fix, `Local` inherited nesting=2, scoring its inner
5008 // `if` at nesting 2 (+3) for a sum of 6. The two-deep nesting is
5009 // load-bearing: one level deep, the inherited nesting (1)
5010 // coincidentally equals the depth bump (1) and the bug is invisible.
5011 check_metrics::<CsharpParser>(
5012 "class C {
5013 void Outer(bool flag) {
5014 if (flag) {
5015 if (flag) {
5016 void Local() {
5017 if (flag) {
5018 System.Console.WriteLine(\"x\");
5019 }
5020 }
5021 Local();
5022 }
5023 }
5024 }
5025 }",
5026 "foo.cs",
5027 |metric| {
5028 assert_eq!(metric.cognitive.cognitive_sum(), 5);
5029 assert_eq!(metric.cognitive.cognitive_max(), 3);
5030 },
5031 );
5032 }
5033
5034 #[cfg(feature = "csharp")]
5035 #[test]
5036 fn csharp_goto_statement() {
5037 // Per SonarSource Cognitive Complexity §B2 (issue #225), any `goto`
5038 // is an unstructured jump and adds +1. Mirrors C++'s `GotoStatement`
5039 // and Go's `GotoStatement` handling.
5040 // expected: if (+1, nesting=0) + goto neg (+1) = 2.
5041 check_metrics::<CsharpParser>(
5042 "class X {
5043 int Classify(int x) {
5044 if (x < 0) goto neg; // +1, +1
5045 return x;
5046 neg:
5047 return -x;
5048 }
5049 }",
5050 "foo.cs",
5051 |metric| {
5052 assert_eq!(metric.cognitive.cognitive_sum(), 2);
5053 assert_eq!(metric.cognitive.cognitive_max(), 2);
5054 insta::assert_json_snapshot!(
5055 metric.cognitive,
5056 @r#"
5057 {
5058 "sum": 2,
5059 "value": 0,
5060 "average": 2.0,
5061 "min": 0,
5062 "max": 2
5063 }
5064 "#
5065 );
5066 },
5067 );
5068 }
5069
5070 #[cfg(feature = "csharp")]
5071 #[test]
5072 fn csharp_goto_case_and_default() {
5073 // `goto case` and `goto default` inside a `switch` are also
5074 // unstructured jumps (+1 each) per SonarSource §B2.
5075 // expected: switch (+1, nesting=0) + goto case 2 (+1)
5076 // + goto default (+1) = 3.
5077 check_metrics::<CsharpParser>(
5078 "class X {
5079 int Walk(int x) {
5080 switch (x) { // +1
5081 case 1: goto case 2; // +1
5082 case 2: return 2;
5083 case 3: goto default; // +1
5084 default: return 0;
5085 }
5086 }
5087 }",
5088 "foo.cs",
5089 |metric| {
5090 assert_eq!(metric.cognitive.cognitive_sum(), 3);
5091 assert_eq!(metric.cognitive.cognitive_max(), 3);
5092 insta::assert_json_snapshot!(
5093 metric.cognitive,
5094 @r#"
5095 {
5096 "sum": 3,
5097 "value": 0,
5098 "average": 3.0,
5099 "min": 0,
5100 "max": 3
5101 }
5102 "#
5103 );
5104 },
5105 );
5106 }
5107
5108 #[cfg(feature = "csharp")]
5109 #[test]
5110 fn csharp_unlabeled_break_not_counted() {
5111 // Negative test for issue #225: C#'s grammar does not allow
5112 // labeled `break`/`continue` (those are syntactically rejected),
5113 // and plain `break;` / `continue;` are not unstructured jumps under
5114 // SonarSource §B2 — they must add 0. Only the `for` + `if`
5115 // contribute.
5116 // expected: for (+1) + if (+2) = 3.
5117 check_metrics::<CsharpParser>(
5118 "class X {
5119 void Scan(int[] m) {
5120 for (int i = 0; i < m.Length; i++) { // +1
5121 if (m[i] < 0) break; // +2, +0
5122 }
5123 }
5124 }",
5125 "foo.cs",
5126 |metric| {
5127 assert_eq!(metric.cognitive.cognitive_sum(), 3);
5128 assert_eq!(metric.cognitive.cognitive_max(), 3);
5129 insta::assert_json_snapshot!(
5130 metric.cognitive,
5131 @r#"
5132 {
5133 "sum": 3,
5134 "value": 0,
5135 "average": 3.0,
5136 "min": 0,
5137 "max": 3
5138 }
5139 "#
5140 );
5141 },
5142 );
5143 }
5144
5145 #[cfg(feature = "perl")]
5146 #[test]
5147 fn perl_no_cognitive() {
5148 check_metrics::<PerlParser>("my $a = 42;", "foo.pl", |metric| {
5149 insta::assert_json_snapshot!(metric.cognitive, @r#"
5150 {
5151 "sum": 0,
5152 "value": 0,
5153 "average": 0.0,
5154 "min": 0,
5155 "max": 0
5156 }
5157 "#);
5158 });
5159 }
5160
5161 #[cfg(feature = "perl")]
5162 #[test]
5163 fn perl_simple_function() {
5164 check_metrics::<PerlParser>(
5165 "sub f {
5166 return 1;
5167 }",
5168 "foo.pl",
5169 |metric| {
5170 insta::assert_json_snapshot!(metric.cognitive, @r#"
5171 {
5172 "sum": 0,
5173 "value": 0,
5174 "average": 0.0,
5175 "min": 0,
5176 "max": 0
5177 }
5178 "#);
5179 },
5180 );
5181 }
5182
5183 #[cfg(feature = "perl")]
5184 #[test]
5185 fn perl_sequence_same_booleans() {
5186 check_metrics::<PerlParser>(
5187 "sub f {
5188 if ($a && $b && $c) { # +1 if, +1 first &&-chain
5189 print 'x';
5190 }
5191 }",
5192 "foo.pl",
5193 |metric| {
5194 insta::assert_json_snapshot!(metric.cognitive, @r#"
5195 {
5196 "sum": 2,
5197 "value": 0,
5198 "average": 2.0,
5199 "min": 0,
5200 "max": 2
5201 }
5202 "#);
5203 },
5204 );
5205 }
5206
5207 #[cfg(feature = "perl")]
5208 #[test]
5209 fn perl_sequence_different_booleans() {
5210 check_metrics::<PerlParser>(
5211 "sub f {
5212 if ($a && $b || $c) { # +1 if, +1 &&, +1 ||
5213 print 'x';
5214 }
5215 }",
5216 "foo.pl",
5217 |metric| {
5218 insta::assert_json_snapshot!(metric.cognitive, @r#"
5219 {
5220 "sum": 3,
5221 "value": 0,
5222 "average": 3.0,
5223 "min": 0,
5224 "max": 3
5225 }
5226 "#);
5227 },
5228 );
5229 }
5230
5231 #[cfg(feature = "perl")]
5232 #[test]
5233 fn perl_compound_short_circuit_assignment_249() {
5234 // Regression for issue #249: `&&=`, `||=`, `//=` are compound
5235 // short-circuit assignments (e.g. `$x //= 1` ≡ `$x = $x // 1`)
5236 // and each carries one boolean-sequence decision. The grammar
5237 // exposes the operator token inside `binary_expression`, so the
5238 // existing arm picks them up once `compute_perl_booleans`
5239 // recognises the three `*EQ` tokens.
5240 check_metrics::<PerlParser>(
5241 "sub f {
5242 my ($x, $y, $z) = @_;
5243 $x ||= 1; # +1 (||=)
5244 $y &&= 2; # +1 (&&=)
5245 $z //= 3; # +1 (//=)
5246 return $x;
5247 }",
5248 "foo.pl",
5249 |metric| {
5250 assert_eq!(metric.cognitive.cognitive_sum(), 3);
5251 assert_eq!(metric.cognitive.cognitive_max(), 3);
5252 insta::assert_json_snapshot!(
5253 metric.cognitive,
5254 @r#"
5255 {
5256 "sum": 3,
5257 "value": 0,
5258 "average": 3.0,
5259 "min": 0,
5260 "max": 3
5261 }
5262 "#
5263 );
5264 },
5265 );
5266 }
5267
5268 #[cfg(feature = "perl")]
5269 #[test]
5270 fn perl_not_booleans() {
5271 // `!` does not break boolean sequences (issue #392): pre-order
5272 // visits the outer `&&` BinaryExpression first, so the inner
5273 // `&&` lies within its span and is a continuation.
5274 check_metrics::<PerlParser>(
5275 "sub f {
5276 if ($a && !($b && $c)) { # +1 if, +1 outer &&; inner && continues
5277 print 'x';
5278 }
5279 }",
5280 "foo.pl",
5281 |metric| {
5282 insta::assert_json_snapshot!(metric.cognitive, @r#"
5283 {
5284 "sum": 2,
5285 "value": 0,
5286 "average": 2.0,
5287 "min": 0,
5288 "max": 2
5289 }
5290 "#);
5291 },
5292 );
5293 }
5294
5295 #[cfg(feature = "perl")]
5296 #[test]
5297 fn perl_1_level_nesting() {
5298 check_metrics::<PerlParser>(
5299 "sub f {
5300 for my $i (1..3) { # +1 for
5301 if ($i % 2) { # +2 if (nested 1)
5302 print $i;
5303 }
5304 }
5305 }",
5306 "foo.pl",
5307 |metric| {
5308 insta::assert_json_snapshot!(metric.cognitive, @r#"
5309 {
5310 "sum": 3,
5311 "value": 0,
5312 "average": 3.0,
5313 "min": 0,
5314 "max": 3
5315 }
5316 "#);
5317 },
5318 );
5319 }
5320
5321 #[cfg(feature = "perl")]
5322 #[test]
5323 fn perl_2_level_nesting() {
5324 check_metrics::<PerlParser>(
5325 "sub f {
5326 for my $i (1..3) { # +1 for
5327 while ($n > 0) { # +2 while (nested 1)
5328 if ($n % 2) { # +3 if (nested 2)
5329 $n--;
5330 }
5331 }
5332 }
5333 }",
5334 "foo.pl",
5335 |metric| {
5336 insta::assert_json_snapshot!(metric.cognitive, @r#"
5337 {
5338 "sum": 6,
5339 "value": 0,
5340 "average": 6.0,
5341 "min": 0,
5342 "max": 6
5343 }
5344 "#);
5345 },
5346 );
5347 }
5348
5349 #[cfg(feature = "perl")]
5350 #[test]
5351 fn perl_break_continue() {
5352 // Perl's `last`/`next` are loop-control statements; per Sonar's
5353 // cognitive rule, they do not add complexity in their bare form
5354 // (the surrounding loop already contributes +1).
5355 check_metrics::<PerlParser>(
5356 "sub f {
5357 while (1) { # +1 while (nesting becomes 1)
5358 last if $done; # +2 postfix-if at nesting=1
5359 next; # +0 bare loop control
5360 }
5361 }",
5362 "foo.pl",
5363 |metric| {
5364 insta::assert_json_snapshot!(metric.cognitive, @r#"
5365 {
5366 "sum": 3,
5367 "value": 0,
5368 "average": 3.0,
5369 "min": 0,
5370 "max": 3
5371 }
5372 "#);
5373 },
5374 );
5375 }
5376
5377 #[cfg(feature = "perl")]
5378 #[test]
5379 fn perl_if_elsif_else() {
5380 check_metrics::<PerlParser>(
5381 "sub f {
5382 if ($x) { # +1 if
5383 print 'a';
5384 } elsif ($y) { # +1 elsif
5385 print 'b';
5386 } else { # +1 else
5387 print 'c';
5388 }
5389 }",
5390 "foo.pl",
5391 |metric| {
5392 insta::assert_json_snapshot!(metric.cognitive, @r#"
5393 {
5394 "sum": 3,
5395 "value": 0,
5396 "average": 3.0,
5397 "min": 0,
5398 "max": 3
5399 }
5400 "#);
5401 },
5402 );
5403 }
5404
5405 #[cfg(feature = "perl")]
5406 #[test]
5407 fn perl_function_definition_without_sub_depth() {
5408 // Regression: FunctionDefinitionWithoutSub must be a stop in
5409 // increment_function_depth so that a `sub` nested inside a `method`
5410 // block gets depth=1, making its structural elements cost +2 instead
5411 // of +1. `method name { }` (Method::Signatures style) is what
5412 // tree-sitter-perl parses as function_definition_without_sub.
5413 check_metrics::<PerlParser>(
5414 "method outer {
5415 sub inner {
5416 if (1) { } # +2 (depth=1)
5417 }
5418 }",
5419 "foo.pl",
5420 |metric| {
5421 insta::assert_json_snapshot!(metric.cognitive, @r#"
5422 {
5423 "sum": 2,
5424 "value": 0,
5425 "average": 1.0,
5426 "min": 0,
5427 "max": 2
5428 }
5429 "#);
5430 },
5431 );
5432 }
5433
5434 #[cfg(feature = "perl")]
5435 #[test]
5436 fn perl_goto_single_increment() {
5437 // Regression (#450): `goto LABEL;` parses as `goto_expression`
5438 // wrapping the anonymous `goto` keyword token. The walker visits
5439 // both, so matching `Goto | GotoExpression` counted the jump twice
5440 // (cognitive 2). Matching only `GotoExpression` scores the correct
5441 // +1.
5442 check_metrics::<PerlParser>("sub f { goto LABEL; LABEL: return; }", "foo.pl", |metric| {
5443 // expected: one `goto` jump (§B2) = +1
5444 assert_eq!(metric.cognitive.cognitive_sum(), 1);
5445 insta::assert_json_snapshot!(metric.cognitive, @r#"
5446 {
5447 "sum": 1,
5448 "value": 0,
5449 "average": 1.0,
5450 "min": 0,
5451 "max": 1
5452 }
5453 "#);
5454 });
5455 }
5456
5457 #[cfg(feature = "perl")]
5458 #[test]
5459 fn perl_labeled_loop_control() {
5460 // Regression (#450): the jump target of `last/next/redo LABEL` is
5461 // carried as an `Identifier` child of `loop_control_statement`
5462 // (`Label` is the loop-*definition* node `OUTER:`). Gating on
5463 // `Label` was a dead arm — labeled jumps scored +0. Each labeled
5464 // form is now +1 (§B2). The bare forms below stay +0.
5465 check_metrics::<PerlParser>(
5466 "OUTER: for my $i (@a) { # +1 for
5467 last OUTER; # +1 labeled
5468 next OUTER; # +1 labeled
5469 redo OUTER; # +1 labeled
5470 }",
5471 "foo.pl",
5472 |metric| {
5473 // expected: +1 for-loop, +1 each labeled last/next/redo = 4
5474 assert_eq!(metric.cognitive.cognitive_sum(), 4);
5475 insta::assert_json_snapshot!(metric.cognitive, @r#"
5476 {
5477 "sum": 4,
5478 "value": 4,
5479 "average": 4.0,
5480 "min": 4,
5481 "max": 4
5482 }
5483 "#);
5484 },
5485 );
5486 }
5487
5488 #[cfg(feature = "perl")]
5489 #[test]
5490 fn perl_bare_loop_control_zero() {
5491 // Bare `last;` / `next;` / `redo;` have no `Identifier` jump-target
5492 // child and must stay +0 — only the surrounding loop counts (§B2).
5493 check_metrics::<PerlParser>(
5494 "for my $i (@a) { # +1 for
5495 last; # +0
5496 next; # +0
5497 redo; # +0
5498 }",
5499 "foo.pl",
5500 |metric| {
5501 // expected: only the +1 for-loop; bare jumps add nothing
5502 assert_eq!(metric.cognitive.cognitive_sum(), 1);
5503 insta::assert_json_snapshot!(metric.cognitive, @r#"
5504 {
5505 "sum": 1,
5506 "value": 1,
5507 "average": 1.0,
5508 "min": 1,
5509 "max": 1
5510 }
5511 "#);
5512 },
5513 );
5514 }
5515
5516 #[cfg(feature = "typescript")]
5517 #[test]
5518 fn tsx_nested_if_for_with_booleans() {
5519 check_metrics::<TsxParser>(
5520 "function process(items: number[]) {
5521 if (items.length > 0) { // +1
5522 for (let i = 0; i < items.length; i++) { // +2 (nesting=1)
5523 if (items[i] > 0 && items[i] < 100) { // +3 (nesting=2) +1 (&&)
5524 console.log(items[i]);
5525 }
5526 }
5527 }
5528 }",
5529 "foo.tsx",
5530 |metric| {
5531 insta::assert_json_snapshot!(
5532 metric.cognitive,
5533 @r#"
5534 {
5535 "sum": 7,
5536 "value": 0,
5537 "average": 7.0,
5538 "min": 0,
5539 "max": 7
5540 }
5541 "#
5542 );
5543 },
5544 );
5545 }
5546
5547 #[cfg(feature = "typescript")]
5548 #[test]
5549 fn typescript_nested_if_with_boolean_sequence() {
5550 check_metrics::<TypescriptParser>(
5551 "function validate(input: string, strict: boolean): boolean {
5552 if (input.length > 0) { // +1
5553 if (strict && input.trim() === input) { // +2 (nesting=1) +1 (&&)
5554 return true;
5555 }
5556 }
5557 return false;
5558 }",
5559 "foo.ts",
5560 |metric| {
5561 insta::assert_json_snapshot!(
5562 metric.cognitive,
5563 @r#"
5564 {
5565 "sum": 4,
5566 "value": 0,
5567 "average": 4.0,
5568 "min": 0,
5569 "max": 4
5570 }
5571 "#
5572 );
5573 },
5574 );
5575 }
5576
5577 #[cfg(feature = "typescript")]
5578 #[test]
5579 fn typescript_try_catch_with_nesting() {
5580 check_metrics::<TypescriptParser>(
5581 "function fetchData(url: string): string {
5582 try {
5583 if (url.length === 0) { // +1
5584 throw new Error('empty url');
5585 }
5586 return url;
5587 } catch (e) { // +1
5588 if (e instanceof Error) { // +2 (nesting=1)
5589 return e.message;
5590 }
5591 return 'unknown error';
5592 }
5593 }",
5594 "foo.ts",
5595 |metric| {
5596 insta::assert_json_snapshot!(
5597 metric.cognitive,
5598 @r#"
5599 {
5600 "sum": 4,
5601 "value": 0,
5602 "average": 4.0,
5603 "min": 0,
5604 "max": 4
5605 }
5606 "#
5607 );
5608 },
5609 );
5610 }
5611
5612 #[cfg(feature = "kotlin")]
5613 #[test]
5614 fn kotlin_cognitive_control_flow() {
5615 check_metrics::<KotlinParser>(
5616 "fun process(x: Int, y: Int): String {
5617 if (x > 0) { // +1
5618 for (i in 1..x) { // +2 (nesting=1)
5619 if (i % 2 == 0) { // +3 (nesting=2)
5620 println(i)
5621 }
5622 }
5623 } else if (x < 0) { // +1 (else-if: flat +1 for else, if not counted as else-if)
5624 when (y) { // +2 (nesting=1)
5625 1 -> println(\"one\")
5626 2 -> println(\"two\")
5627 else -> println(\"other\")
5628 }
5629 } else { // +1
5630 while (y > 0) { // +2
5631 println(y)
5632 }
5633 }
5634 return if (x > y) \"big\" else \"small\"
5635 }",
5636 "foo.kt",
5637 |metric| {
5638 insta::assert_json_snapshot!(
5639 metric.cognitive,
5640 @r#"
5641 {
5642 "sum": 14,
5643 "value": 0,
5644 "average": 14.0,
5645 "min": 0,
5646 "max": 14
5647 }
5648 "#
5649 );
5650 },
5651 );
5652 }
5653
5654 #[cfg(feature = "kotlin")]
5655 #[test]
5656 fn kotlin_no_cognitive() {
5657 check_metrics::<KotlinParser>("fun main() { val x = 42 }", "foo.kt", |metric| {
5658 insta::assert_json_snapshot!(metric.cognitive, @r#"
5659 {
5660 "sum": 0,
5661 "value": 0,
5662 "average": 0.0,
5663 "min": 0,
5664 "max": 0
5665 }
5666 "#);
5667 });
5668 }
5669
5670 #[cfg(feature = "kotlin")]
5671 #[test]
5672 fn kotlin_simple_if_with_boolean() {
5673 check_metrics::<KotlinParser>(
5674 "fun test(a: Boolean, b: Boolean) { if (a && b) { val x = 1 } }",
5675 "foo.kt",
5676 |metric| {
5677 insta::assert_json_snapshot!(metric.cognitive, @r#"
5678 {
5679 "sum": 2,
5680 "value": 0,
5681 "average": 2.0,
5682 "min": 0,
5683 "max": 2
5684 }
5685 "#);
5686 },
5687 );
5688 }
5689
5690 #[cfg(feature = "kotlin")]
5691 #[test]
5692 fn kotlin_nesting() {
5693 check_metrics::<KotlinParser>(
5694 "fun test(items: List<Int>) {
5695 if (items.isNotEmpty()) {
5696 for (i in items) {
5697 if (i > 0) {
5698 println(i)
5699 }
5700 }
5701 }
5702 }",
5703 "foo.kt",
5704 |metric| {
5705 insta::assert_json_snapshot!(metric.cognitive, @r#"
5706 {
5707 "sum": 6,
5708 "value": 0,
5709 "average": 6.0,
5710 "min": 0,
5711 "max": 6
5712 }
5713 "#);
5714 },
5715 );
5716 }
5717
5718 #[cfg(feature = "kotlin")]
5719 #[test]
5720 fn kotlin_when_expression() {
5721 check_metrics::<KotlinParser>(
5722 "fun test(x: Int) { when { x > 10 -> val a = 1; x > 5 -> val b = 2; else -> val c = 3 } }",
5723 "foo.kt",
5724 |metric| {
5725 insta::assert_json_snapshot!(metric.cognitive, @r#"
5726 {
5727 "sum": 1,
5728 "value": 0,
5729 "average": 1.0,
5730 "min": 0,
5731 "max": 1
5732 }
5733 "#);
5734 },
5735 );
5736 }
5737
5738 #[cfg(feature = "kotlin")]
5739 #[test]
5740 fn kotlin_when_else_no_increment() {
5741 check_metrics::<KotlinParser>(
5742 "fun test(x: Int) {
5743 when (x) {
5744 1 -> println(\"one\")
5745 2 -> println(\"two\")
5746 else -> println(\"other\")
5747 }
5748 }",
5749 "foo.kt",
5750 |metric| {
5751 insta::assert_json_snapshot!(metric.cognitive, @r#"
5752 {
5753 "sum": 1,
5754 "value": 0,
5755 "average": 1.0,
5756 "min": 0,
5757 "max": 1
5758 }
5759 "#);
5760 },
5761 );
5762 }
5763
5764 #[cfg(feature = "kotlin")]
5765 #[test]
5766 fn kotlin_labeled_break_continue() {
5767 // Regression (#450): tree-sitter-kotlin-ng has no break/continue
5768 // jump-statement kind — `break@outer` / `continue@outer` are
5769 // `labeled_expression` nodes. The Kotlin impl had no arm for them,
5770 // so labeled jumps scored +0. Each labeled jump is now +1 (§B2);
5771 // the bare `break` below (a plain identifier) stays +0.
5772 check_metrics::<KotlinParser>(
5773 "fun f() {
5774 outer@ for (i in 1..10) { // +1 for
5775 break@outer // +1 labeled
5776 continue@outer // +1 labeled
5777 break // +0 bare
5778 }
5779 }",
5780 "foo.kt",
5781 |metric| {
5782 // expected: +1 for-loop, +1 each labeled break/continue = 3
5783 assert_eq!(metric.cognitive.cognitive_sum(), 3);
5784 insta::assert_json_snapshot!(metric.cognitive, @r#"
5785 {
5786 "sum": 3,
5787 "value": 0,
5788 "average": 3.0,
5789 "min": 0,
5790 "max": 3
5791 }
5792 "#);
5793 },
5794 );
5795 }
5796
5797 #[cfg(feature = "kotlin")]
5798 #[test]
5799 fn kotlin_labeled_nonjump_expression_not_counted() {
5800 // Regression (#450 follow-up): tree-sitter-kotlin-ng models ANY
5801 // labeled expression as `labeled_expression`, not only labeled
5802 // jumps. The original #450 arm was unconditional, so a labeled
5803 // non-jump (`lbl@ run { … }`) wrongly scored +1. The arm now gates
5804 // on the `label` token being the fused jump keyword `break@` /
5805 // `continue@`; an ordinary `name@` label must contribute +0.
5806 // Pre-fix this scored 1.0; verified via test-via-revert.
5807 check_metrics::<KotlinParser>("fun f() { lbl@ run { println(1) } }", "foo.kt", |metric| {
5808 // expected: labeled non-jump is not a structured-control-flow
5809 // break, so it adds nothing.
5810 assert_eq!(metric.cognitive.cognitive_sum(), 0);
5811 });
5812 }
5813
5814 #[cfg(feature = "kotlin")]
5815 #[test]
5816 fn kotlin_else_in_if_still_increments() {
5817 check_metrics::<KotlinParser>(
5818 "fun test(x: Int) {
5819 if (x > 0) {
5820 println(\"positive\")
5821 } else {
5822 println(\"non-positive\")
5823 }
5824 }",
5825 "foo.kt",
5826 |metric| {
5827 insta::assert_json_snapshot!(metric.cognitive, @r#"
5828 {
5829 "sum": 2,
5830 "value": 0,
5831 "average": 2.0,
5832 "min": 0,
5833 "max": 2
5834 }
5835 "#);
5836 },
5837 );
5838 }
5839
5840 #[cfg(feature = "kotlin")]
5841 #[test]
5842 fn kotlin_else_if_chain() {
5843 check_metrics::<KotlinParser>(
5844 "fun test(x: Int) {
5845 if (x > 10) {
5846 } else if (x > 5) {
5847 } else if (x > 0) {
5848 } else {
5849 }
5850 }",
5851 "foo.kt",
5852 |metric| {
5853 insta::assert_json_snapshot!(metric.cognitive, @r#"
5854 {
5855 "sum": 4,
5856 "value": 0,
5857 "average": 4.0,
5858 "min": 0,
5859 "max": 4
5860 }
5861 "#);
5862 },
5863 );
5864 }
5865
5866 #[cfg(feature = "kotlin")]
5867 #[test]
5868 fn kotlin_lambda_nesting() {
5869 check_metrics::<KotlinParser>(
5870 "fun test() { val f = { if (true) { } } }",
5871 "foo.kt",
5872 |metric| {
5873 insta::assert_json_snapshot!(metric.cognitive, @r#"
5874 {
5875 "sum": 2,
5876 "value": 0,
5877 "average": 1.0,
5878 "min": 0,
5879 "max": 2
5880 }
5881 "#);
5882 },
5883 );
5884 }
5885
5886 #[cfg(feature = "kotlin")]
5887 #[test]
5888 fn kotlin_secondary_constructor_depth() {
5889 // Regression: SecondaryConstructor must be a stop in increment_function_depth so
5890 // that a local `fun` nested inside it gets depth=1, making its structural elements
5891 // cost +2 instead of +1.
5892 check_metrics::<KotlinParser>(
5893 "class Foo {
5894 constructor(x: Int) {
5895 fun inner(): Boolean {
5896 if (x > 0) { return true } // +2 (depth=1)
5897 return false
5898 }
5899 }
5900 }",
5901 "foo.kt",
5902 |metric| {
5903 insta::assert_json_snapshot!(metric.cognitive, @r#"
5904 {
5905 "sum": 2,
5906 "value": 0,
5907 "average": 1.0,
5908 "min": 0,
5909 "max": 2
5910 }
5911 "#);
5912 },
5913 );
5914 }
5915
5916 #[cfg(feature = "go")]
5917 #[test]
5918 fn go_no_cognitive() {
5919 check_metrics::<GoParser>("package main\nvar x = 42", "foo.go", |metric| {
5920 insta::assert_json_snapshot!(
5921 metric.cognitive,
5922 @r#"
5923 {
5924 "sum": 0,
5925 "value": 0,
5926 "average": 0.0,
5927 "min": 0,
5928 "max": 0
5929 }
5930 "#
5931 );
5932 });
5933 }
5934
5935 #[cfg(feature = "go")]
5936 #[test]
5937 fn go_simple_function() {
5938 check_metrics::<GoParser>(
5939 "package main
5940 func f(a, b bool) {
5941 if a && b { // +1 (if) +1 (&&)
5942 return
5943 }
5944 if a || b { // +1 (if) +1 (||)
5945 return
5946 }
5947 }",
5948 "foo.go",
5949 |metric| {
5950 insta::assert_json_snapshot!(
5951 metric.cognitive,
5952 @r#"
5953 {
5954 "sum": 4,
5955 "value": 0,
5956 "average": 4.0,
5957 "min": 0,
5958 "max": 4
5959 }
5960 "#
5961 );
5962 },
5963 );
5964 }
5965
5966 #[cfg(feature = "go")]
5967 #[test]
5968 fn go_nesting() {
5969 check_metrics::<GoParser>(
5970 "package main
5971 func f(x int, items []int) {
5972 if x > 0 { // +1 (nesting 0)
5973 for _, v := range items { // +2 (nesting 1)
5974 if v > 0 { // +3 (nesting 2)
5975 println(v)
5976 }
5977 }
5978 }
5979 }",
5980 "foo.go",
5981 |metric| {
5982 insta::assert_json_snapshot!(
5983 metric.cognitive,
5984 @r#"
5985 {
5986 "sum": 6,
5987 "value": 0,
5988 "average": 6.0,
5989 "min": 0,
5990 "max": 6
5991 }
5992 "#
5993 );
5994 },
5995 );
5996 }
5997
5998 #[cfg(feature = "go")]
5999 #[test]
6000 fn go_switch() {
6001 check_metrics::<GoParser>(
6002 "package main
6003 func f(x int) {
6004 switch x { // +1 (nesting 0)
6005 case 1:
6006 if x > 0 { // +2 (nesting 1)
6007 println(x)
6008 }
6009 default:
6010 println(x)
6011 }
6012 }",
6013 "foo.go",
6014 |metric| {
6015 insta::assert_json_snapshot!(
6016 metric.cognitive,
6017 @r#"
6018 {
6019 "sum": 3,
6020 "value": 0,
6021 "average": 3.0,
6022 "min": 0,
6023 "max": 3
6024 }
6025 "#
6026 );
6027 },
6028 );
6029 }
6030
6031 #[cfg(feature = "go")]
6032 #[test]
6033 fn go_goto() {
6034 check_metrics::<GoParser>(
6035 "package main
6036 func f(n int) {
6037 if n > 10 { // +1 (nesting 0)
6038 goto end // +1 (goto)
6039 }
6040 end:
6041 return
6042 }",
6043 "foo.go",
6044 |metric| {
6045 insta::assert_json_snapshot!(
6046 metric.cognitive,
6047 @r#"
6048 {
6049 "sum": 2,
6050 "value": 0,
6051 "average": 2.0,
6052 "min": 0,
6053 "max": 2
6054 }
6055 "#
6056 );
6057 },
6058 );
6059 }
6060
6061 #[cfg(feature = "go")]
6062 #[test]
6063 fn go_else_if_chain() {
6064 check_metrics::<GoParser>(
6065 "package main
6066 func f(x int) {
6067 if x > 0 { // +1 (nesting 0)
6068 println(x)
6069 } else if x < 0 { // +1 (else-if)
6070 println(-x)
6071 } else { // +1 (else)
6072 println(0)
6073 }
6074 }",
6075 "foo.go",
6076 |metric| {
6077 insta::assert_json_snapshot!(
6078 metric.cognitive,
6079 @r#"
6080 {
6081 "sum": 3,
6082 "value": 0,
6083 "average": 3.0,
6084 "min": 0,
6085 "max": 3
6086 }
6087 "#
6088 );
6089 },
6090 );
6091 }
6092
6093 #[cfg(feature = "go")]
6094 #[test]
6095 fn go_labeled_break_continue() {
6096 check_metrics::<GoParser>(
6097 "package main
6098 func f() {
6099 outer:
6100 for i := 0; i < 3; i++ { // +1 (nesting 0)
6101 for j := 0; j < 3; j++ { // +2 (nesting 1)
6102 if i == j { // +3 (nesting 2)
6103 continue outer // +1 (labeled continue)
6104 }
6105 }
6106 }
6107 }",
6108 "foo.go",
6109 |metric| {
6110 insta::assert_json_snapshot!(
6111 metric.cognitive,
6112 @r#"
6113 {
6114 "sum": 7,
6115 "value": 0,
6116 "average": 7.0,
6117 "min": 0,
6118 "max": 7
6119 }
6120 "#
6121 );
6122 },
6123 );
6124 }
6125
6126 #[cfg(feature = "go")]
6127 #[test]
6128 fn go_method_declaration() {
6129 // Coverage: MethodDeclaration is processed as a function boundary (nesting
6130 // reset) identically to FunctionDeclaration. The depth-stop fix from
6131 // 081f893 (adding MethodDeclaration to increment_function_depth's stop
6132 // list) cannot be regression-tested with valid Go because method
6133 // declarations cannot be nested inside other functions or methods.
6134 check_metrics::<GoParser>(
6135 "package main
6136 type T struct{ val int }
6137 func (t T) positive() bool {
6138 if t.val > 0 { // +1
6139 return true
6140 }
6141 return false
6142 }",
6143 "foo.go",
6144 |metric| {
6145 insta::assert_json_snapshot!(metric.cognitive, @r#"
6146 {
6147 "sum": 1,
6148 "value": 0,
6149 "average": 1.0,
6150 "min": 0,
6151 "max": 1
6152 }
6153 "#);
6154 },
6155 );
6156 }
6157
6158 #[cfg(feature = "bash")]
6159 #[test]
6160 fn bash_no_cognitive() {
6161 check_metrics::<BashParser>("a=42", "foo.sh", |metric| {
6162 insta::assert_json_snapshot!(
6163 metric.cognitive,
6164 @r#"
6165 {
6166 "sum": 0,
6167 "value": 0,
6168 "average": 0.0,
6169 "min": 0,
6170 "max": 0
6171 }
6172 "#
6173 );
6174 });
6175 }
6176
6177 #[cfg(feature = "bash")]
6178 #[test]
6179 fn bash_simple_if() {
6180 check_metrics::<BashParser>(
6181 "f() {
6182 if [ -z \"$1\" ]; then # +1
6183 echo empty
6184 fi
6185 }",
6186 "foo.sh",
6187 |metric| {
6188 insta::assert_json_snapshot!(
6189 metric.cognitive,
6190 @r#"
6191 {
6192 "sum": 1,
6193 "value": 0,
6194 "average": 1.0,
6195 "min": 0,
6196 "max": 1
6197 }
6198 "#
6199 );
6200 },
6201 );
6202 }
6203
6204 #[cfg(feature = "bash")]
6205 #[test]
6206 fn bash_if_elif_else() {
6207 check_metrics::<BashParser>(
6208 "f() {
6209 if [ \"$1\" = a ]; then # +1
6210 echo a
6211 elif [ \"$1\" = b ]; then # +1
6212 echo b
6213 else # +1
6214 echo other
6215 fi
6216 }",
6217 "foo.sh",
6218 |metric| {
6219 insta::assert_json_snapshot!(
6220 metric.cognitive,
6221 @r#"
6222 {
6223 "sum": 3,
6224 "value": 0,
6225 "average": 3.0,
6226 "min": 0,
6227 "max": 3
6228 }
6229 "#
6230 );
6231 },
6232 );
6233 }
6234
6235 #[cfg(feature = "bash")]
6236 #[test]
6237 fn bash_nested_loops() {
6238 check_metrics::<BashParser>(
6239 "f() {
6240 for i in 1 2 3; do # +1
6241 while [ \"$x\" -lt 10 ]; do # +2 (nested)
6242 x=$((x+1))
6243 done
6244 done
6245 }",
6246 "foo.sh",
6247 |metric| {
6248 insta::assert_json_snapshot!(
6249 metric.cognitive,
6250 @r#"
6251 {
6252 "sum": 3,
6253 "value": 0,
6254 "average": 3.0,
6255 "min": 0,
6256 "max": 3
6257 }
6258 "#
6259 );
6260 },
6261 );
6262 }
6263
6264 #[cfg(feature = "bash")]
6265 #[test]
6266 fn bash_until_loop() {
6267 // `until` parses to `Bash::WhileStatement`; this test pins that
6268 // assumption so a future grammar bump that adds a dedicated
6269 // `UntilStatement` variant is caught.
6270 check_metrics::<BashParser>(
6271 "f() {
6272 until [ -z \"$x\" ]; do # +1
6273 x=$(pop)
6274 done
6275 }",
6276 "foo.sh",
6277 |metric| {
6278 insta::assert_json_snapshot!(
6279 metric.cognitive,
6280 @r#"
6281 {
6282 "sum": 1,
6283 "value": 0,
6284 "average": 1.0,
6285 "min": 0,
6286 "max": 1
6287 }
6288 "#
6289 );
6290 },
6291 );
6292 }
6293
6294 #[cfg(feature = "bash")]
6295 #[test]
6296 fn bash_case() {
6297 // `case` adds +1 nesting; case arms do not contribute extra cognitive
6298 // cost (matching Kotlin's `WhenExpression` treatment).
6299 check_metrics::<BashParser>(
6300 "f() {
6301 case \"$1\" in # +1
6302 a) echo a ;;
6303 b) echo b ;;
6304 *) echo other ;;
6305 esac
6306 }",
6307 "foo.sh",
6308 |metric| {
6309 insta::assert_json_snapshot!(
6310 metric.cognitive,
6311 @r#"
6312 {
6313 "sum": 1,
6314 "value": 0,
6315 "average": 1.0,
6316 "min": 0,
6317 "max": 1
6318 }
6319 "#
6320 );
6321 },
6322 );
6323 }
6324
6325 #[cfg(feature = "bash")]
6326 #[test]
6327 fn bash_arithmetic_ternary_increases_nesting() {
6328 // Regression for #1268: Bash's only ternary form scored zero
6329 // cognitive complexity. It nests like the C-family
6330 // `ConditionalExpression`. Both arithmetic contexts are covered —
6331 // the `$(( … ))` expansion and the bare `(( … ))` statement.
6332 check_metrics::<BashParser>(
6333 "f() {
6334 local m=$(( a > b ? a : b )) # +1 ternary
6335 }
6336 g() {
6337 (( x = a ? b : c )) # +1 ternary
6338 }",
6339 "foo.sh",
6340 |metric| {
6341 // One increment per function, at nesting 0 in each.
6342 insta::assert_json_snapshot!(
6343 metric.cognitive,
6344 @r#"
6345 {
6346 "sum": 2,
6347 "value": 0,
6348 "average": 1.0,
6349 "min": 0,
6350 "max": 1
6351 }
6352 "#
6353 );
6354 },
6355 );
6356 }
6357
6358 #[cfg(feature = "bash")]
6359 #[test]
6360 fn bash_nested_arithmetic_ternary_charges_the_inner_one_twice() {
6361 // A ternary inside a ternary is +1 for the outer and +2 for the
6362 // inner (one increment plus one nesting level), the same charge
6363 // every nesting construct carries (#1268).
6364 check_metrics::<BashParser>(
6365 "h() {
6366 local n=$(( a ? b : c ? d : e )) # +1 outer, +2 inner
6367 }",
6368 "foo.sh",
6369 |metric| {
6370 insta::assert_json_snapshot!(
6371 metric.cognitive,
6372 @r#"
6373 {
6374 "sum": 3,
6375 "value": 0,
6376 "average": 3.0,
6377 "min": 0,
6378 "max": 3
6379 }
6380 "#
6381 );
6382 },
6383 );
6384 }
6385
6386 #[cfg(feature = "bash")]
6387 #[test]
6388 fn bash_boolean_sequence() {
6389 // First if: a chain of `&&` is one boolean increment regardless of
6390 // length (consecutive same-operator chain). Second if: `&& … ||` is
6391 // two operator transitions, so two boolean increments.
6392 check_metrics::<BashParser>(
6393 "f() {
6394 if [[ -n \"$x\" ]] && [[ -n \"$y\" ]] && [[ -n \"$z\" ]]; then
6395 # +1 if, +1 boolean (one && chain)
6396 echo all
6397 fi
6398 if [[ -n \"$x\" ]] && [[ -n \"$y\" ]] || [[ -n \"$z\" ]]; then
6399 # +1 if, +2 boolean (&& then ||)
6400 echo mixed
6401 fi
6402 }",
6403 "foo.sh",
6404 |metric| {
6405 insta::assert_json_snapshot!(
6406 metric.cognitive,
6407 @r#"
6408 {
6409 "sum": 5,
6410 "value": 0,
6411 "average": 5.0,
6412 "min": 0,
6413 "max": 5
6414 }
6415 "#
6416 );
6417 },
6418 );
6419 }
6420
6421 #[cfg(feature = "tcl")]
6422 #[test]
6423 fn tcl_no_cognitive() {
6424 // No proc, no control flow → cognitive complexity is zero everywhere.
6425 check_metrics::<TclParser>("set x 1", "foo.tcl", |metric| {
6426 assert_eq!(metric.cognitive.cognitive_sum(), 0);
6427 assert_eq!(metric.cognitive.cognitive_max(), 0);
6428 insta::assert_json_snapshot!(metric.cognitive);
6429 });
6430 }
6431
6432 #[cfg(feature = "tcl")]
6433 #[test]
6434 fn tcl_simple_function() {
6435 // proc with one if and one &&: if(+1) + &&(+1) = 2.
6436 check_metrics::<TclParser>(
6437 "proc f {a} {
6438 if {$a > 0 && $a < 10} {
6439 puts yes
6440 }
6441}",
6442 "foo.tcl",
6443 |metric| {
6444 assert_eq!(metric.cognitive.cognitive_sum(), 2);
6445 assert_eq!(metric.cognitive.cognitive_max(), 2);
6446 insta::assert_json_snapshot!(metric.cognitive);
6447 },
6448 );
6449 }
6450
6451 #[cfg(feature = "tcl")]
6452 #[test]
6453 fn tcl_sequence_same_booleans() {
6454 // Sequences of the same boolean operator count as a single increment.
6455 // `$a && $b && $c` → +1 (one && group), not +2.
6456 check_metrics::<TclParser>(
6457 "proc f {a b c d} {
6458 if {$a && $b && $c} {
6459 puts yes
6460 }
6461 if {$a || $b || $c || $d} {
6462 puts no
6463 }
6464}",
6465 "foo.tcl",
6466 |metric| {
6467 // Two ifs (+1 each) + two single-op chains (+1 each) = 4.
6468 assert_eq!(metric.cognitive.cognitive_sum(), 4);
6469 assert_eq!(metric.cognitive.cognitive_max(), 4);
6470 insta::assert_json_snapshot!(metric.cognitive);
6471 },
6472 );
6473 }
6474
6475 #[cfg(feature = "tcl")]
6476 #[test]
6477 fn tcl_sequence_different_booleans() {
6478 // Switching operator type increments again: `$a && $b || $c` → +2 (one &&, one ||).
6479 check_metrics::<TclParser>(
6480 "proc f {a b c} {
6481 if {$a && $b || $c} {
6482 puts yes
6483 }
6484}",
6485 "foo.tcl",
6486 |metric| {
6487 // if(+1) + &&(+1) + ||(+1) = 3.
6488 assert_eq!(metric.cognitive.cognitive_sum(), 3);
6489 assert_eq!(metric.cognitive.cognitive_max(), 3);
6490 insta::assert_json_snapshot!(metric.cognitive);
6491 },
6492 );
6493 }
6494
6495 #[cfg(feature = "tcl")]
6496 #[test]
6497 fn tcl_not_booleans() {
6498 // `!` does not contribute cognitive cost on its own (issue
6499 // #392). The single `&&` between the two negations contributes
6500 // +1, plus +1 for the surrounding `if`.
6501 check_metrics::<TclParser>(
6502 "proc f {a b} {
6503 if {!$a && !$b} {
6504 puts yes
6505 }
6506}",
6507 "foo.tcl",
6508 |metric| {
6509 // if(+1) + &&(+1) = 2; the `!` operators do not increment.
6510 assert_eq!(metric.cognitive.cognitive_sum(), 2);
6511 assert_eq!(metric.cognitive.cognitive_max(), 2);
6512 insta::assert_json_snapshot!(metric.cognitive);
6513 },
6514 );
6515 }
6516
6517 #[cfg(feature = "tcl")]
6518 #[test]
6519 fn tcl_1_level_nesting() {
6520 // while(+1) then if at depth 1 (+2) = 3 for the proc.
6521 check_metrics::<TclParser>(
6522 "proc f {x} {
6523 while {$x > 0} {
6524 if {$x > 10} {
6525 set x [expr {$x - 1}]
6526 }
6527 }
6528}",
6529 "foo.tcl",
6530 |metric| {
6531 // while(+1) + if at depth 1 (+2) = 3.
6532 assert_eq!(metric.cognitive.cognitive_sum(), 3);
6533 assert_eq!(metric.cognitive.cognitive_max(), 3);
6534 insta::assert_json_snapshot!(metric.cognitive);
6535 },
6536 );
6537 }
6538
6539 #[cfg(feature = "tcl")]
6540 #[test]
6541 fn tcl_2_level_nesting() {
6542 // while(+1) + foreach at depth 1 (+2) + if at depth 2 (+3) = 6.
6543 check_metrics::<TclParser>(
6544 "proc f {x} {
6545 while {$x > 0} {
6546 foreach y {1 2 3} {
6547 if {$y > $x} {
6548 puts found
6549 }
6550 }
6551 }
6552}",
6553 "foo.tcl",
6554 |metric| {
6555 // while(+1) + foreach at depth 1 (+2) + if at depth 2 (+3) = 6.
6556 assert_eq!(metric.cognitive.cognitive_sum(), 6);
6557 assert_eq!(metric.cognitive.cognitive_max(), 6);
6558 insta::assert_json_snapshot!(metric.cognitive);
6559 },
6560 );
6561 }
6562
6563 #[cfg(feature = "tcl")]
6564 #[test]
6565 fn tcl_catch_cognitive() {
6566 // `catch` is a conditional handler: +1 at nesting 0, then body at nesting 1.
6567 // Nested if inside catch body: +2 (depth 1).
6568 check_metrics::<TclParser>(
6569 "proc f {x} {
6570 catch {
6571 if {$x < 0} {
6572 error negative
6573 }
6574 } msg
6575}",
6576 "foo.tcl",
6577 |metric| {
6578 // catch(+1) + if at depth 1 (+2) = 3.
6579 assert_eq!(metric.cognitive.cognitive_sum(), 3);
6580 assert_eq!(metric.cognitive.cognitive_max(), 3);
6581 insta::assert_json_snapshot!(metric.cognitive);
6582 },
6583 );
6584 }
6585
6586 #[cfg(feature = "tcl")]
6587 #[test]
6588 fn tcl_if_elseif_else() {
6589 // if(+1) + elseif(+1) + else(+1) = 3; nesting does not increase for elseif/else.
6590 check_metrics::<TclParser>(
6591 "proc f {x} {
6592 if {$x > 10} {
6593 puts big
6594 } elseif {$x > 5} {
6595 puts medium
6596 } else {
6597 puts small
6598 }
6599}",
6600 "foo.tcl",
6601 |metric| {
6602 // if(+1) + elseif(+1) + else(+1) = 3.
6603 assert_eq!(metric.cognitive.cognitive_sum(), 3);
6604 assert_eq!(metric.cognitive.cognitive_max(), 3);
6605 insta::assert_json_snapshot!(metric.cognitive);
6606 },
6607 );
6608 }
6609
6610 #[cfg(feature = "tcl")]
6611 #[test]
6612 fn tcl_not_booleans_nested() {
6613 // `$a && !($b && $c)`: `!` does not break boolean sequences
6614 // (issue #392); inner `&&` is a continuation of the outer.
6615 check_metrics::<TclParser>(
6616 "proc f {a b c} {
6617 if {$a && !($b && $c)} {
6618 puts yes
6619 }
6620}",
6621 "foo.tcl",
6622 |metric| {
6623 // if(+1) + outer &&(+1); inner && continues outer's span → 2.
6624 assert_eq!(metric.cognitive.cognitive_sum(), 2);
6625 assert_eq!(metric.cognitive.cognitive_max(), 2);
6626 insta::assert_json_snapshot!(metric.cognitive);
6627 },
6628 );
6629 }
6630
6631 #[cfg(feature = "tcl")]
6632 #[test]
6633 fn tcl_not_booleans_double_nested() {
6634 // `!($a || $b) && !($c || $d)`: the two `||` sub-expressions and
6635 // the connecting `&&` are at distinct positions with distinct
6636 // operator tokens, so each starts a new boolean sequence
6637 // regardless of the `!` wrapping (issue #392). if(+1) + &&(+1)
6638 // + first ||(+1) + second ||(+1) = 4.
6639 check_metrics::<TclParser>(
6640 "proc f {a b c d} {
6641 if {!($a || $b) && !($c || $d)} {
6642 puts yes
6643 }
6644}",
6645 "foo.tcl",
6646 |metric| {
6647 // if(+1) + &&(+1) + first || (+1) + second || (+1) = 4.
6648 assert_eq!(metric.cognitive.cognitive_sum(), 4);
6649 assert_eq!(metric.cognitive.cognitive_max(), 4);
6650 insta::assert_json_snapshot!(metric.cognitive);
6651 },
6652 );
6653 }
6654
6655 #[cfg(feature = "tcl")]
6656 #[test]
6657 fn tcl_nested_procedure_cognitive() {
6658 // Inner proc is at depth=1; its `if` adds +1+1=2 instead of +1+0=1.
6659 check_metrics::<TclParser>(
6660 "proc outer {x} {
6661 proc inner {y} {
6662 if {$y > 0} {
6663 puts positive
6664 }
6665 }
6666 inner $x
6667}",
6668 "foo.tcl",
6669 |metric| {
6670 // Aggregated: inner proc's `if` at depth 1 contributes 2.
6671 assert_eq!(metric.cognitive.cognitive_sum(), 2);
6672 assert_eq!(metric.cognitive.cognitive_max(), 2);
6673 insta::assert_json_snapshot!(metric.cognitive);
6674 },
6675 );
6676 }
6677
6678 #[cfg(feature = "tcl")]
6679 #[test]
6680 fn tcl_ternary_cognitive() {
6681 // Ternary `? :` inside expr is a conditional expression: adds +1+depth.
6682 // At proc body depth 0: +1. Inside a while (depth 1): +2.
6683 check_metrics::<TclParser>(
6684 "proc f {x} {
6685 set y [expr {$x > 0 ? $x : -$x}]
6686 while {$y > 10} {
6687 set y [expr {$y > 5 ? $y - 1 : 0}]
6688 }
6689}",
6690 "foo.tcl",
6691 |metric| {
6692 // outer ternary(+1) + while(+1) + inner ternary at depth 1 (+2) = 4.
6693 assert_eq!(metric.cognitive.cognitive_sum(), 4);
6694 assert_eq!(metric.cognitive.cognitive_max(), 4);
6695 insta::assert_json_snapshot!(metric.cognitive);
6696 },
6697 );
6698 }
6699
6700 #[cfg(feature = "tcl")]
6701 #[test]
6702 fn tcl_switch_cognitive() {
6703 // Tcl `switch` is a generic command, not a dedicated kind. As a
6704 // switch-like structure it adds +1 plus current nesting once; the arm
6705 // count and the `default` arm do not add cognitive cost, matching
6706 // C-family `SwitchStatement` and Bash `case` (issue #467, lesson 11).
6707 check_metrics::<TclParser>(
6708 "proc f {x} {
6709 switch $x {
6710 1 { puts a }
6711 2 { puts b }
6712 default { puts c }
6713 }
6714}",
6715 "foo.tcl",
6716 |metric| {
6717 // One switch structure at proc-body nesting 0 → +1.
6718 assert_eq!(metric.cognitive.cognitive_sum(), 1);
6719 assert_eq!(metric.cognitive.cognitive_max(), 1);
6720 },
6721 );
6722 }
6723
6724 #[cfg(feature = "tcl")]
6725 #[test]
6726 fn tcl_switch_cognitive_nested() {
6727 // A `switch` nested inside an outer `switch` arm pays the nesting
6728 // penalty: outer +1 (nesting 0), inner +1+1 (nesting 1) = 3 (issue #467).
6729 check_metrics::<TclParser>(
6730 "proc f {x y} {
6731 switch $x {
6732 1 {
6733 switch $y {
6734 a { puts p }
6735 b { puts q }
6736 }
6737 }
6738 2 { puts b }
6739 }
6740}",
6741 "foo.tcl",
6742 |metric| {
6743 // outer switch(+1) + inner switch at nesting 1 (+2) = 3.
6744 assert_eq!(metric.cognitive.cognitive_sum(), 3);
6745 assert_eq!(metric.cognitive.cognitive_max(), 3);
6746 },
6747 );
6748 }
6749
6750 #[cfg(feature = "tcl")]
6751 #[test]
6752 fn tcl_switch_split_form_adds_no_cognitive() {
6753 // The split arm form passes each arm body as its own sibling
6754 // `braced_word` argument, so `tcl_switch_arm_list` finds no
6755 // single wrapping arm list and declines the construct (issue
6756 // #467). Cognitive then treats it as an ordinary command: no
6757 // structural increment and no nesting for its bodies, which is
6758 // why the inner `if` here charges 1 rather than 2.
6759 check_metrics::<TclParser>(
6760 "proc f {x} {
6761 switch $x a { puts a } b { if {$x} { puts b } }
6762}",
6763 "foo.tcl",
6764 |metric| {
6765 // The `switch` adds nothing; the `if` sits at proc-body
6766 // nesting 0 and charges +1.
6767 assert_eq!(metric.cognitive.cognitive_sum(), 1);
6768 assert_eq!(metric.cognitive.cognitive_max(), 1);
6769 },
6770 );
6771 }
6772
6773 #[cfg(feature = "tcl")]
6774 #[test]
6775 fn tcl_for_cognitive() {
6776 // Tcl `for` is a generic command — the grammar has no `for` rule —
6777 // so it is detected by leading word (issue #1264): a loop adds +1
6778 // plus current nesting, matching `while`/`foreach`.
6779 check_metrics::<TclParser>(
6780 "proc f {n} {
6781 for {set i 0} {$i < $n} {incr i} {
6782 puts $i
6783 }
6784}",
6785 "foo.tcl",
6786 |metric| {
6787 // One `for` at proc-body nesting 0 → +1.
6788 assert_eq!(metric.cognitive.cognitive_sum(), 1);
6789 assert_eq!(metric.cognitive.cognitive_max(), 1);
6790 },
6791 );
6792 }
6793
6794 #[cfg(feature = "tcl")]
6795 #[test]
6796 fn tcl_qualified_for_resolves_to_the_builtin() {
6797 // `::for` is `for` reached through the global namespace, so it
6798 // scores exactly what `tcl_for_cognitive` asserts for the bare
6799 // spelling. Until the leading word was normalised, the qualified
6800 // form resolved to no builtin and the loop scored 0 while
6801 // Halstead and the dump still read its body as a script
6802 // (#1381 review).
6803 check_metrics::<TclParser>(
6804 "proc f {n} {
6805 ::for {set i 0} {$i < $n} {incr i} {
6806 puts $i
6807 }
6808}",
6809 "foo.tcl",
6810 |metric| {
6811 assert_eq!(metric.cognitive.cognitive_sum(), 1);
6812 assert_eq!(metric.cognitive.cognitive_max(), 1);
6813 },
6814 );
6815 }
6816
6817 #[cfg(feature = "tcl")]
6818 #[test]
6819 fn tcl_namespaced_for_is_not_the_builtin() {
6820 // Control for the test above: only the *leading* qualifier names
6821 // the global namespace. `ns::for` is a different command living
6822 // in `ns`, so it scores nothing — the direction the `ns::eval`
6823 // row of `BRACED_WORD_VALUE_CASES` pins for Halstead. The braced
6824 // arguments are deliberately operator-free, as in
6825 // `tcl_for_cyclomatic_name_gate`, so nothing inside them can
6826 // supply the increment the leading word must.
6827 check_metrics::<TclParser>(
6828 "proc f {} {
6829 ns::for {a} {b} {c} {d}
6830}",
6831 "foo.tcl",
6832 |metric| {
6833 assert_eq!(metric.cognitive.cognitive_sum(), 0);
6834 assert_eq!(metric.cognitive.cognitive_max(), 0);
6835 },
6836 );
6837 }
6838
6839 #[cfg(feature = "tcl")]
6840 #[test]
6841 fn tcl_for_cognitive_nested() {
6842 // The `for` also nests its body: constructs inside it pay the
6843 // nesting penalty the missing loop previously swallowed (#1264).
6844 check_metrics::<TclParser>(
6845 "proc f {n} {
6846 for {set i 0} {$i < $n} {incr i} {
6847 if {$i == 2} {
6848 puts $i
6849 }
6850 }
6851}",
6852 "foo.tcl",
6853 |metric| {
6854 // for(+1, nesting 0) + if at nesting 1 (+2) = 3.
6855 assert_eq!(metric.cognitive.cognitive_sum(), 3);
6856 assert_eq!(metric.cognitive.cognitive_max(), 3);
6857 },
6858 );
6859 }
6860
6861 #[cfg(feature = "tcl")]
6862 #[test]
6863 fn tcl_for_cognitive_name_gate() {
6864 // The detection reads the command's `name` field: a command whose
6865 // name merely starts with "for" (`format`, with `for`-shaped braced
6866 // arguments) and a `for` word in argument position (`puts for`) must
6867 // both stay at zero (issue #1264).
6868 check_metrics::<TclParser>(
6869 "proc f {} {
6870 format {a} {b} {c} {d}
6871 puts for
6872}",
6873 "foo.tcl",
6874 |metric| {
6875 assert_eq!(metric.cognitive.cognitive_sum(), 0);
6876 assert_eq!(metric.cognitive.cognitive_max(), 0);
6877 },
6878 );
6879 }
6880
6881 #[cfg(all(feature = "irules", feature = "tcl"))]
6882 #[test]
6883 fn tcl_irules_for_parity() {
6884 // iRules models `for` as a dedicated kind counted by the kind
6885 // dispatch; Tcl detects it by leading word (issue #1264). The same
6886 // loop must score identically in both — and the iRules figure also
6887 // pins that its dedicated kind is not double-counted through the
6888 // Tcl command-name path. One loop at nesting 0 → +1 in each.
6889 check_metrics::<TclParser>(
6890 "proc f {} {
6891 for {set i 0} {$i < 10} {incr i} {
6892 puts $i
6893 }
6894}",
6895 "foo.tcl",
6896 |metric| {
6897 assert_eq!(metric.cognitive.cognitive_sum(), 1);
6898 assert_eq!(metric.cognitive.cognitive_max(), 1);
6899 },
6900 );
6901 check_metrics::<IrulesParser>(
6902 "when HTTP_REQUEST {
6903 for {set i 0} {$i < 10} {incr i} {
6904 puts $i
6905 }
6906}
6907",
6908 "foo.irule",
6909 |metric| {
6910 assert_eq!(metric.cognitive.cognitive_sum(), 1);
6911 assert_eq!(metric.cognitive.cognitive_max(), 1);
6912 },
6913 );
6914 }
6915
6916 #[cfg(feature = "tcl")]
6917 #[test]
6918 fn tcl_try_on_error_cognitive() {
6919 // Tcl `try`'s `on error` handler is a conditional error path:
6920 // +1 plus current nesting (issue #1266), matching `catch`;
6921 // `finally` is unconditional and free.
6922 check_metrics::<TclParser>(
6923 "proc f {} {
6924 try {
6925 risky
6926 } on error {msg} {
6927 puts $msg
6928 } finally {
6929 cleanup
6930 }
6931}",
6932 "foo.tcl",
6933 |metric| {
6934 // One handler at proc-body nesting 0 → +1.
6935 assert_eq!(metric.cognitive.cognitive_sum(), 1);
6936 assert_eq!(metric.cognitive.cognitive_max(), 1);
6937 },
6938 );
6939 }
6940
6941 #[cfg(feature = "tcl")]
6942 #[test]
6943 fn tcl_try_finally_only_cognitive() {
6944 // A `try` with only a `finally` has no conditional path and must
6945 // stay at zero (issue #1266, the cross-language `finally`
6946 // convention of #416).
6947 check_metrics::<TclParser>(
6948 "proc f {} {
6949 try {
6950 risky
6951 } finally {
6952 cleanup
6953 }
6954}",
6955 "foo.tcl",
6956 |metric| {
6957 assert_eq!(metric.cognitive.cognitive_sum(), 0);
6958 assert_eq!(metric.cognitive.cognitive_max(), 0);
6959 },
6960 );
6961 }
6962
6963 #[cfg(feature = "tcl")]
6964 #[test]
6965 fn tcl_try_handler_nesting_cognitive() {
6966 // Only the handler body nests (issue #1266): the `try` body and
6967 // `finally` body run unconditionally and stay at the inherited
6968 // level, so an `if` inside each of the three blocks is charged
6969 // differently. This pins the per-child nesting seed — charging
6970 // `increase_nesting` at the whole `try` node instead would score
6971 // the try-body and finally-body `if`s +2 each (sum 7).
6972 check_metrics::<TclParser>(
6973 "proc f {} {
6974 try {
6975 if {1} {
6976 a
6977 }
6978 } on error {msg} {
6979 if {1} {
6980 b
6981 }
6982 } finally {
6983 if {1} {
6984 c
6985 }
6986 }
6987}",
6988 "foo.tcl",
6989 |metric| {
6990 // handler(+1) + try-body if(+1, nesting 0)
6991 // + handler-body if(+2, nesting 1)
6992 // + finally-body if(+1, nesting 0) = 5.
6993 assert_eq!(metric.cognitive.cognitive_sum(), 5);
6994 assert_eq!(metric.cognitive.cognitive_max(), 5);
6995 },
6996 );
6997 }
6998
6999 #[cfg(feature = "irules")]
7000 #[test]
7001 fn irules_try_trap_cognitive() {
7002 // iRules wraps each `try` handler in a dedicated `on_handler` /
7003 // `trap_handler` node: +1 each plus nesting, and the handler body
7004 // nests (issue #1266). Before this the handlers opened anonymous
7005 // function spaces, which reset nesting and hid the construct from
7006 // the enclosing proc entirely.
7007 check_metrics::<IrulesParser>(
7008 "proc f {} {
7009 try {
7010 risky
7011 } on error {msg} {
7012 puts $msg
7013 } trap {POSIX} {msg} {
7014 if {1} {
7015 puts $msg
7016 }
7017 }
7018}",
7019 "foo.irule",
7020 |metric| {
7021 // on(+1) + trap(+1) + if at nesting 1 inside trap (+2) = 4.
7022 assert_eq!(metric.cognitive.cognitive_sum(), 4);
7023 assert_eq!(metric.cognitive.cognitive_max(), 4);
7024 },
7025 );
7026 }
7027
7028 #[cfg(all(feature = "irules", feature = "tcl"))]
7029 #[test]
7030 fn tcl_irules_try_parity() {
7031 // The same single-handler `try` must score identically in Tcl
7032 // (flat `on`/`error` tokens under `try`, per-child nesting seed)
7033 // and iRules (a dedicated `on_handler` wrapper) — issue #1266.
7034 // handler(+1) + if at nesting 1 inside the handler (+2) = 3.
7035 let source = "proc f {} {
7036 try {
7037 risky
7038 } on error {msg} {
7039 if {1} {
7040 puts $msg
7041 }
7042 }
7043}";
7044 check_metrics::<TclParser>(source, "foo.tcl", |metric| {
7045 assert_eq!(metric.cognitive.cognitive_sum(), 3);
7046 assert_eq!(metric.cognitive.cognitive_max(), 3);
7047 });
7048 check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
7049 assert_eq!(metric.cognitive.cognitive_sum(), 3);
7050 assert_eq!(metric.cognitive.cognitive_max(), 3);
7051 });
7052 }
7053
7054 /// Drift marker for #1266 (lesson 34 / grammar-dispatch §2): the
7055 /// iRules `on_handler` / `trap_handler` kinds are emitted **only**
7056 /// under `try`.
7057 ///
7058 /// Statement-level `on <event> { … }` and `trap { … }` inside an event
7059 /// body are generic `command` nodes at the pinned grammar, which is
7060 /// what lets Cognitive and Cyclomatic charge those two kinds
7061 /// unconditionally as catch clauses. A bump that promoted the
7062 /// statement-level spelling to the same kinds would silently turn
7063 /// every one of those commands into a decision point — and, since the
7064 /// checker treats handler kinds as part of the `try` shape, would
7065 /// re-run the mistake #1266 fixed, where the handlers were read as
7066 /// `when`-style event handlers and opened function spaces of their
7067 /// own. Nothing else in the suite would go red.
7068 #[cfg(feature = "irules")]
7069 #[test]
7070 fn irules_try_handler_kinds_appear_only_under_try() {
7071 use std::path::PathBuf;
7072
7073 use crate::test_support::ast_has_kind_id;
7074
7075 let path = PathBuf::from("foo.irule");
7076
7077 let statement_level = "when HTTP_REQUEST {\n\
7078 \x20 on scriptbody { log \"x\" }\n\
7079 \x20 trap { log \"y\" }\n\
7080 }\n";
7081 let parser = IrulesParser::new(statement_level.as_bytes().to_vec(), &path, None);
7082 // Non-vacuity: both spellings must really be in this parse, as
7083 // `command`s whose leading word is the handler keyword. Asserting
7084 // only the absence below would pass just as well against a source
7085 // the grammar rejected outright.
7086 let command_names: Vec<&str> = parser
7087 .root()
7088 .preorder()
7089 .filter(|node| node.kind_id() == Irules::Command as u16)
7090 .filter_map(|node| {
7091 node.child_by_field_name("name")
7092 .and_then(|name| name.utf8_text(statement_level.as_bytes()))
7093 })
7094 .collect();
7095 assert!(
7096 command_names.contains(&"on") && command_names.contains(&"trap"),
7097 "statement-level `on` / `trap` no longer parse as commands: {command_names:?}",
7098 );
7099 assert!(
7100 !ast_has_kind_id(&parser, Irules::OnHandler as u16),
7101 "statement-level `on` now emits `on_handler`; re-derive the \
7102 Cognitive / Cyclomatic handler arms and the checker's `try` \
7103 shape before trusting them",
7104 );
7105 assert!(
7106 !ast_has_kind_id(&parser, Irules::TrapHandler as u16),
7107 "statement-level `trap` now emits `trap_handler`; re-derive the \
7108 Cognitive / Cyclomatic handler arms and the checker's `try` \
7109 shape before trusting them",
7110 );
7111
7112 // And the kinds are reachable, so the assertions above discriminate
7113 // rather than naming variants the grammar never emits at all.
7114 let under_try = "when HTTP_REQUEST {\n\
7115 \x20 try { risky } on error {e} { log $e } trap {POSIX} {e} { log $e }\n\
7116 }\n";
7117 let parser = IrulesParser::new(under_try.as_bytes().to_vec(), &path, None);
7118 assert!(
7119 ast_has_kind_id(&parser, Irules::OnHandler as u16),
7120 "`on error` under `try` no longer emits `on_handler`",
7121 );
7122 assert!(
7123 ast_has_kind_id(&parser, Irules::TrapHandler as u16),
7124 "`trap` under `try` no longer emits `trap_handler`",
7125 );
7126 }
7127
7128 #[cfg(feature = "lua")]
7129 #[test]
7130 fn lua_cognitive_no_cognitive() {
7131 // Top-level local assignment, no control flow → cognitive complexity is zero.
7132 check_metrics::<LuaParser>("local x = 42", "foo.lua", |metric| {
7133 insta::assert_json_snapshot!(
7134 metric.cognitive,
7135 @r#"
7136 {
7137 "sum": 0,
7138 "value": 0,
7139 "average": 0.0,
7140 "min": 0,
7141 "max": 0
7142 }
7143 "#
7144 );
7145 });
7146 }
7147
7148 #[cfg(feature = "lua")]
7149 #[test]
7150 fn lua_cognitive_simple_function() {
7151 // Two `if … and …` statements at function scope: each contributes
7152 // +1 (if) + 1 (and) = 2; total 4.
7153 check_metrics::<LuaParser>(
7154 "local function f(a, b, c, d)
7155 if a and b then
7156 return 1
7157 end
7158 if c and d then
7159 return 1
7160 end
7161end",
7162 "foo.lua",
7163 |metric| {
7164 insta::assert_json_snapshot!(
7165 metric.cognitive,
7166 @r#"
7167 {
7168 "sum": 4,
7169 "value": 0,
7170 "average": 4.0,
7171 "min": 0,
7172 "max": 4
7173 }
7174 "#
7175 );
7176 },
7177 );
7178 }
7179
7180 #[cfg(feature = "lua")]
7181 #[test]
7182 fn lua_cognitive_sequence_same_booleans() {
7183 // Sequences of the same boolean operator count as a single increment.
7184 // `a and b and c` → +1 (one and-group), `a or b or c or d` → +1.
7185 // Plus +1 per `if` ⇒ 4 total.
7186 check_metrics::<LuaParser>(
7187 "local function f(a, b, c, d)
7188 if a and b and c then
7189 return 1
7190 end
7191 if a or b or c or d then
7192 return 1
7193 end
7194end",
7195 "foo.lua",
7196 |metric| {
7197 insta::assert_json_snapshot!(
7198 metric.cognitive,
7199 @r#"
7200 {
7201 "sum": 4,
7202 "value": 0,
7203 "average": 4.0,
7204 "min": 0,
7205 "max": 4
7206 }
7207 "#
7208 );
7209 },
7210 );
7211 }
7212
7213 #[cfg(feature = "lua")]
7214 #[test]
7215 fn lua_cognitive_not_booleans() {
7216 // `not a and not b`: `not` does not contribute cognitive cost
7217 // on its own (issue #392); the single `and` between the two
7218 // negations contributes +1. if(+1) + and(+1) = 2.
7219 check_metrics::<LuaParser>(
7220 "local function f(a, b)
7221 if not a and not b then
7222 return 1
7223 end
7224end",
7225 "foo.lua",
7226 |metric| {
7227 insta::assert_json_snapshot!(
7228 metric.cognitive,
7229 @r#"
7230 {
7231 "sum": 2,
7232 "value": 0,
7233 "average": 2.0,
7234 "min": 0,
7235 "max": 2
7236 }
7237 "#
7238 );
7239 },
7240 );
7241 }
7242
7243 #[cfg(feature = "lua")]
7244 #[test]
7245 fn lua_cognitive_sequence_different_booleans() {
7246 // Switching operator type increments again: `a and b or c`
7247 // → if(+1) + and(+1) + or(+1) = 3.
7248 check_metrics::<LuaParser>(
7249 "local function f(a, b, c)
7250 if a and b or c then
7251 return 1
7252 end
7253end",
7254 "foo.lua",
7255 |metric| {
7256 insta::assert_json_snapshot!(
7257 metric.cognitive,
7258 @r#"
7259 {
7260 "sum": 3,
7261 "value": 0,
7262 "average": 3.0,
7263 "min": 0,
7264 "max": 3
7265 }
7266 "#
7267 );
7268 },
7269 );
7270 }
7271
7272 #[cfg(feature = "lua")]
7273 #[test]
7274 fn lua_cognitive_1_level_nesting() {
7275 // for at depth 0 (+1) + if at depth 1 (+2) = 3.
7276 check_metrics::<LuaParser>(
7277 "local function f(t)
7278 for i = 1, #t do
7279 if t[i] > 0 then
7280 return t[i]
7281 end
7282 end
7283end",
7284 "foo.lua",
7285 |metric| {
7286 insta::assert_json_snapshot!(
7287 metric.cognitive,
7288 @r#"
7289 {
7290 "sum": 3,
7291 "value": 0,
7292 "average": 3.0,
7293 "min": 0,
7294 "max": 3
7295 }
7296 "#
7297 );
7298 },
7299 );
7300 }
7301
7302 #[cfg(feature = "lua")]
7303 #[test]
7304 fn lua_cognitive_2_level_nesting() {
7305 // outer for (+1) + inner for at depth 1 (+2) + if at depth 2 (+3) = 6.
7306 check_metrics::<LuaParser>(
7307 "local function f(t)
7308 for i = 1, #t do
7309 for j = 1, #t do
7310 if t[i] > t[j] then
7311 return t[i]
7312 end
7313 end
7314 end
7315end",
7316 "foo.lua",
7317 |metric| {
7318 insta::assert_json_snapshot!(
7319 metric.cognitive,
7320 @r#"
7321 {
7322 "sum": 6,
7323 "value": 0,
7324 "average": 6.0,
7325 "min": 0,
7326 "max": 6
7327 }
7328 "#
7329 );
7330 },
7331 );
7332 }
7333
7334 #[cfg(feature = "lua")]
7335 #[test]
7336 fn lua_cognitive_break_continue() {
7337 // Lua's `break` is always unlabeled (the grammar has no labeled
7338 // break and no `continue`), so per SonarSource Cognitive Complexity
7339 // §B2 it adds +0 — issue #435. for(+1) + if at depth 1 (+2) = 3.
7340 check_metrics::<LuaParser>(
7341 "local function f(t)
7342 for i = 1, #t do
7343 if t[i] < 0 then
7344 break
7345 end
7346 end
7347end",
7348 "foo.lua",
7349 |metric| {
7350 assert_eq!(metric.cognitive.cognitive_sum(), 3);
7351 insta::assert_json_snapshot!(
7352 metric.cognitive,
7353 @r#"
7354 {
7355 "sum": 3,
7356 "value": 0,
7357 "average": 3.0,
7358 "min": 0,
7359 "max": 3
7360 }
7361 "#
7362 );
7363 },
7364 );
7365 }
7366
7367 #[cfg(feature = "lua")]
7368 #[test]
7369 fn lua_cognitive_goto_counted() {
7370 // `goto label` is a genuinely unstructured jump and adds +1 per
7371 // SonarSource §B2, even though Lua's unlabeled `break` does not
7372 // (issue #435). Only the `goto` contributes: +1.
7373 check_metrics::<LuaParser>(
7374 "local function f()
7375 ::top::
7376 goto top
7377end",
7378 "foo.lua",
7379 |metric| {
7380 assert_eq!(metric.cognitive.cognitive_sum(), 1);
7381 insta::assert_json_snapshot!(
7382 metric.cognitive,
7383 @r#"
7384 {
7385 "sum": 1,
7386 "value": 0,
7387 "average": 1.0,
7388 "min": 0,
7389 "max": 1
7390 }
7391 "#
7392 );
7393 },
7394 );
7395 }
7396
7397 #[cfg(feature = "lua")]
7398 #[test]
7399 fn lua_cognitive_elseif_nesting() {
7400 // Lua-specific: `elseif_statement` is a dedicated grammar node that
7401 // stays at the same nesting level as the enclosing `if`. Chain:
7402 // if(+1) + elseif(+1) + elseif(+1) + else(+1) = 4.
7403 check_metrics::<LuaParser>(
7404 "local function classify(x)
7405 if x > 0 then
7406 return 1
7407 elseif x < 0 then
7408 return -1
7409 elseif x == 0 then
7410 return 0
7411 else
7412 return 0
7413 end
7414end",
7415 "foo.lua",
7416 |metric| {
7417 insta::assert_json_snapshot!(
7418 metric.cognitive,
7419 @r#"
7420 {
7421 "sum": 4,
7422 "value": 0,
7423 "average": 4.0,
7424 "min": 0,
7425 "max": 4
7426 }
7427 "#
7428 );
7429 },
7430 );
7431 }
7432
7433 #[cfg(feature = "typescript")]
7434 #[test]
7435 fn typescript_switch_statement() {
7436 check_metrics::<TypescriptParser>(
7437 "function describe(x: number): string {
7438 switch (x) { // +1
7439 case 1:
7440 return 'one';
7441 case 2:
7442 return 'two';
7443 default:
7444 return 'other';
7445 }
7446 }",
7447 "foo.ts",
7448 |metric| {
7449 assert_eq!(metric.cognitive.cognitive_sum(), 1);
7450 assert_eq!(metric.cognitive.cognitive_max(), 1);
7451 insta::assert_json_snapshot!(metric.cognitive);
7452 },
7453 );
7454 }
7455
7456 #[cfg(feature = "typescript")]
7457 #[test]
7458 fn typescript_no_cognitive() {
7459 check_metrics::<TypescriptParser>(
7460 "function f(a: number, b: number): number {
7461 return a + b;
7462 }",
7463 "foo.ts",
7464 |metric| {
7465 assert_eq!(metric.cognitive.cognitive_sum(), 0);
7466 assert_eq!(metric.cognitive.cognitive_max(), 0);
7467 insta::assert_json_snapshot!(metric.cognitive);
7468 },
7469 );
7470 }
7471
7472 #[cfg(feature = "typescript")]
7473 #[test]
7474 fn tsx_no_cognitive() {
7475 check_metrics::<TsxParser>(
7476 "function f(a: number, b: number): number {
7477 return a + b;
7478 }",
7479 "foo.tsx",
7480 |metric| {
7481 assert_eq!(metric.cognitive.cognitive_sum(), 0);
7482 assert_eq!(metric.cognitive.cognitive_max(), 0);
7483 insta::assert_json_snapshot!(metric.cognitive);
7484 },
7485 );
7486 }
7487
7488 #[cfg(feature = "typescript")]
7489 #[test]
7490 fn tsx_simple_if() {
7491 check_metrics::<TsxParser>(
7492 "function f(x: number): number {
7493 if (x > 0) { // +1
7494 return x;
7495 }
7496 return 0;
7497 }",
7498 "foo.tsx",
7499 |metric| {
7500 assert_eq!(metric.cognitive.cognitive_sum(), 1);
7501 assert_eq!(metric.cognitive.cognitive_max(), 1);
7502 insta::assert_json_snapshot!(metric.cognitive);
7503 },
7504 );
7505 }
7506
7507 #[cfg(feature = "typescript")]
7508 #[test]
7509 fn tsx_boolean_sequence() {
7510 check_metrics::<TsxParser>(
7511 "function f(a: boolean, b: boolean, c: boolean): boolean {
7512 return a && b && c; // +1 (&&, sequence)
7513 }",
7514 "foo.tsx",
7515 |metric| {
7516 assert_eq!(metric.cognitive.cognitive_sum(), 1);
7517 assert_eq!(metric.cognitive.cognitive_max(), 1);
7518 insta::assert_json_snapshot!(metric.cognitive);
7519 },
7520 );
7521 }
7522
7523 #[cfg(feature = "typescript")]
7524 #[test]
7525 fn tsx_2_level_nesting() {
7526 check_metrics::<TsxParser>(
7527 "function f(a: number[], n: number): number {
7528 for (let i = 0; i < a.length; i++) { // +1
7529 if (a[i] > n) { // +2 (nesting=1)
7530 return a[i];
7531 }
7532 }
7533 return -1;
7534 }",
7535 "foo.tsx",
7536 |metric| {
7537 // for(+1) + if at depth 1 (+2) = 3.
7538 assert_eq!(metric.cognitive.cognitive_sum(), 3);
7539 assert_eq!(metric.cognitive.cognitive_max(), 3);
7540 insta::assert_json_snapshot!(metric.cognitive);
7541 },
7542 );
7543 }
7544
7545 #[cfg(feature = "typescript")]
7546 #[test]
7547 fn tsx_else_if_chain() {
7548 check_metrics::<TsxParser>(
7549 "function classify(x: number): string {
7550 if (x < 0) { // +1
7551 return 'neg';
7552 } else if (x === 0) { // +1 (else if = structural, not nesting)
7553 return 'zero';
7554 } else { // +1
7555 return 'pos';
7556 }
7557 }",
7558 "foo.tsx",
7559 |metric| {
7560 // if(+1) + else-if(+1) + else(+1) = 3.
7561 assert_eq!(metric.cognitive.cognitive_sum(), 3);
7562 assert_eq!(metric.cognitive.cognitive_max(), 3);
7563 insta::assert_json_snapshot!(metric.cognitive);
7564 },
7565 );
7566 }
7567
7568 #[cfg(feature = "javascript")]
7569 #[test]
7570 fn js_sibling_bool_sequences() {
7571 // (a&&b)||(c&&d) — the right-hand && is a *new* sequence (sibling, not nested),
7572 // so it should score +1, giving a total of 3 (&&, ||, &&).
7573 // The pre-existing bug stored only (kind_id) and treated the right && as a
7574 // continuation of the earlier && sequence, incorrectly yielding 2.
7575 check_metrics::<JavascriptParser>(
7576 "function f(a, b, c, d) {
7577 return (a && b) || (c && d); // +1(&&) +1(||) +1(&&) = 3
7578 }",
7579 "foo.js",
7580 |metric| {
7581 assert_eq!(metric.cognitive.cognitive_sum(), 3);
7582 assert_eq!(metric.cognitive.cognitive_max(), 3);
7583 insta::assert_json_snapshot!(metric.cognitive);
7584 },
7585 );
7586 }
7587
7588 #[cfg(feature = "javascript")]
7589 #[test]
7590 fn js_nested_bool_same_op() {
7591 // a||(b&&c&&d) — the inner && operators are nested inside ||, so they form
7592 // one sequence and only the first should score +1. Total = 2 (||, &&).
7593 check_metrics::<JavascriptParser>(
7594 "function f(a, b, c, d) {
7595 return a || (b && c && d); // +1(||) +1(&&) = 2
7596 }",
7597 "foo.js",
7598 |metric| {
7599 assert_eq!(metric.cognitive.cognitive_sum(), 2);
7600 assert_eq!(metric.cognitive.cognitive_max(), 2);
7601 insta::assert_json_snapshot!(metric.cognitive);
7602 },
7603 );
7604 }
7605
7606 #[cfg(feature = "python")]
7607 #[test]
7608 fn python_sibling_bool_sequences() {
7609 // Python uses keyword boolean operators (`and`/`or`), routed through a
7610 // different `T` instantiation of `compute_booleans` than the JS `&&`/`||`
7611 // tests. Verifies the sibling-detection fix applies across operator kinds.
7612 // (a and b) or (c and d) — the right-hand `and` is a sibling, not nested.
7613 // Expected: and_left(+1) + or(+1) + and_right(+1) = 3.
7614 check_metrics::<PythonParser>(
7615 "def f(a, b, c, d):
7616 return (a and b) or (c and d) # +1(and) +1(or) +1(and) = 3
7617 ",
7618 "foo.py",
7619 |metric| {
7620 assert_eq!(metric.cognitive.cognitive_sum(), 3);
7621 assert_eq!(metric.cognitive.cognitive_max(), 3);
7622 insta::assert_json_snapshot!(metric.cognitive);
7623 },
7624 );
7625 }
7626
7627 #[cfg(feature = "python")]
7628 #[test]
7629 fn python_nested_bool_same_op() {
7630 // a or (b and c and d) — the inner `and` operators are nested inside `or`,
7631 // forming one sequence. Expected: or(+1) + and(+1) = 2.
7632 check_metrics::<PythonParser>(
7633 "def f(a, b, c, d):
7634 return a or (b and c and d) # +1(or) +1(and) = 2
7635 ",
7636 "foo.py",
7637 |metric| {
7638 assert_eq!(metric.cognitive.cognitive_sum(), 2);
7639 assert_eq!(metric.cognitive.cognitive_max(), 2);
7640 insta::assert_json_snapshot!(metric.cognitive);
7641 },
7642 );
7643 }
7644
7645 #[cfg(feature = "perl")]
7646 #[test]
7647 fn perl_sibling_bool_sequences() {
7648 // Perl uses `compute_perl_booleans` (a separate function supporting five
7649 // operator kinds including `//`). Verifies the sibling-detection fix also
7650 // covers that code path.
7651 // ($a && $b) || ($c && $d) — the right-hand `&&` is a sibling.
7652 // Expected: &&(+1) + ||(+1) + &&(+1) = 3.
7653 check_metrics::<PerlParser>(
7654 "sub f {
7655 my ($a, $b, $c, $d) = @_;
7656 return ($a && $b) || ($c && $d); # +1(&&) +1(||) +1(&&) = 3
7657 }",
7658 "foo.pl",
7659 |metric| {
7660 assert_eq!(metric.cognitive.cognitive_sum(), 3);
7661 assert_eq!(metric.cognitive.cognitive_max(), 3);
7662 insta::assert_json_snapshot!(metric.cognitive);
7663 },
7664 );
7665 }
7666
7667 #[cfg(feature = "perl")]
7668 #[test]
7669 fn perl_nested_bool_same_op() {
7670 // $a || ($b && $c && $d) — the inner `&&` operators are nested inside `||`,
7671 // forming one sequence. Exercises the `compute_perl_booleans` continuation
7672 // guard (the only path distinct from `compute_booleans`).
7673 // Expected: ||(+1) + &&(+1) = 2.
7674 check_metrics::<PerlParser>(
7675 "sub f {
7676 my ($a, $b, $c, $d) = @_;
7677 return $a || ($b && $c && $d); # +1(||) +1(&&) = 2
7678 }",
7679 "foo.pl",
7680 |metric| {
7681 assert_eq!(metric.cognitive.cognitive_sum(), 2);
7682 assert_eq!(metric.cognitive.cognitive_max(), 2);
7683 insta::assert_json_snapshot!(metric.cognitive);
7684 },
7685 );
7686 }
7687
7688 #[cfg(feature = "rust")]
7689 #[test]
7690 fn rust_sibling_bool_sequences() {
7691 // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested.
7692 // Expected: &&(+1) + ||(+1) + &&(+1) = 3.
7693 check_metrics::<RustParser>(
7694 "fn f(a: bool, b: bool, c: bool, d: bool) -> bool {
7695 (a && b) || (c && d) // +1(&&) +1(||) +1(&&) = 3
7696 }",
7697 "foo.rs",
7698 |metric| {
7699 assert_eq!(metric.cognitive.cognitive_sum(), 3);
7700 assert_eq!(metric.cognitive.cognitive_max(), 3);
7701 insta::assert_json_snapshot!(metric.cognitive);
7702 },
7703 );
7704 }
7705
7706 #[cfg(feature = "rust")]
7707 #[test]
7708 fn rust_nested_bool_same_op() {
7709 // a||(b&&c&&d) — the inner && operators are nested, forming one sequence.
7710 // Expected: ||(+1) + &&(+1) = 2.
7711 check_metrics::<RustParser>(
7712 "fn f(a: bool, b: bool, c: bool, d: bool) -> bool {
7713 a || (b && c && d) // +1(||) +1(&&) = 2
7714 }",
7715 "foo.rs",
7716 |metric| {
7717 assert_eq!(metric.cognitive.cognitive_sum(), 2);
7718 assert_eq!(metric.cognitive.cognitive_max(), 2);
7719 insta::assert_json_snapshot!(metric.cognitive);
7720 },
7721 );
7722 }
7723
7724 #[cfg(feature = "c")]
7725 #[test]
7726 fn c_sibling_bool_sequences() {
7727 // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested.
7728 // Expected: &&(+1) + ||(+1) + &&(+1) = 3.
7729 check_metrics::<CParser>(
7730 "int f(int a, int b, int c, int d) {
7731 return (a && b) || (c && d); // +1(&&) +1(||) +1(&&) = 3
7732 }",
7733 "foo.c",
7734 |metric| {
7735 assert_eq!(metric.cognitive.cognitive_sum(), 3);
7736 assert_eq!(metric.cognitive.cognitive_max(), 3);
7737 insta::assert_json_snapshot!(metric.cognitive);
7738 },
7739 );
7740 }
7741
7742 #[cfg(feature = "c")]
7743 #[test]
7744 fn c_nested_bool_same_op() {
7745 // a||(b&&c&&d) — the inner && operators are nested, forming one sequence.
7746 // Expected: ||(+1) + &&(+1) = 2.
7747 check_metrics::<CParser>(
7748 "int f(int a, int b, int c, int d) {
7749 return a || (b && c && d); // +1(||) +1(&&) = 2
7750 }",
7751 "foo.c",
7752 |metric| {
7753 assert_eq!(metric.cognitive.cognitive_sum(), 2);
7754 assert_eq!(metric.cognitive.cognitive_max(), 2);
7755 insta::assert_json_snapshot!(metric.cognitive);
7756 },
7757 );
7758 }
7759
7760 #[cfg(feature = "mozjs")]
7761 #[test]
7762 fn mozjs_sibling_bool_sequences() {
7763 // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested.
7764 // Expected: &&(+1) + ||(+1) + &&(+1) = 3.
7765 check_metrics::<MozjsParser>(
7766 "function f(a, b, c, d) {
7767 return (a && b) || (c && d); // +1(&&) +1(||) +1(&&) = 3
7768 }",
7769 "foo.js",
7770 |metric| {
7771 assert_eq!(metric.cognitive.cognitive_sum(), 3);
7772 assert_eq!(metric.cognitive.cognitive_max(), 3);
7773 insta::assert_json_snapshot!(metric.cognitive);
7774 },
7775 );
7776 }
7777
7778 #[cfg(feature = "mozjs")]
7779 #[test]
7780 fn mozjs_nested_bool_same_op() {
7781 // a||(b&&c&&d) — the inner && operators are nested, forming one sequence.
7782 // Expected: ||(+1) + &&(+1) = 2.
7783 check_metrics::<MozjsParser>(
7784 "function f(a, b, c, d) {
7785 return a || (b && c && d); // +1(||) +1(&&) = 2
7786 }",
7787 "foo.js",
7788 |metric| {
7789 assert_eq!(metric.cognitive.cognitive_sum(), 2);
7790 assert_eq!(metric.cognitive.cognitive_max(), 2);
7791 insta::assert_json_snapshot!(metric.cognitive);
7792 },
7793 );
7794 }
7795
7796 #[cfg(feature = "typescript")]
7797 #[test]
7798 fn typescript_sibling_bool_sequences() {
7799 // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested.
7800 // Expected: &&(+1) + ||(+1) + &&(+1) = 3.
7801 check_metrics::<TypescriptParser>(
7802 "function f(a: boolean, b: boolean, c: boolean, d: boolean): boolean {
7803 return (a && b) || (c && d); // +1(&&) +1(||) +1(&&) = 3
7804 }",
7805 "foo.ts",
7806 |metric| {
7807 assert_eq!(metric.cognitive.cognitive_sum(), 3);
7808 assert_eq!(metric.cognitive.cognitive_max(), 3);
7809 insta::assert_json_snapshot!(metric.cognitive);
7810 },
7811 );
7812 }
7813
7814 #[cfg(feature = "typescript")]
7815 #[test]
7816 fn typescript_nested_bool_same_op() {
7817 // a||(b&&c&&d) — the inner && operators are nested, forming one sequence.
7818 // Expected: ||(+1) + &&(+1) = 2.
7819 check_metrics::<TypescriptParser>(
7820 "function f(a: boolean, b: boolean, c: boolean, d: boolean): boolean {
7821 return a || (b && c && d); // +1(||) +1(&&) = 2
7822 }",
7823 "foo.ts",
7824 |metric| {
7825 assert_eq!(metric.cognitive.cognitive_sum(), 2);
7826 assert_eq!(metric.cognitive.cognitive_max(), 2);
7827 insta::assert_json_snapshot!(metric.cognitive);
7828 },
7829 );
7830 }
7831
7832 #[cfg(feature = "typescript")]
7833 #[test]
7834 fn tsx_sibling_bool_sequences() {
7835 // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested.
7836 // Expected: &&(+1) + ||(+1) + &&(+1) = 3.
7837 check_metrics::<TsxParser>(
7838 "function f(a: boolean, b: boolean, c: boolean, d: boolean): boolean {
7839 return (a && b) || (c && d); // +1(&&) +1(||) +1(&&) = 3
7840 }",
7841 "foo.tsx",
7842 |metric| {
7843 assert_eq!(metric.cognitive.cognitive_sum(), 3);
7844 assert_eq!(metric.cognitive.cognitive_max(), 3);
7845 insta::assert_json_snapshot!(metric.cognitive);
7846 },
7847 );
7848 }
7849
7850 #[cfg(feature = "typescript")]
7851 #[test]
7852 fn tsx_nested_bool_same_op() {
7853 // a||(b&&c&&d) — the inner && operators are nested, forming one sequence.
7854 // Expected: ||(+1) + &&(+1) = 2.
7855 check_metrics::<TsxParser>(
7856 "function f(a: boolean, b: boolean, c: boolean, d: boolean): boolean {
7857 return a || (b && c && d); // +1(||) +1(&&) = 2
7858 }",
7859 "foo.tsx",
7860 |metric| {
7861 assert_eq!(metric.cognitive.cognitive_sum(), 2);
7862 assert_eq!(metric.cognitive.cognitive_max(), 2);
7863 insta::assert_json_snapshot!(metric.cognitive);
7864 },
7865 );
7866 }
7867
7868 #[cfg(feature = "javascript")]
7869 #[test]
7870 fn javascript_nullish_coalescing_chain_230() {
7871 // Regression for issue #230: `??` is a short-circuit operator and
7872 // must form a boolean sequence. `a ?? b ?? c` is a single chain
7873 // of identical operators and collapses to a single +1 under
7874 // Sonar B1 (same rule as `&&` / `||`).
7875 check_metrics::<JavascriptParser>(
7876 "function pick(a, b, c) {
7877 return a ?? b ?? c; // +1 (chain of ??)
7878 }",
7879 "foo.js",
7880 |metric| {
7881 assert_eq!(metric.cognitive.cognitive_sum(), 1);
7882 assert_eq!(metric.cognitive.cognitive_max(), 1);
7883 insta::assert_json_snapshot!(
7884 metric.cognitive,
7885 @r#"
7886 {
7887 "sum": 1,
7888 "value": 0,
7889 "average": 1.0,
7890 "min": 0,
7891 "max": 1
7892 }
7893 "#
7894 );
7895 },
7896 );
7897 }
7898
7899 #[cfg(feature = "typescript")]
7900 #[test]
7901 fn typescript_nullish_coalescing_with_if_230() {
7902 // Regression for issue #230: the example from the issue body.
7903 // Boolean sequences pay a flat +1 (no nesting penalty) per Sonar
7904 // B1, so the issue body's stated total of 3 was wrong — the
7905 // correct answer is if(+1) + ?? chain (+1) = 2. Previously the
7906 // `??` chain was not counted at all (= 1).
7907 check_metrics::<TypescriptParser>(
7908 "function risky(x: string | null, fallback: string | null): string {
7909 if (x === \"y\") { // +1
7910 return x ?? fallback ?? \"unknown\"; // +1 (chain of ??)
7911 }
7912 return \"no\";
7913 }",
7914 "foo.ts",
7915 |metric| {
7916 assert_eq!(metric.cognitive.cognitive_sum(), 2);
7917 assert_eq!(metric.cognitive.cognitive_max(), 2);
7918 insta::assert_json_snapshot!(
7919 metric.cognitive,
7920 @r#"
7921 {
7922 "sum": 2,
7923 "value": 0,
7924 "average": 2.0,
7925 "min": 0,
7926 "max": 2
7927 }
7928 "#
7929 );
7930 },
7931 );
7932 }
7933
7934 #[cfg(feature = "typescript")]
7935 #[test]
7936 fn tsx_nullish_coalescing_chain_230() {
7937 // Regression for issue #230: TSX parity with JS/TS for `??`.
7938 check_metrics::<TsxParser>(
7939 "function pick(a: number | null, b: number | null, c: number): number {
7940 return a ?? b ?? c; // +1 (chain of ??)
7941 }",
7942 "foo.tsx",
7943 |metric| {
7944 assert_eq!(metric.cognitive.cognitive_sum(), 1);
7945 assert_eq!(metric.cognitive.cognitive_max(), 1);
7946 insta::assert_json_snapshot!(
7947 metric.cognitive,
7948 @r#"
7949 {
7950 "sum": 1,
7951 "value": 0,
7952 "average": 1.0,
7953 "min": 0,
7954 "max": 1
7955 }
7956 "#
7957 );
7958 },
7959 );
7960 }
7961
7962 #[cfg(feature = "mozjs")]
7963 #[test]
7964 fn mozjs_nullish_coalescing_chain_230() {
7965 // Regression for issue #230: Mozjs parity with JS for `??`.
7966 check_metrics::<MozjsParser>(
7967 "function pick(a, b, c) {
7968 return a ?? b ?? c; // +1 (chain of ??)
7969 }",
7970 "foo.js",
7971 |metric| {
7972 assert_eq!(metric.cognitive.cognitive_sum(), 1);
7973 assert_eq!(metric.cognitive.cognitive_max(), 1);
7974 insta::assert_json_snapshot!(
7975 metric.cognitive,
7976 @r#"
7977 {
7978 "sum": 1,
7979 "value": 0,
7980 "average": 1.0,
7981 "min": 0,
7982 "max": 1
7983 }
7984 "#
7985 );
7986 },
7987 );
7988 }
7989
7990 #[cfg(feature = "csharp")]
7991 #[test]
7992 fn csharp_null_coalescing_cognitive_230() {
7993 // Regression for issue #230: C# `??` must form a boolean sequence
7994 // just like `&&` / `||`. Boolean sequences pay a flat +1 (no
7995 // nesting penalty) per Sonar B1.
7996 // if(+1) + ?? chain (+1) = 2. Previously the `??` chain
7997 // contributed nothing and the function scored 1.
7998 check_metrics::<CsharpParser>(
7999 "class C {
8000 string Risky(string x, string fallback) {
8001 if (x == \"y\") { // +1
8002 return x ?? fallback ?? \"unknown\"; // +1 (chain of ??)
8003 }
8004 return \"no\";
8005 }
8006 }",
8007 "foo.cs",
8008 |metric| {
8009 assert_eq!(metric.cognitive.cognitive_sum(), 2);
8010 assert_eq!(metric.cognitive.cognitive_max(), 2);
8011 insta::assert_json_snapshot!(
8012 metric.cognitive,
8013 @r#"
8014 {
8015 "sum": 2,
8016 "value": 0,
8017 "average": 2.0,
8018 "min": 0,
8019 "max": 2
8020 }
8021 "#
8022 );
8023 },
8024 );
8025 }
8026
8027 #[cfg(feature = "php")]
8028 #[test]
8029 fn php_null_coalescing_cognitive_230() {
8030 // Regression for issue #230: PHP `??` must form a boolean sequence
8031 // just like `&&` / `||`. Parallels the PHP cyclomatic
8032 // null-coalescing handling. Boolean sequences pay a flat +1 (no
8033 // nesting penalty) per Sonar B1.
8034 // if(+1) + ?? chain (+1) = 2.
8035 check_metrics::<PhpParser>(
8036 "<?php
8037 function risky($x, $fallback) {
8038 if ($x === \"y\") { // +1
8039 return $x ?? $fallback ?? \"unknown\"; // +1 (chain of ??)
8040 }
8041 return \"no\";
8042 }",
8043 "foo.php",
8044 |metric| {
8045 assert_eq!(metric.cognitive.cognitive_sum(), 2);
8046 assert_eq!(metric.cognitive.cognitive_max(), 2);
8047 insta::assert_json_snapshot!(
8048 metric.cognitive,
8049 @r#"
8050 {
8051 "sum": 2,
8052 "value": 0,
8053 "average": 2.0,
8054 "min": 0,
8055 "max": 2
8056 }
8057 "#
8058 );
8059 },
8060 );
8061 }
8062
8063 // Companions to `php_null_coalescing_cognitive_230`: the PHP
8064 // cognitive operator set extends past `&&` / `||` / `??` to include
8065 // the word-form `and` / `or`, mirroring PHP cyclomatic. A chain of
8066 // identical word-form operators collapses to a single
8067 // boolean-sequence increment under Sonar B1, the same way `&&` /
8068 // `||` chains do. Each word-form gets its own test so a regression
8069 // that drops a single variant (e.g. only `Or`) is still caught;
8070 // `xor` is a value, not a sequence (#1536), and its test pins that.
8071
8072 #[cfg(feature = "php")]
8073 #[test]
8074 fn php_word_form_and_forms_boolean_sequence_230() {
8075 check_metrics::<PhpParser>(
8076 "<?php
8077 function check_and($a, $b, $c, $d) {
8078 if ($a and $b and $c and $d) { // +1 (if) + 1 (and chain)
8079 return true;
8080 }
8081 return false;
8082 }",
8083 "foo.php",
8084 |metric| {
8085 assert_eq!(metric.cognitive.cognitive_sum(), 2);
8086 assert_eq!(metric.cognitive.cognitive_max(), 2);
8087 },
8088 );
8089 }
8090
8091 #[cfg(feature = "php")]
8092 #[test]
8093 fn php_word_form_or_forms_boolean_sequence_230() {
8094 check_metrics::<PhpParser>(
8095 "<?php
8096 function check_or($a, $b, $c, $d) {
8097 if ($a or $b or $c or $d) { // +1 (if) + 1 (or chain)
8098 return true;
8099 }
8100 return false;
8101 }",
8102 "foo.php",
8103 |metric| {
8104 assert_eq!(metric.cognitive.cognitive_sum(), 2);
8105 assert_eq!(metric.cognitive.cognitive_max(), 2);
8106 },
8107 );
8108 }
8109
8110 #[cfg(feature = "php")]
8111 #[test]
8112 fn php_word_form_xor_is_a_value_not_a_sequence_1536() {
8113 // `xor` evaluates both operands, so it is a value like `^`
8114 // (#1536): only the `if` scores, and the `^` twin agrees.
8115 for op in ["xor", "^"] {
8116 check_metrics::<PhpParser>(
8117 &format!(
8118 "<?php
8119 function check_xor($a, $b, $c, $d) {{
8120 if ($a {op} $b {op} $c {op} $d) {{ // +1 (if)
8121 return true;
8122 }}
8123 return false;
8124 }}"
8125 ),
8126 "foo.php",
8127 |metric| {
8128 assert_eq!(metric.cognitive.cognitive_sum(), 1);
8129 assert_eq!(metric.cognitive.cognitive_max(), 1);
8130 },
8131 );
8132 }
8133 }
8134
8135 #[cfg(feature = "java")]
8136 #[test]
8137 fn java_cognitive_else_if_chain() {
8138 // Regression for #115: else-if chains must not receive a nesting
8139 // increment for the `if` inside `else if`. Expected breakdown:
8140 // if(+1) + else(+1) + else(+1) + else(+1) = 4.
8141 check_metrics::<JavaParser>(
8142 "class X {
8143 public static void f(int x) {
8144 if (x > 10) {
8145 } else if (x > 5) {
8146 } else if (x > 0) {
8147 } else {
8148 }
8149 }
8150 }",
8151 "foo.java",
8152 |metric| {
8153 insta::assert_json_snapshot!(
8154 metric.cognitive,
8155 @r#"
8156 {
8157 "sum": 4,
8158 "value": 0,
8159 "average": 4.0,
8160 "min": 0,
8161 "max": 4
8162 }
8163 "#
8164 );
8165 },
8166 );
8167 }
8168
8169 #[cfg(feature = "java")]
8170 #[test]
8171 fn java_cognitive_nested_else_if() {
8172 // Regression for #115: else-if inside a loop must still respect
8173 // the loop's nesting for the initial `if`, but the `else if`
8174 // branch should only pay a flat +1 via the `else` keyword.
8175 // for(+1) + if at nesting=1(+2) + else(+1) + else(+1) = 5.
8176 check_metrics::<JavaParser>(
8177 "class X {
8178 public static void f(int x) {
8179 for (int i = 0; i < x; i++) {
8180 if (i > 10) {
8181 } else if (i > 5) {
8182 } else {
8183 }
8184 }
8185 }
8186 }",
8187 "foo.java",
8188 |metric| {
8189 insta::assert_json_snapshot!(
8190 metric.cognitive,
8191 @r#"
8192 {
8193 "sum": 5,
8194 "value": 0,
8195 "average": 5.0,
8196 "min": 0,
8197 "max": 5
8198 }
8199 "#
8200 );
8201 },
8202 );
8203 }
8204
8205 #[cfg(feature = "java")]
8206 #[test]
8207 fn java_cognitive_if_inside_else_block_is_not_else_if() {
8208 // Regression for #115: an `if` whose previous sibling is the block's
8209 // opening brace (not the `else` keyword) is a nested independent
8210 // statement, NOT an else-if continuation. It must pay the full
8211 // nesting penalty.
8212 // if(+1, nesting=0) + else(+1) + inner if(+2, nesting=1) = 4.
8213 check_metrics::<JavaParser>(
8214 "class X {
8215 public static void f(int a, int c) {
8216 if (a > 0) {
8217 } else {
8218 if (c > 0) {
8219 }
8220 }
8221 }
8222 }",
8223 "foo.java",
8224 |metric| {
8225 insta::assert_json_snapshot!(
8226 metric.cognitive,
8227 @r#"
8228 {
8229 "sum": 4,
8230 "value": 0,
8231 "average": 4.0,
8232 "min": 0,
8233 "max": 4
8234 }
8235 "#
8236 );
8237 },
8238 );
8239 }
8240
8241 #[cfg(feature = "java")]
8242 #[test]
8243 fn java_sibling_bool_sequences() {
8244 // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested.
8245 // Expected: &&(+1) + ||(+1) + &&(+1) = 3.
8246 check_metrics::<JavaParser>(
8247 "class X {
8248 boolean f(boolean a, boolean b, boolean c, boolean d) {
8249 return (a && b) || (c && d); // +1(&&) +1(||) +1(&&) = 3
8250 }
8251 }",
8252 "foo.java",
8253 |metric| {
8254 assert_eq!(metric.cognitive.cognitive_sum(), 3);
8255 assert_eq!(metric.cognitive.cognitive_max(), 3);
8256 insta::assert_json_snapshot!(metric.cognitive);
8257 },
8258 );
8259 }
8260
8261 #[cfg(feature = "java")]
8262 #[test]
8263 fn java_nested_bool_same_op() {
8264 // a||(b&&c&&d) — the inner && operators are nested, forming one sequence.
8265 // Expected: ||(+1) + &&(+1) = 2.
8266 check_metrics::<JavaParser>(
8267 "class X {
8268 boolean f(boolean a, boolean b, boolean c, boolean d) {
8269 return a || (b && c && d); // +1(||) +1(&&) = 2
8270 }
8271 }",
8272 "foo.java",
8273 |metric| {
8274 assert_eq!(metric.cognitive.cognitive_sum(), 2);
8275 assert_eq!(metric.cognitive.cognitive_max(), 2);
8276 insta::assert_json_snapshot!(metric.cognitive);
8277 },
8278 );
8279 }
8280
8281 #[cfg(feature = "groovy")]
8282 #[test]
8283 fn groovy_no_cognitive() {
8284 check_metrics::<GroovyParser>("class A { int x = 42 }", "foo.groovy", |metric| {
8285 assert_eq!(metric.cognitive.cognitive_sum(), 0);
8286 });
8287 }
8288
8289 #[cfg(feature = "groovy")]
8290 #[test]
8291 fn groovy_single_branch_function() {
8292 check_metrics::<GroovyParser>(
8293 "void f(int x) {
8294 if (x > 0) {
8295 println(x)
8296 }
8297 }",
8298 "foo.groovy",
8299 |metric| {
8300 // if = +1
8301 assert_eq!(metric.cognitive.cognitive_sum(), 1);
8302 },
8303 );
8304 }
8305
8306 #[cfg(feature = "groovy")]
8307 #[test]
8308 fn groovy_nested_if() {
8309 check_metrics::<GroovyParser>(
8310 "void f(int x, int y) {
8311 if (x > 0) {
8312 if (y > 0) {
8313 println(x)
8314 }
8315 }
8316 }",
8317 "foo.groovy",
8318 |metric| {
8319 // outer if (+1) + inner if (+2 for nesting depth 1) = 3
8320 assert_eq!(metric.cognitive.cognitive_sum(), 3);
8321 },
8322 );
8323 }
8324
8325 #[cfg(feature = "groovy")]
8326 #[test]
8327 fn groovy_else_if_chain() {
8328 // Regression for the #115 / #239 stub pattern: an `else if`
8329 // chain must NOT receive a nesting increment for the `if`
8330 // inside `else if`. Without the sibling-`Else` pattern in
8331 // `Checker::is_else_if`, this would have scored higher.
8332 check_metrics::<GroovyParser>(
8333 "class X {
8334 static void f(int x) {
8335 if (x > 10) {
8336 } else if (x > 5) {
8337 } else if (x > 0) {
8338 } else {
8339 }
8340 }
8341 }",
8342 "foo.groovy",
8343 |metric| {
8344 // if(+1) + else(+1) + else(+1) + else(+1) = 4
8345 assert_eq!(metric.cognitive.cognitive_sum(), 4);
8346 },
8347 );
8348 }
8349
8350 #[cfg(feature = "groovy")]
8351 #[test]
8352 fn groovy_else_if_chain_lower_than_nested_ifs() {
8353 // The `else if` chain in `groovy_else_if_chain` MUST score
8354 // lower than an equivalent depth of nested `if` blocks — this
8355 // is the inequality the test exists to defend (lesson 10).
8356 check_metrics::<GroovyParser>(
8357 "class X {
8358 static void f(int x) {
8359 if (x > 10) {
8360 if (x > 5) {
8361 if (x > 0) {
8362 }
8363 }
8364 }
8365 }
8366 }",
8367 "foo.groovy",
8368 |metric| {
8369 // 3 nested `if`s: 1 + 2 + 3 = 6 (each deeper layer
8370 // pays a higher nesting cost). The chain in
8371 // `groovy_else_if_chain` produces 4, so this MUST
8372 // exceed it.
8373 assert!(metric.cognitive.cognitive_sum() > 4);
8374 },
8375 );
8376 }
8377
8378 #[cfg(feature = "groovy")]
8379 #[test]
8380 fn groovy_sequence_booleans_same_op() {
8381 // SonarSource B1: a chain of identical short-circuit ops counts as one.
8382 check_metrics::<GroovyParser>(
8383 "void f(boolean a, boolean b, boolean c) {
8384 if (a && b && c) { println(a) }
8385 }",
8386 "foo.groovy",
8387 |metric| {
8388 // if (+1) + boolean sequence (+1) = 2
8389 assert_eq!(metric.cognitive.cognitive_sum(), 2);
8390 },
8391 );
8392 }
8393
8394 #[cfg(feature = "groovy")]
8395 #[test]
8396 fn groovy_sequence_booleans_mixed_ops() {
8397 // A `&&` followed by `||` is two distinct sequences = +2.
8398 check_metrics::<GroovyParser>(
8399 "void f(boolean a, boolean b, boolean c) {
8400 if (a && b || c) { println(a) }
8401 }",
8402 "foo.groovy",
8403 |metric| {
8404 // if (+1) + && (+1) + || (+1) = 3
8405 assert_eq!(metric.cognitive.cognitive_sum(), 3);
8406 },
8407 );
8408 }
8409
8410 #[cfg(feature = "groovy")]
8411 #[test]
8412 fn groovy_not_operator_negation() {
8413 // SonarSource: `!` negation flips a boolean sequence's polarity
8414 // but doesn't add cognitive cost on its own.
8415 check_metrics::<GroovyParser>(
8416 "void f(boolean a, boolean b) {
8417 if (a && !b) { println(a) }
8418 }",
8419 "foo.groovy",
8420 |metric| {
8421 // if(+1) + && (+1) = 2
8422 assert_eq!(metric.cognitive.cognitive_sum(), 2);
8423 },
8424 );
8425 }
8426
8427 #[cfg(feature = "groovy")]
8428 #[test]
8429 fn groovy_for_while_do_loops() {
8430 check_metrics::<GroovyParser>(
8431 "void f(int n) {
8432 for (int i = 0; i < n; i++) {
8433 while (i > 0) {
8434 i--
8435 }
8436 }
8437 }",
8438 "foo.groovy",
8439 |metric| {
8440 // for(+1) + while inside for(+2) = 3
8441 assert_eq!(metric.cognitive.cognitive_sum(), 3);
8442 },
8443 );
8444 }
8445
8446 #[cfg(feature = "groovy")]
8447 #[test]
8448 fn groovy_enhanced_for() {
8449 check_metrics::<GroovyParser>(
8450 "void f(List items) {
8451 for (item in items) {
8452 println(item)
8453 }
8454 }",
8455 "foo.groovy",
8456 |metric| {
8457 assert_eq!(metric.cognitive.cognitive_sum(), 1);
8458 },
8459 );
8460 }
8461
8462 #[cfg(feature = "groovy")]
8463 #[test]
8464 fn groovy_try_catch_nesting() {
8465 check_metrics::<GroovyParser>(
8466 "void f() {
8467 try {
8468 risky()
8469 } catch (Exception e) {
8470 handle(e)
8471 }
8472 }",
8473 "foo.groovy",
8474 |metric| {
8475 // catch(+1) = 1
8476 assert_eq!(metric.cognitive.cognitive_sum(), 1);
8477 },
8478 );
8479 }
8480
8481 #[cfg(feature = "groovy")]
8482 #[test]
8483 fn groovy_ternary_expression() {
8484 check_metrics::<GroovyParser>(
8485 "void f(int x) {
8486 def y = (x > 0) ? 1 : 2
8487 }",
8488 "foo.groovy",
8489 |metric| {
8490 // ternary(+1) = 1
8491 assert_eq!(metric.cognitive.cognitive_sum(), 1);
8492 },
8493 );
8494 }
8495
8496 #[cfg(feature = "groovy")]
8497 #[test]
8498 fn groovy_elvis_chain_246() {
8499 // Regression for issue #246: Groovy's Elvis operator `?:` is
8500 // a short-circuit nullish operator analogous to Kotlin's `?:`
8501 // (#239) and JS `??`. `a ?: b ?: c` is a single chain of
8502 // identical operators and collapses to a single +1 under
8503 // SonarSource Cognitive Complexity B1 — the same rule applied
8504 // to `&&` / `||`. Closed by swapping the prior amaanq grammar
8505 // (which mis-parsed Elvis as `ternary_expression` + MISSING
8506 // identifier) for `dekobon-tree-sitter-groovy`, which models
8507 // Elvis as a distinct `elvis_expression` node.
8508 check_metrics::<GroovyParser>(
8509 "def pick(a, b, c) {
8510 return a ?: b ?: c // +1 (Elvis chain)
8511 }",
8512 "foo.groovy",
8513 |metric| {
8514 assert_eq!(metric.cognitive.cognitive_sum(), 1);
8515 assert_eq!(metric.cognitive.cognitive_max(), 1);
8516 },
8517 );
8518 }
8519
8520 #[cfg(feature = "groovy")]
8521 #[test]
8522 fn groovy_elvis_inside_if_246() {
8523 // Regression for issue #246: Elvis chain inside an `if` body.
8524 // Boolean sequences pay a flat +1 (no nesting penalty) per
8525 // SonarSource B1: if(+1) + Elvis chain(+1) = 2.
8526 check_metrics::<GroovyParser>(
8527 "def f(a, b) {
8528 if (a != null) { // +1
8529 return a ?: b ?: 'x' // +1 (Elvis chain)
8530 }
8531 return 'no'
8532 }",
8533 "foo.groovy",
8534 |metric| {
8535 assert_eq!(metric.cognitive.cognitive_sum(), 2);
8536 assert_eq!(metric.cognitive.cognitive_max(), 2);
8537 },
8538 );
8539 }
8540
8541 #[cfg(feature = "groovy")]
8542 #[test]
8543 fn groovy_labeled_break_continue() {
8544 // SonarSource B2: labeled break/continue each add +1.
8545 check_metrics::<GroovyParser>(
8546 "void f() {
8547 outer:
8548 for (int i = 0; i < 10; i++) {
8549 inner:
8550 for (int j = 0; j < 10; j++) {
8551 if (i == j) break outer
8552 if (i < j) continue inner
8553 }
8554 }
8555 }",
8556 "foo.groovy",
8557 |metric| {
8558 // for(+1) + for(+2 nested) + if(+3) + break label(+1)
8559 // + if(+3) + continue label(+1) = 11
8560 assert_eq!(metric.cognitive.cognitive_sum(), 11);
8561 },
8562 );
8563 }
8564
8565 #[cfg(feature = "groovy")]
8566 #[test]
8567 fn groovy_multiple_branch_function() {
8568 // Sibling `if` statements at the same nesting level each
8569 // contribute +1; an `else` at the same level adds another
8570 // +1 via the Else arm.
8571 check_metrics::<GroovyParser>(
8572 "class X {
8573 static void print(boolean a, boolean b) {
8574 if (a) {
8575 println 'test1'
8576 }
8577 if (b) {
8578 println 'test2'
8579 } else {
8580 println 'test3'
8581 }
8582 }
8583 }",
8584 "foo.groovy",
8585 |metric| {
8586 // if(+1) + if(+1) + else(+1) = 3
8587 assert_eq!(metric.cognitive.cognitive_sum(), 3);
8588 },
8589 );
8590 }
8591
8592 #[cfg(feature = "groovy")]
8593 #[test]
8594 fn groovy_unlabeled_break_continue_not_counted() {
8595 // SonarSource B2: plain `break` / `continue` are NOT
8596 // unstructured jumps and must add 0 — only labeled forms
8597 // pay the +1. Matches Java's identical fixture.
8598 check_metrics::<GroovyParser>(
8599 "class X {
8600 void scan(int[] m) {
8601 for (int i = 0; i < m.length; i++) {
8602 if (m[i] < 0) continue
8603 if (m[i] > 100) break
8604 }
8605 }
8606 }",
8607 "foo.groovy",
8608 |metric| {
8609 // for(+1) + if(+2) + if(+2) = 5 (break/continue add 0)
8610 assert_eq!(metric.cognitive.cognitive_sum(), 5);
8611 },
8612 );
8613 }
8614
8615 #[cfg(feature = "groovy")]
8616 #[test]
8617 fn groovy_cognitive_closure_body_counts_lambda_nesting() {
8618 // #519: control flow inside a Groovy closure must pay the same
8619 // lambda-nesting surcharge as Java's `LambdaExpression`, so the
8620 // byte-equivalent construct scores identically across languages
8621 // (lesson #11). The byte-for-byte Java equivalent
8622 // (`list.forEach(item -> { if (a) { while (b) {} } })`) also
8623 // reports cognitive sum 5.0.
8624 //
8625 // Test-via-revert (.claude/rules/testing.md): removing the
8626 // `Closure => { lambda += 1; }` arm drops this to 3.0 — the
8627 // missing +2 lambda surcharge on the nested `if`/`while`.
8628 check_metrics::<GroovyParser>(
8629 "class X {
8630 static void f(java.util.List list, boolean a, boolean b) {
8631 list.each { if (a) { while (b) {} } }
8632 }
8633 }",
8634 "foo.groovy",
8635 |metric| {
8636 // closure(lambda=1) -> if at nesting=1(+2)
8637 // -> while at nesting=2(+3) = 5
8638 assert_eq!(metric.cognitive.cognitive_sum(), 5);
8639 },
8640 );
8641 }
8642
8643 #[cfg(feature = "groovy")]
8644 #[test]
8645 fn groovy_nested_method_resets_nesting_and_adds_depth() {
8646 // Regression for #696: a local-class method declared two `if`s deep
8647 // inside an outer method must reset nesting to 0 and gain a
8648 // function-depth surcharge — not inherit the enclosing nesting.
8649 //
8650 // expected: outer `if` (+1, nesting=0) + inner `if` (+2, nesting=1)
8651 // + Local.f's `if` (+1 base + 1 depth = +2, nesting=0, depth=1) = 5.
8652 // Before the fix, `f` inherited nesting=2 from the two enclosing
8653 // `if`s, scoring its inner `if` at nesting 2 (+3) for a sum of 6.
8654 // The two-deep nesting is load-bearing: one level deep, the
8655 // inherited nesting (1) coincidentally equals the depth bump (1).
8656 check_metrics::<GroovyParser>(
8657 "class Outer {
8658 void outer(boolean a) {
8659 if (a) {
8660 if (a) {
8661 class Local {
8662 void f(boolean b) {
8663 if (b) { g() }
8664 }
8665 }
8666 }
8667 }
8668 }
8669 }",
8670 "foo.groovy",
8671 |metric| {
8672 assert_eq!(metric.cognitive.cognitive_sum(), 5);
8673 assert_eq!(metric.cognitive.cognitive_max(), 3);
8674 },
8675 );
8676 }
8677
8678 #[cfg(feature = "groovy")]
8679 #[test]
8680 fn groovy_cognitive_top_level_typed_method_parity() {
8681 // Regression for the upstream grammar defect
8682 // tree-sitter-groovy#20, fixed in =0.2.2: a top-level method
8683 // with an explicit return type whose body contained a `;`
8684 // (e.g. a C-style `for`) misparsed into identifier + call +
8685 // standalone closure, so it was not recognized as a function and
8686 // its body brace-block was a `Closure` — which the new lambda arm
8687 // would have spuriously surcharged. Post-fix the typed form must
8688 // parse as a real method and score identically to the `def` form.
8689 let typed = "void f(int n) {
8690 for (int i = 0; i < n; i++) {
8691 if (i > 0) { }
8692 }
8693 }";
8694 let untyped = "def f(int n) {
8695 for (int i = 0; i < n; i++) {
8696 if (i > 0) { }
8697 }
8698 }";
8699 // for(+1) + if at nesting=1(+2) = 3; no lambda surcharge because
8700 // the body is a `block`, not a misparsed `Closure`.
8701 check_metrics::<GroovyParser>(typed, "foo.groovy", |metric| {
8702 assert_eq!(metric.cognitive.cognitive_sum(), 3);
8703 });
8704 check_metrics::<GroovyParser>(untyped, "foo.groovy", |metric| {
8705 assert_eq!(metric.cognitive.cognitive_sum(), 3);
8706 });
8707 }
8708
8709 #[cfg(feature = "groovy")]
8710 #[test]
8711 fn groovy_cognitive_nested_else_if() {
8712 // Regression for the #115 stub pattern at deeper nesting:
8713 // an `else if` chain inside a `for` loop must still respect
8714 // the loop's nesting for the initial `if`, but each
8715 // `else`-chained branch pays a flat +1 via the Else arm.
8716 // Matches Java's identical fixture.
8717 check_metrics::<GroovyParser>(
8718 "class X {
8719 static void f(int x) {
8720 for (int i = 0; i < x; i++) {
8721 if (i > 10) {
8722 } else if (i > 5) {
8723 } else {
8724 }
8725 }
8726 }
8727 }",
8728 "foo.groovy",
8729 |metric| {
8730 // for(+1) + if at nesting=1(+2) + else(+1) + else(+1) = 5
8731 assert_eq!(metric.cognitive.cognitive_sum(), 5);
8732 },
8733 );
8734 }
8735
8736 #[cfg(feature = "groovy")]
8737 #[test]
8738 fn groovy_cognitive_if_inside_else_block_is_not_else_if() {
8739 // Regression for #115 — an inner `if` whose previous sibling
8740 // is the block's opening brace (not the `else` keyword) is a
8741 // nested independent statement, NOT an else-if continuation,
8742 // so it pays the full nesting penalty. Matches Java's
8743 // identical fixture.
8744 check_metrics::<GroovyParser>(
8745 "class X {
8746 static void f(int a, int c) {
8747 if (a > 0) {
8748 } else {
8749 if (c > 0) {
8750 }
8751 }
8752 }
8753 }",
8754 "foo.groovy",
8755 |metric| {
8756 // if(+1, nesting=0) + else(+1) + inner if(+2, nesting=1) = 4
8757 assert_eq!(metric.cognitive.cognitive_sum(), 4);
8758 },
8759 );
8760 }
8761
8762 #[cfg(feature = "groovy")]
8763 #[test]
8764 fn groovy_nested_ternary() {
8765 // Nested ternaries inside an `if` compound by nesting — same
8766 // rule as Java's `java_nested_ternary` (which itself mirrors
8767 // the C++ regression for #172).
8768 check_metrics::<GroovyParser>(
8769 "class X {
8770 static String classify(int a, int b) {
8771 if (a > 0) {
8772 return b > 0 ? (b > 10 ? 'big' : 'small') : 'neg'
8773 }
8774 return 'zero'
8775 }
8776 }",
8777 "foo.groovy",
8778 |metric| {
8779 // if(+1, nesting=0) + outer ternary(+1+1=+2, nesting=1)
8780 // + inner ternary(+1+2=+3, nesting=2) = 6
8781 assert_eq!(metric.cognitive.cognitive_sum(), 6);
8782 },
8783 );
8784 }
8785
8786 #[cfg(feature = "csharp")]
8787 #[test]
8788 fn csharp_cognitive_else_if_chain() {
8789 // Regression for #115: else-if chains must not receive a nesting
8790 // increment for the `if` inside `else if`. Expected breakdown:
8791 // if(+1) + else(+1) + else(+1) + else(+1) = 4.
8792 check_metrics::<CsharpParser>(
8793 "class X {
8794 public static void F(int x) {
8795 if (x > 10) {
8796 } else if (x > 5) {
8797 } else if (x > 0) {
8798 } else {
8799 }
8800 }
8801 }",
8802 "foo.cs",
8803 |metric| {
8804 insta::assert_json_snapshot!(
8805 metric.cognitive,
8806 @r#"
8807 {
8808 "sum": 4,
8809 "value": 0,
8810 "average": 4.0,
8811 "min": 0,
8812 "max": 4
8813 }
8814 "#
8815 );
8816 },
8817 );
8818 }
8819
8820 #[cfg(feature = "csharp")]
8821 #[test]
8822 fn csharp_cognitive_nested_else_if() {
8823 // Regression for #115: else-if inside a loop must still respect
8824 // the loop's nesting for the initial `if`, but the `else if`
8825 // branch should only pay a flat +1 via the `else` keyword.
8826 // for(+1) + if at nesting=1(+2) + else(+1) + else(+1) = 5.
8827 check_metrics::<CsharpParser>(
8828 "class X {
8829 public static void F(int x) {
8830 for (int i = 0; i < x; i++) {
8831 if (i > 10) {
8832 } else if (i > 5) {
8833 } else {
8834 }
8835 }
8836 }
8837 }",
8838 "foo.cs",
8839 |metric| {
8840 insta::assert_json_snapshot!(
8841 metric.cognitive,
8842 @r#"
8843 {
8844 "sum": 5,
8845 "value": 0,
8846 "average": 5.0,
8847 "min": 0,
8848 "max": 5
8849 }
8850 "#
8851 );
8852 },
8853 );
8854 }
8855
8856 #[cfg(feature = "csharp")]
8857 #[test]
8858 fn csharp_cognitive_if_inside_else_block_is_not_else_if() {
8859 // Regression for #115: an `if` whose previous sibling is the block's
8860 // opening brace (not the `else` keyword) is a nested independent
8861 // statement, NOT an else-if continuation. It must pay the full
8862 // nesting penalty.
8863 // if(+1, nesting=0) + else(+1) + inner if(+2, nesting=1) = 4.
8864 check_metrics::<CsharpParser>(
8865 "class X {
8866 public static void F(int a, int c) {
8867 if (a > 0) {
8868 } else {
8869 if (c > 0) {
8870 }
8871 }
8872 }
8873 }",
8874 "foo.cs",
8875 |metric| {
8876 insta::assert_json_snapshot!(
8877 metric.cognitive,
8878 @r#"
8879 {
8880 "sum": 4,
8881 "value": 0,
8882 "average": 4.0,
8883 "min": 0,
8884 "max": 4
8885 }
8886 "#
8887 );
8888 },
8889 );
8890 }
8891
8892 #[cfg(feature = "csharp")]
8893 #[test]
8894 fn csharp_sibling_bool_sequences() {
8895 // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested.
8896 // Expected: &&(+1) + ||(+1) + &&(+1) = 3.
8897 check_metrics::<CsharpParser>(
8898 "class X {
8899 bool F(bool a, bool b, bool c, bool d) {
8900 return (a && b) || (c && d);
8901 }
8902 }",
8903 "foo.cs",
8904 |metric| {
8905 assert_eq!(metric.cognitive.cognitive_sum(), 3);
8906 assert_eq!(metric.cognitive.cognitive_max(), 3);
8907 insta::assert_json_snapshot!(metric.cognitive);
8908 },
8909 );
8910 }
8911
8912 #[cfg(feature = "csharp")]
8913 #[test]
8914 fn csharp_nested_bool_same_op() {
8915 // a||(b&&c&&d) — the inner && operators are nested, forming one sequence.
8916 // Expected: ||(+1) + &&(+1) = 2.
8917 check_metrics::<CsharpParser>(
8918 "class X {
8919 bool F(bool a, bool b, bool c, bool d) {
8920 return a || (b && c && d);
8921 }
8922 }",
8923 "foo.cs",
8924 |metric| {
8925 assert_eq!(metric.cognitive.cognitive_sum(), 2);
8926 assert_eq!(metric.cognitive.cognitive_max(), 2);
8927 insta::assert_json_snapshot!(metric.cognitive);
8928 },
8929 );
8930 }
8931
8932 #[cfg(feature = "kotlin")]
8933 #[test]
8934 fn kotlin_sibling_bool_sequences() {
8935 // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested.
8936 // Expected: &&(+1) + ||(+1) + &&(+1) = 3.
8937 check_metrics::<KotlinParser>(
8938 "fun f(a: Boolean, b: Boolean, c: Boolean, d: Boolean) =
8939 (a && b) || (c && d) // +1(&&) +1(||) +1(&&) = 3",
8940 "foo.kt",
8941 |metric| {
8942 assert_eq!(metric.cognitive.cognitive_sum(), 3);
8943 assert_eq!(metric.cognitive.cognitive_max(), 3);
8944 insta::assert_json_snapshot!(metric.cognitive);
8945 },
8946 );
8947 }
8948
8949 #[cfg(feature = "kotlin")]
8950 #[test]
8951 fn kotlin_nested_bool_same_op() {
8952 // a||(b&&c&&d) — the inner && operators are nested, forming one sequence.
8953 // Expected: ||(+1) + &&(+1) = 2.
8954 check_metrics::<KotlinParser>(
8955 "fun f(a: Boolean, b: Boolean, c: Boolean, d: Boolean) =
8956 a || (b && c && d) // +1(||) +1(&&) = 2",
8957 "foo.kt",
8958 |metric| {
8959 assert_eq!(metric.cognitive.cognitive_sum(), 2);
8960 assert_eq!(metric.cognitive.cognitive_max(), 2);
8961 insta::assert_json_snapshot!(metric.cognitive);
8962 },
8963 );
8964 }
8965
8966 #[cfg(feature = "kotlin")]
8967 #[test]
8968 fn kotlin_elvis_chain_239() {
8969 // Regression for issue #239: Kotlin's Elvis operator `?:` is a
8970 // short-circuit nullish operator analogous to JS `??` and must
8971 // form a boolean sequence. `a ?: b ?: c` is a single chain of
8972 // identical operators and collapses to a single +1 under Sonar
8973 // B1 (same rule as `&&` / `||`). Previously the Elvis chain was
8974 // not counted at all (= 0).
8975 check_metrics::<KotlinParser>(
8976 "fun pick(a: String?, b: String?, c: String): String = a ?: b ?: c // +1 (Elvis chain)",
8977 "foo.kt",
8978 |metric| {
8979 assert_eq!(metric.cognitive.cognitive_sum(), 1);
8980 assert_eq!(metric.cognitive.cognitive_max(), 1);
8981 insta::assert_json_snapshot!(
8982 metric.cognitive,
8983 @r#"
8984 {
8985 "sum": 1,
8986 "value": 0,
8987 "average": 1.0,
8988 "min": 0,
8989 "max": 1
8990 }
8991 "#
8992 );
8993 },
8994 );
8995 }
8996
8997 #[cfg(feature = "kotlin")]
8998 #[test]
8999 fn kotlin_elvis_inside_if_239() {
9000 // Regression for issue #239: Elvis chain inside an `if` body.
9001 // Boolean sequences pay a flat +1 (no nesting penalty) per
9002 // Sonar B1: if(+1) + ?: chain(+1) = 2. Previously the Elvis
9003 // chain was not counted at all and the function scored 1.
9004 check_metrics::<KotlinParser>(
9005 "fun f(a: String?, b: String?): String {
9006 if (a != null) { // +1
9007 return a ?: b ?: \"x\" // +1 (Elvis chain)
9008 }
9009 return \"no\"
9010 }",
9011 "foo.kt",
9012 |metric| {
9013 assert_eq!(metric.cognitive.cognitive_sum(), 2);
9014 assert_eq!(metric.cognitive.cognitive_max(), 2);
9015 insta::assert_json_snapshot!(
9016 metric.cognitive,
9017 @r#"
9018 {
9019 "sum": 2,
9020 "value": 0,
9021 "average": 2.0,
9022 "min": 0,
9023 "max": 2
9024 }
9025 "#
9026 );
9027 },
9028 );
9029 }
9030
9031 #[cfg(feature = "go")]
9032 #[test]
9033 fn go_sibling_bool_sequences() {
9034 // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested.
9035 // Expected: &&(+1) + ||(+1) + &&(+1) = 3.
9036 check_metrics::<GoParser>(
9037 "package main
9038 func f(a, b, c, d bool) bool {
9039 return (a && b) || (c && d) // +1(&&) +1(||) +1(&&) = 3
9040 }",
9041 "foo.go",
9042 |metric| {
9043 assert_eq!(metric.cognitive.cognitive_sum(), 3);
9044 assert_eq!(metric.cognitive.cognitive_max(), 3);
9045 insta::assert_json_snapshot!(metric.cognitive);
9046 },
9047 );
9048 }
9049
9050 #[cfg(feature = "go")]
9051 #[test]
9052 fn go_nested_bool_same_op() {
9053 // a||(b&&c&&d) — the inner && operators are nested, forming one sequence.
9054 // Expected: ||(+1) + &&(+1) = 2.
9055 check_metrics::<GoParser>(
9056 "package main
9057 func f(a, b, c, d bool) bool {
9058 return a || (b && c && d) // +1(||) +1(&&) = 2
9059 }",
9060 "foo.go",
9061 |metric| {
9062 assert_eq!(metric.cognitive.cognitive_sum(), 2);
9063 assert_eq!(metric.cognitive.cognitive_max(), 2);
9064 insta::assert_json_snapshot!(metric.cognitive);
9065 },
9066 );
9067 }
9068
9069 #[cfg(feature = "tcl")]
9070 #[test]
9071 fn tcl_sibling_bool_sequences() {
9072 // ($a && $b) || ($c && $d) — the right-hand && is a sibling, not nested.
9073 // Expected: if(+1) + ||(+1) + &&(+1) + &&(+1) = 4.
9074 check_metrics::<TclParser>(
9075 "proc f {a b c d} {
9076 if {($a && $b) || ($c && $d)} {
9077 puts yes
9078 }
9079}",
9080 "foo.tcl",
9081 |metric| {
9082 assert_eq!(metric.cognitive.cognitive_sum(), 4);
9083 assert_eq!(metric.cognitive.cognitive_max(), 4);
9084 insta::assert_json_snapshot!(metric.cognitive);
9085 },
9086 );
9087 }
9088
9089 #[cfg(feature = "tcl")]
9090 #[test]
9091 fn tcl_nested_bool_same_op() {
9092 // $a || ($b && $c && $d) — the inner && operators are nested, one sequence.
9093 // Expected: if(+1) + ||(+1) + &&(+1) = 3.
9094 check_metrics::<TclParser>(
9095 "proc f {a b c d} {
9096 if {$a || ($b && $c && $d)} {
9097 puts yes
9098 }
9099}",
9100 "foo.tcl",
9101 |metric| {
9102 assert_eq!(metric.cognitive.cognitive_sum(), 3);
9103 assert_eq!(metric.cognitive.cognitive_max(), 3);
9104 insta::assert_json_snapshot!(metric.cognitive);
9105 },
9106 );
9107 }
9108
9109 #[cfg(feature = "lua")]
9110 #[test]
9111 fn lua_sibling_bool_sequences() {
9112 // (a and b) or (c and d) — the right-hand `and` is a sibling, not nested.
9113 // Expected: if(+1) + or(+1) + and(+1) + and(+1) = 4.
9114 check_metrics::<LuaParser>(
9115 "local function f(a, b, c, d)
9116 if (a and b) or (c and d) then
9117 return 1
9118 end
9119end",
9120 "foo.lua",
9121 |metric| {
9122 assert_eq!(metric.cognitive.cognitive_sum(), 4);
9123 assert_eq!(metric.cognitive.cognitive_max(), 4);
9124 insta::assert_json_snapshot!(metric.cognitive);
9125 },
9126 );
9127 }
9128
9129 #[cfg(feature = "lua")]
9130 #[test]
9131 fn lua_nested_bool_same_op() {
9132 // a or (b and c and d) — the inner `and` operators are nested, one sequence.
9133 // Expected: if(+1) + or(+1) + and(+1) = 3.
9134 check_metrics::<LuaParser>(
9135 "local function f(a, b, c, d)
9136 if a or (b and c and d) then
9137 return 1
9138 end
9139end",
9140 "foo.lua",
9141 |metric| {
9142 assert_eq!(metric.cognitive.cognitive_sum(), 3);
9143 assert_eq!(metric.cognitive.cognitive_max(), 3);
9144 insta::assert_json_snapshot!(metric.cognitive);
9145 },
9146 );
9147 }
9148
9149 #[cfg(feature = "bash")]
9150 #[test]
9151 fn bash_sibling_bool_sequences() {
9152 // [[ a ]] && [[ b ]] || [[ c ]] && [[ d ]] — bash is left-associative so this
9153 // parses as ((a&&b)||c)&&d with three distinct operator-type transitions.
9154 // Expected: if(+1) + &&(+1) + ||(+1) + &&(+1) = 4.
9155 check_metrics::<BashParser>(
9156 "f() {
9157 if [[ -n \"$a\" ]] && [[ -n \"$b\" ]] || [[ -n \"$c\" ]] && [[ -n \"$d\" ]]; then
9158 echo test
9159 fi
9160 }",
9161 "foo.sh",
9162 |metric| {
9163 assert_eq!(metric.cognitive.cognitive_sum(), 4);
9164 assert_eq!(metric.cognitive.cognitive_max(), 4);
9165 insta::assert_json_snapshot!(metric.cognitive);
9166 },
9167 );
9168 }
9169
9170 #[cfg(feature = "bash")]
9171 #[test]
9172 fn bash_nested_bool_same_op() {
9173 // [[ a ]] || [[ b ]] && [[ c ]] && [[ d ]] — bash left-associativity gives
9174 // ((a||b)&&c)&&d: the two && operators are parent/child so the second is
9175 // a continuation (no extra increment).
9176 // Expected: if(+1) + &&(+1, outer chain) + ||(+1) = 3.
9177 check_metrics::<BashParser>(
9178 "f() {
9179 if [[ -n \"$a\" ]] || [[ -n \"$b\" ]] && [[ -n \"$c\" ]] && [[ -n \"$d\" ]]; then
9180 echo test
9181 fi
9182 }",
9183 "foo.sh",
9184 |metric| {
9185 assert_eq!(metric.cognitive.cognitive_sum(), 3);
9186 assert_eq!(metric.cognitive.cognitive_max(), 3);
9187 insta::assert_json_snapshot!(metric.cognitive);
9188 },
9189 );
9190 }
9191
9192 #[cfg(feature = "php")]
9193 #[test]
9194 fn php_no_cognitive() {
9195 check_metrics::<PhpParser>("<?php $a = 42;", "foo.php", |metric| {
9196 assert_eq!(metric.cognitive.cognitive_sum(), 0);
9197 assert_eq!(metric.cognitive.cognitive_max(), 0);
9198 insta::assert_json_snapshot!(metric.cognitive);
9199 });
9200 }
9201
9202 #[cfg(feature = "php")]
9203 #[test]
9204 fn php_simple_function() {
9205 // Single `if` inside a function: +1.
9206 check_metrics::<PhpParser>(
9207 "<?php
9208 function f(bool $a): void {
9209 if ($a) {
9210 echo 'hi';
9211 }
9212 }",
9213 "foo.php",
9214 |metric| {
9215 assert_eq!(metric.cognitive.cognitive_sum(), 1);
9216 assert_eq!(metric.cognitive.cognitive_max(), 1);
9217 insta::assert_json_snapshot!(metric.cognitive);
9218 },
9219 );
9220 }
9221
9222 #[cfg(feature = "php")]
9223 #[test]
9224 fn php_nested_function_resets_nesting_775() {
9225 // Regression for #775 (the #696 gap): a PHP named function defined
9226 // inside control flow must reset structural nesting to 0 at the
9227 // definition boundary and pick up the +1 function-depth surcharge,
9228 // exactly like Java/C/Rust/etc. Before the fix, `inner` inherited
9229 // `outer`'s leaked nesting (2 by the time the definition is reached)
9230 // and scored its body against it: `inner` was 7 (and the file 10).
9231 //
9232 // After the fix, inside `inner` nesting resets to 0 and depth = 1
9233 // (it is nested in `outer`), so:
9234 // inner `if ($b)`: structural += (nesting 0 + depth 1) + 1 = 2
9235 // inner `if ($d)`: structural += (nesting 1 + depth 1) + 1 = 3
9236 // => inner = 5
9237 // `outer` itself (excluding the nested space) is:
9238 // `if ($a)`: +1 (nesting 0→1); `if ($c)`: +2 (nesting 1→2) => 3
9239 // so the file sum is 3 + 5 = 8, max = 5 (the `inner` space).
9240 check_metrics::<PhpParser>(
9241 "<?php
9242 function outer() {
9243 if ($a) {
9244 if ($c) {
9245 function inner() {
9246 if ($b) {
9247 if ($d) {
9248 echo 'x';
9249 }
9250 }
9251 }
9252 }
9253 }
9254 }",
9255 "foo.php",
9256 |metric| {
9257 assert_eq!(metric.cognitive.cognitive_sum(), 8);
9258 assert_eq!(metric.cognitive.cognitive_max(), 5);
9259 },
9260 );
9261 }
9262
9263 #[cfg(feature = "php")]
9264 #[test]
9265 fn php_top_level_function_unchanged_775() {
9266 // Regression guard paired with `php_nested_function_resets_nesting_775`:
9267 // a *top-level* PHP function with the same body is unaffected by the
9268 // #775 fix — nesting is already 0 and depth is 0 there. The two
9269 // `if` statements score +1 and +2 respectively, so cognitive = 3.
9270 // If the #775 boundary arm ever over-fires on top-level functions,
9271 // this value moves and the test fails.
9272 check_metrics::<PhpParser>(
9273 "<?php
9274 function inner() {
9275 if ($b) {
9276 if ($d) {
9277 echo 'x';
9278 }
9279 }
9280 }",
9281 "foo.php",
9282 |metric| {
9283 assert_eq!(metric.cognitive.cognitive_sum(), 3);
9284 assert_eq!(metric.cognitive.cognitive_max(), 3);
9285 },
9286 );
9287 }
9288
9289 #[cfg(feature = "php")]
9290 #[test]
9291 fn php_if_elseif_else() {
9292 // PHP exposes `elseif` as a dedicated `else_if_clause` node, scored
9293 // as a branch extension (+1, no nesting) via the `ElseIfClause` arm
9294 // — parallel to bash/perl/ruby. An `if … elseif … else` chain is
9295 // therefore +1 each = 3. PHP previously had no cognitive test for
9296 // the `elseif` dispatch; this pins it.
9297 //
9298 // Note: the one-word `elseif` parses as its own `else_if_clause`
9299 // node, dispatched directly to the branch-extension arm, so
9300 // `is_else_if` is never consulted on this path. PHP's *two-word*
9301 // `else if` is the nested-`if` shape that C++/JS/Java have, and it
9302 // does go through the `IfStatement if !Self::is_else_if` guard —
9303 // see `php_two_word_else_if_529` (#529).
9304 check_metrics::<PhpParser>(
9305 "<?php
9306 function f(int $a): void {
9307 if ($a > 0) { // +1
9308 echo 'pos';
9309 } elseif ($a < 0) { // +1
9310 echo 'neg';
9311 } else { // +1
9312 echo 'zero';
9313 }
9314 }",
9315 "foo.php",
9316 |metric| {
9317 assert_eq!(metric.cognitive.cognitive_sum(), 3);
9318 assert_eq!(metric.cognitive.cognitive_max(), 3);
9319 insta::assert_json_snapshot!(
9320 metric.cognitive,
9321 @r#"
9322 {
9323 "sum": 3,
9324 "value": 0,
9325 "average": 3.0,
9326 "min": 0,
9327 "max": 3
9328 }
9329 "#
9330 );
9331 },
9332 );
9333 }
9334
9335 #[cfg(feature = "php")]
9336 #[test]
9337 fn php_two_word_else_if_529() {
9338 // PHP's two-word `else if` parses as an `else_clause` wrapping a
9339 // nested `if_statement` (`else_clause → if_statement`), unlike the
9340 // one-word `elseif` keyword which is a dedicated `else_if_clause`
9341 // node. Before #529 the nested `IfStatement` fell through PHP's
9342 // unguarded cognitive `IfStatement` arm: it fired `increase_nesting`
9343 // (+1, plus an inflated nesting level for later arms) on top of the
9344 // wrapping `else_clause`'s branch extension (+1), so the chain below
9345 // scored 5 instead of the correct 3 — and worse for deeper chains.
9346 //
9347 // Correct SonarSource value: `if` +1, `else if` +1 branch
9348 // extension, `else` +1 = 3. The fix adds the
9349 // `IfStatement if !Self::is_else_if(node)` guard and teaches PHP's
9350 // `is_else_if` to recognize the `else_clause → if_statement` shape.
9351 // This test guards both halves: it scores identically to the
9352 // one-word `php_if_elseif_else` form. Verified by revert — against
9353 // pre-#529 code it asserts 5 and fails.
9354 check_metrics::<PhpParser>(
9355 "<?php
9356 function f(int $a): void {
9357 if ($a == 1) { // +1
9358 echo 'one';
9359 } else if ($a == 2) { // +1 branch extension, no nesting
9360 echo 'two';
9361 } else { // +1 branch extension
9362 echo 'zero';
9363 }
9364 }",
9365 "foo.php",
9366 |metric| {
9367 assert_eq!(metric.cognitive.cognitive_sum(), 3);
9368 assert_eq!(metric.cognitive.cognitive_max(), 3);
9369 insta::assert_json_snapshot!(
9370 metric.cognitive,
9371 @r#"
9372 {
9373 "sum": 3,
9374 "value": 0,
9375 "average": 3.0,
9376 "min": 0,
9377 "max": 3
9378 }
9379 "#
9380 );
9381 },
9382 );
9383 }
9384
9385 #[cfg(feature = "php")]
9386 #[test]
9387 fn php_two_word_else_if_chain_nesting_529() {
9388 // A genuinely nested `if` inside a two-word `else if` arm must still
9389 // pay its nesting penalty — the #529 guard suppresses only the
9390 // else-if-continuation `IfStatement`, not real nesting. Here the
9391 // inner `if ($a > 0)` sits one level deep inside the `else if` arm:
9392 // `if` +1, `else if` +1, inner `if` +2 (base + nesting), final
9393 // `else` +1 = 5. Pre-#529 the misattributed nesting inflated this
9394 // super-linearly; this pins the corrected total and confirms the
9395 // guard does not over-suppress real nesting.
9396 check_metrics::<PhpParser>(
9397 "<?php
9398 function f(int $a): void {
9399 if ($a == 1) { // +1
9400 echo 'one';
9401 } else if ($a == 2) { // +1
9402 if ($a > 0) { // +2 (base + nesting)
9403 echo 'pos';
9404 }
9405 } else { // +1
9406 echo 'zero';
9407 }
9408 }",
9409 "foo.php",
9410 |metric| {
9411 assert_eq!(metric.cognitive.cognitive_sum(), 5);
9412 assert_eq!(metric.cognitive.cognitive_max(), 5);
9413 insta::assert_json_snapshot!(
9414 metric.cognitive,
9415 @r#"
9416 {
9417 "sum": 5,
9418 "value": 0,
9419 "average": 5.0,
9420 "min": 0,
9421 "max": 5
9422 }
9423 "#
9424 );
9425 },
9426 );
9427 }
9428
9429 #[cfg(feature = "php")]
9430 #[test]
9431 fn php_alternative_syntax_elseif_529() {
9432 // PHP's alternative (colon) syntax `if …: … elseif …: … else: …
9433 // endif;` requires the one-word `elseif` keyword — two-word
9434 // `else if` is a PHP fatal parse error there (the grammar emits an
9435 // `ERROR` node). The valid one-word form parses as the dedicated
9436 // `else_if_clause` node, scored as a branch extension (+1, no
9437 // nesting) just like the brace form. Discovered while fixing #529:
9438 // pins that the colon-syntax `elseif` chain scores 3, the same as
9439 // the brace `php_if_elseif_else` form, and guards against a future
9440 // change that mishandles the alternative-syntax dispatch.
9441 check_metrics::<PhpParser>(
9442 "<?php
9443 function f(int $a): void {
9444 if ($a > 0): // +1
9445 echo 'pos';
9446 elseif ($a < 0): // +1 branch extension, no nesting
9447 echo 'neg';
9448 else: // +1 branch extension
9449 echo 'zero';
9450 endif;
9451 }",
9452 "foo.php",
9453 |metric| {
9454 assert_eq!(metric.cognitive.cognitive_sum(), 3);
9455 assert_eq!(metric.cognitive.cognitive_max(), 3);
9456 insta::assert_json_snapshot!(
9457 metric.cognitive,
9458 @r#"
9459 {
9460 "sum": 3,
9461 "value": 0,
9462 "average": 3.0,
9463 "min": 0,
9464 "max": 3
9465 }
9466 "#
9467 );
9468 },
9469 );
9470 }
9471
9472 #[cfg(feature = "php")]
9473 #[test]
9474 fn php_ternary() {
9475 // PHP's ternary `?:` (grammar `conditional_expression`) is a
9476 // conditional construct: +1 base + nesting. Regression test for
9477 // issue #224. Note: this differs from PHP's
9478 // `match_conditional_expression` (the `match` expression),
9479 // which is handled separately by `MatchExpression`.
9480 check_metrics::<PhpParser>(
9481 "<?php
9482 function check(int $a): bool {
9483 return $a > 0 ? true : false; // +1
9484 }",
9485 "foo.php",
9486 |metric| {
9487 assert_eq!(metric.cognitive.cognitive_sum(), 1);
9488 assert_eq!(metric.cognitive.cognitive_max(), 1);
9489 insta::assert_json_snapshot!(
9490 metric.cognitive,
9491 @r#"
9492 {
9493 "sum": 1,
9494 "value": 0,
9495 "average": 1.0,
9496 "min": 0,
9497 "max": 1
9498 }
9499 "#
9500 );
9501 },
9502 );
9503 }
9504
9505 #[cfg(feature = "php")]
9506 #[test]
9507 fn php_nested_ternary() {
9508 // Nested ternaries inside an `if` compound by nesting (mirrors
9509 // the C++ regression test for #172).
9510 // expected: if (+1) + outer ternary (+2, nesting=1) + inner
9511 // ternary (+3, nesting=2) = 6.
9512 check_metrics::<PhpParser>(
9513 "<?php
9514 function classify(int $a, int $b): string {
9515 if ($a > 0) { // +1
9516 return $b > 0 ? ($b > 10 ? 'big' : 'small') : 'neg'; // +2, +3
9517 }
9518 return 'zero';
9519 }",
9520 "foo.php",
9521 |metric| {
9522 assert_eq!(metric.cognitive.cognitive_sum(), 6);
9523 assert_eq!(metric.cognitive.cognitive_max(), 6);
9524 insta::assert_json_snapshot!(
9525 metric.cognitive,
9526 @r#"
9527 {
9528 "sum": 6,
9529 "value": 0,
9530 "average": 6.0,
9531 "min": 0,
9532 "max": 6
9533 }
9534 "#
9535 );
9536 },
9537 );
9538 }
9539
9540 #[cfg(feature = "php")]
9541 #[test]
9542 fn php_sequence_same_booleans() {
9543 // Sequence of same-operator booleans collapses: a chain of `&&`
9544 // counts as +1 total, not per-operand.
9545 check_metrics::<PhpParser>(
9546 "<?php
9547 function f(bool $a, bool $b, bool $c): bool {
9548 return $a && $b && $c;
9549 }",
9550 "foo.php",
9551 |metric| {
9552 // Chain of identical && collapses to a single +1.
9553 assert_eq!(metric.cognitive.cognitive_sum(), 1);
9554 assert_eq!(metric.cognitive.cognitive_max(), 1);
9555 insta::assert_json_snapshot!(metric.cognitive);
9556 },
9557 );
9558 }
9559
9560 #[cfg(feature = "php")]
9561 #[test]
9562 fn php_sequence_different_booleans() {
9563 // Mix of `&&` and `||` — each operator switch costs +1.
9564 check_metrics::<PhpParser>(
9565 "<?php
9566 function f(bool $a, bool $b, bool $c): bool {
9567 return $a && $b || $c;
9568 }",
9569 "foo.php",
9570 |metric| {
9571 // && chain (+1) + switch to || (+1) = 2.
9572 assert_eq!(metric.cognitive.cognitive_sum(), 2);
9573 assert_eq!(metric.cognitive.cognitive_max(), 2);
9574 insta::assert_json_snapshot!(metric.cognitive);
9575 },
9576 );
9577 }
9578
9579 #[cfg(feature = "php")]
9580 #[test]
9581 fn php_not_booleans() {
9582 // `!` does not break boolean sequences (issue #392): pre-order
9583 // visits the outer `&&` BinaryExpression first, so the inner
9584 // `&&` lies within its span and is a continuation.
9585 check_metrics::<PhpParser>(
9586 "<?php
9587 function f(bool $a, bool $b, bool $c): bool {
9588 return $a && !($b && $c);
9589 }",
9590 "foo.php",
9591 |metric| {
9592 // Outer && (+1); inner && continues outer's span → 1.
9593 assert_eq!(metric.cognitive.cognitive_sum(), 1);
9594 assert_eq!(metric.cognitive.cognitive_max(), 1);
9595 insta::assert_json_snapshot!(metric.cognitive);
9596 },
9597 );
9598 }
9599
9600 #[cfg(feature = "php")]
9601 #[test]
9602 fn php_1_level_nesting() {
9603 // if-inside-loop: outer for (+1) + inner if at depth 1 (+2) = +3.
9604 check_metrics::<PhpParser>(
9605 "<?php
9606 function f(int $n): int {
9607 for ($i = 0; $i < $n; $i++) {
9608 if ($i % 2 === 0) {
9609 return $i;
9610 }
9611 }
9612 return -1;
9613 }",
9614 "foo.php",
9615 |metric| {
9616 // for(+1) + if at depth 1 (+2) = 3.
9617 assert_eq!(metric.cognitive.cognitive_sum(), 3);
9618 assert_eq!(metric.cognitive.cognitive_max(), 3);
9619 insta::assert_json_snapshot!(metric.cognitive);
9620 },
9621 );
9622 }
9623
9624 #[cfg(feature = "php")]
9625 #[test]
9626 fn php_2_level_nesting() {
9627 // for + while + if = +1 +2 +3 = +6.
9628 check_metrics::<PhpParser>(
9629 "<?php
9630 function f(int $n): int {
9631 for ($i = 0; $i < $n; $i++) {
9632 while ($i > 0) {
9633 if ($i % 2 === 0) {
9634 return $i;
9635 }
9636 }
9637 }
9638 return -1;
9639 }",
9640 "foo.php",
9641 |metric| {
9642 // for(+1) + while at depth 1 (+2) + if at depth 2 (+3) = 6.
9643 assert_eq!(metric.cognitive.cognitive_sum(), 6);
9644 assert_eq!(metric.cognitive.cognitive_max(), 6);
9645 insta::assert_json_snapshot!(metric.cognitive);
9646 },
9647 );
9648 }
9649
9650 #[cfg(feature = "php")]
9651 #[test]
9652 fn php_break_continue() {
9653 // PHP `break` and `continue` are not cognitive drivers in this
9654 // impl; only the surrounding loops count.
9655 check_metrics::<PhpParser>(
9656 "<?php
9657 function f(int $n): int {
9658 for ($i = 0; $i < $n; $i++) {
9659 if ($i % 2 === 0) {
9660 continue;
9661 }
9662 if ($i > 100) {
9663 break;
9664 }
9665 }
9666 return 0;
9667 }",
9668 "foo.php",
9669 |metric| {
9670 // for(+1) + first if at depth 1 (+2) + second if at depth 1 (+2) = 5.
9671 assert_eq!(metric.cognitive.cognitive_sum(), 5);
9672 assert_eq!(metric.cognitive.cognitive_max(), 5);
9673 insta::assert_json_snapshot!(metric.cognitive);
9674 },
9675 );
9676 }
9677
9678 #[cfg(feature = "php")]
9679 #[test]
9680 fn php_goto_counted() {
9681 // `goto label;` is a genuinely unstructured jump and adds +1 per
9682 // SonarSource Cognitive Complexity §B2 (issue #435), matching
9683 // C++/C#/Go/Perl/Lua goto handling.
9684 check_metrics::<PhpParser>(
9685 "<?php
9686 function f(int $n): int {
9687 if ($n < 0) {
9688 goto done;
9689 }
9690 done:
9691 return 0;
9692 }",
9693 "foo.php",
9694 |metric| {
9695 // if(+1) + goto(+1) = 2.
9696 assert_eq!(metric.cognitive.cognitive_sum(), 2);
9697 assert_eq!(metric.cognitive.cognitive_max(), 2);
9698 insta::assert_json_snapshot!(metric.cognitive);
9699 },
9700 );
9701 }
9702
9703 #[cfg(feature = "php")]
9704 #[test]
9705 fn php_numeric_break_not_counted() {
9706 // PHP has no labeled break/continue; only the numeric level form
9707 // `break N;` / `continue N;`, which exits N enclosing loops already
9708 // accounted for by nesting. Per issue #435 the numeric form is a
9709 // structured loop-level exit and adds +0.
9710 check_metrics::<PhpParser>(
9711 "<?php
9712 function f(int $n): int {
9713 for ($i = 0; $i < $n; $i++) {
9714 while (true) {
9715 if ($i > 100) {
9716 break 2;
9717 }
9718 }
9719 }
9720 return 0;
9721 }",
9722 "foo.php",
9723 |metric| {
9724 // for(+1) + while at depth 1 (+2) + if at depth 2 (+3) = 6;
9725 // `break 2` adds +0.
9726 assert_eq!(metric.cognitive.cognitive_sum(), 6);
9727 assert_eq!(metric.cognitive.cognitive_max(), 6);
9728 insta::assert_json_snapshot!(metric.cognitive);
9729 },
9730 );
9731 }
9732
9733 // ----- Elixir -----
9734
9735 // No control flow → cognitive complexity is 0.
9736 #[cfg(feature = "elixir")]
9737 #[test]
9738 fn elixir_empty_function() {
9739 check_metrics::<ElixirParser>(
9740 "defmodule Foo do\n def f(x) do\n x\n end\nend\n",
9741 "foo.ex",
9742 |metric| {
9743 assert_eq!(metric.cognitive.cognitive_sum(), 0);
9744 insta::assert_json_snapshot!(
9745 metric.cognitive,
9746 @r#"
9747 {
9748 "sum": 0,
9749 "value": 0,
9750 "average": 0.0,
9751 "min": 0,
9752 "max": 0
9753 }
9754 "#
9755 );
9756 },
9757 );
9758 }
9759
9760 // `if cond do … end`: single-branch construct → +1 nesting at depth
9761 // 0 inside `def` body → cognitive 1.
9762 #[cfg(feature = "elixir")]
9763 #[test]
9764 fn elixir_simple_if() {
9765 check_metrics::<ElixirParser>(
9766 "defmodule Foo do\n def f(x) do\n if x > 0 do\n :pos\n end\n end\nend\n",
9767 "foo.ex",
9768 |metric| {
9769 assert_eq!(metric.cognitive.cognitive_sum(), 1);
9770 insta::assert_json_snapshot!(metric.cognitive);
9771 },
9772 );
9773 }
9774
9775 // `if cond do … else … end`: +1 nesting for `if`, +1 for `else` token
9776 // (matches Java/Kotlin) → cognitive 2.
9777 #[cfg(feature = "elixir")]
9778 #[test]
9779 fn elixir_if_else() {
9780 check_metrics::<ElixirParser>(
9781 "defmodule Foo do\n def f(x) do\n if x > 0 do\n :pos\n else\n :neg\n end\n end\nend\n",
9782 "foo.ex",
9783 |metric| {
9784 // expected: if (+1) + else (+1) = 2
9785 assert_eq!(metric.cognitive.cognitive_sum(), 2);
9786 insta::assert_json_snapshot!(metric.cognitive);
9787 },
9788 );
9789 }
9790
9791 // `case x do … end` with three arms: only the container Call earns
9792 // a nesting bump (matches Java's `SwitchBlock` rule). Individual
9793 // `stab_clause` arms add no extra cost. Expected cognitive 1.
9794 #[cfg(feature = "elixir")]
9795 #[test]
9796 fn elixir_case_arms_count_once() {
9797 check_metrics::<ElixirParser>(
9798 "defmodule Foo do\n def f(x) do\n case x do\n 1 -> :one\n 2 -> :two\n _ -> :other\n end\n end\nend\n",
9799 "foo.ex",
9800 |metric| {
9801 // expected: case +1 (one nesting bump on the container)
9802 assert_eq!(metric.cognitive.cognitive_sum(), 1);
9803 insta::assert_json_snapshot!(metric.cognitive);
9804 },
9805 );
9806 }
9807
9808 // `cond do … end` is structurally identical to `case` for our
9809 // purposes: container Call earns +1 nesting; arms add nothing.
9810 #[cfg(feature = "elixir")]
9811 #[test]
9812 fn elixir_cond_counts_once() {
9813 check_metrics::<ElixirParser>(
9814 "defmodule Foo do\n def f(x) do\n cond do\n x > 0 -> :pos\n x < 0 -> :neg\n true -> :zero\n end\n end\nend\n",
9815 "foo.ex",
9816 |metric| {
9817 // expected: cond +1
9818 assert_eq!(metric.cognitive.cognitive_sum(), 1);
9819 insta::assert_json_snapshot!(metric.cognitive);
9820 },
9821 );
9822 }
9823
9824 // Nested `if` inside another `if`: outer +1, inner +2 (nested
9825 // depth 1) → cognitive 3.
9826 #[cfg(feature = "elixir")]
9827 #[test]
9828 fn elixir_nested_if_amplifies() {
9829 check_metrics::<ElixirParser>(
9830 "defmodule Foo do\n def f(x, y) do\n if x > 0 do\n if y > 0 do\n :both\n end\n end\n end\nend\n",
9831 "foo.ex",
9832 |metric| {
9833 // expected: outer if (+1) + nested if (+2 because nesting=1)
9834 assert_eq!(metric.cognitive.cognitive_sum(), 3);
9835 insta::assert_json_snapshot!(metric.cognitive);
9836 },
9837 );
9838 }
9839
9840 // `try` with `rescue` and `catch`: the `try` wrapper itself does
9841 // NOT bump nesting (matches Java / C#'s "try is a wrapper" rule);
9842 // each `rescue` / `catch` block bumps +1 nesting at depth 0. The
9843 // single `stab_clause` inside each block adds no extra cost.
9844 #[cfg(feature = "elixir")]
9845 #[test]
9846 fn elixir_try_rescue_catch() {
9847 check_metrics::<ElixirParser>(
9848 "defmodule Foo do\n def f do\n try do\n :ok\n rescue\n _ -> :err\n catch\n _ -> :thrown\n end\n end\nend\n",
9849 "foo.ex",
9850 |metric| {
9851 // expected: rescue (+1) + catch (+1) = 2
9852 assert_eq!(metric.cognitive.cognitive_sum(), 2);
9853 insta::assert_json_snapshot!(metric.cognitive);
9854 },
9855 );
9856 }
9857
9858 // Short-circuit booleans: `x && y || z` is two operator types in
9859 // sequence — `&&` once, `||` once → +2. The `if` container that
9860 // surrounds them adds +1 → total cognitive 3.
9861 #[cfg(feature = "elixir")]
9862 #[test]
9863 fn elixir_boolean_sequence() {
9864 check_metrics::<ElixirParser>(
9865 "defmodule Foo do\n def f(x, y, z) do\n if x && y || z do\n :hit\n end\n end\nend\n",
9866 "foo.ex",
9867 |metric| {
9868 // expected: if (+1) + && (+1) + || (+1) = 3
9869 assert_eq!(metric.cognitive.cognitive_sum(), 3);
9870 insta::assert_json_snapshot!(metric.cognitive);
9871 },
9872 );
9873 }
9874
9875 // `Enum.reduce` (and friends) are higher-order calls, NOT control
9876 // flow per the SonarSource spec. They contribute nothing to
9877 // cognitive complexity. The anonymous function body inside
9878 // contributes +1 lambda nesting, but its only operation is a
9879 // function call (no control flow) → cognitive 0.
9880 #[cfg(feature = "elixir")]
9881 #[test]
9882 fn elixir_enum_reduce_is_zero() {
9883 check_metrics::<ElixirParser>(
9884 "defmodule Foo do\n def sum(xs) do\n Enum.reduce(xs, 0, fn x, acc -> acc + x end)\n end\nend\n",
9885 "foo.ex",
9886 |metric| {
9887 // expected: 0 — Enum.reduce is a function call, not
9888 // syntactic control flow; the `fn` body has no decisions.
9889 assert_eq!(metric.cognitive.cognitive_sum(), 0);
9890 insta::assert_json_snapshot!(metric.cognitive);
9891 },
9892 );
9893 }
9894
9895 // Recursion: a `def` whose body calls itself by name. Per the
9896 // SonarSource spec recursion is +1, but our impl skips it for
9897 // scope reasons (documented). The body's lone Call earns nothing,
9898 // so cognitive stays at 0. This test pins the documented omission
9899 // so any future recursion work has to update it deliberately.
9900 #[cfg(feature = "elixir")]
9901 #[test]
9902 fn elixir_recursion_is_zero_documented_limitation() {
9903 check_metrics::<ElixirParser>(
9904 "defmodule Foo do\n def fact(0), do: 1\n def fact(n), do: n * fact(n - 1)\nend\n",
9905 "foo.ex",
9906 |metric| {
9907 assert_eq!(metric.cognitive.cognitive_sum(), 0);
9908 insta::assert_json_snapshot!(metric.cognitive);
9909 },
9910 );
9911 }
9912
9913 #[cfg(feature = "php")]
9914 #[test]
9915 fn php_match_cognitive() {
9916 // `match` is treated like `switch`: a single nesting bump for the
9917 // whole construct, not per arm.
9918 check_metrics::<PhpParser>(
9919 "<?php
9920 function color(string $c): int {
9921 return match ($c) {
9922 'red' => 1,
9923 'green' => 2,
9924 default => 0,
9925 };
9926 }",
9927 "foo.php",
9928 |metric| {
9929 // `match` is treated like `switch`: a single +1 for the construct.
9930 assert_eq!(metric.cognitive.cognitive_sum(), 1);
9931 assert_eq!(metric.cognitive.cognitive_max(), 1);
9932 insta::assert_json_snapshot!(metric.cognitive);
9933 },
9934 );
9935 }
9936
9937 #[cfg(feature = "ruby")]
9938 #[test]
9939 fn ruby_no_cognitive() {
9940 check_metrics::<RubyParser>("a = 42\n", "foo.rb", |metric| {
9941 assert_eq!(metric.cognitive.cognitive_sum(), 0);
9942 insta::assert_json_snapshot!(metric.cognitive);
9943 });
9944 }
9945
9946 #[cfg(feature = "ruby")]
9947 #[test]
9948 fn ruby_simple_function() {
9949 // A function body with no branching scores zero cognitive.
9950 check_metrics::<RubyParser>("def foo\n a = 1\nend\n", "foo.rb", |metric| {
9951 assert_eq!(metric.cognitive.cognitive_sum(), 0);
9952 insta::assert_json_snapshot!(metric.cognitive);
9953 });
9954 }
9955
9956 #[cfg(feature = "ruby")]
9957 #[test]
9958 fn ruby_1_level_nesting() {
9959 // Single `if` inside a function: +1.
9960 check_metrics::<RubyParser>("def foo\n if a\n b\n end\nend\n", "foo.rb", |metric| {
9961 assert_eq!(metric.cognitive.cognitive_sum(), 1);
9962 insta::assert_json_snapshot!(metric.cognitive);
9963 });
9964 }
9965
9966 #[cfg(feature = "ruby")]
9967 #[test]
9968 fn ruby_2_level_nesting() {
9969 // expected: outer `if` (+1) + inner `if` (+2, nested) = 3.
9970 check_metrics::<RubyParser>(
9971 "def foo\n if a\n if b\n c\n end\n end\nend\n",
9972 "foo.rb",
9973 |metric| {
9974 assert_eq!(metric.cognitive.cognitive_sum(), 3);
9975 insta::assert_json_snapshot!(metric.cognitive);
9976 },
9977 );
9978 }
9979
9980 #[cfg(feature = "ruby")]
9981 #[test]
9982 fn ruby_sequence_same_booleans() {
9983 // `a && b && c`: same operator collapses to a single boolean
9984 // sequence (+1). Plus the enclosing `if` (+1) → 2.
9985 check_metrics::<RubyParser>(
9986 "def foo\n if a && b && c\n d\n end\nend\n",
9987 "foo.rb",
9988 |metric| {
9989 assert_eq!(metric.cognitive.cognitive_sum(), 2);
9990 insta::assert_json_snapshot!(metric.cognitive);
9991 },
9992 );
9993 }
9994
9995 #[cfg(feature = "ruby")]
9996 #[test]
9997 fn ruby_sequence_different_booleans() {
9998 // `a && b || c`: alternating operators add per change.
9999 check_metrics::<RubyParser>(
10000 "def foo\n if a && b || c\n d\n end\nend\n",
10001 "foo.rb",
10002 |metric| {
10003 assert_eq!(metric.cognitive.cognitive_sum(), 3);
10004 insta::assert_json_snapshot!(metric.cognitive);
10005 },
10006 );
10007 }
10008
10009 #[cfg(feature = "ruby")]
10010 #[test]
10011 fn ruby_not_booleans() {
10012 // `!a` (Unary) is the not-operator: it doesn't add cognitive
10013 // load by itself. Only the enclosing `if` counts.
10014 check_metrics::<RubyParser>(
10015 "def foo\n if !a\n b\n end\nend\n",
10016 "foo.rb",
10017 |metric| {
10018 assert_eq!(metric.cognitive.cognitive_sum(), 1);
10019 insta::assert_json_snapshot!(metric.cognitive);
10020 },
10021 );
10022 }
10023
10024 #[cfg(feature = "ruby")]
10025 #[test]
10026 fn ruby_break_next() {
10027 // Ruby has no labeled loops, so `break`/`next` are always
10028 // unlabeled. Per SonarSource Cognitive Complexity §B2 an unlabeled
10029 // break/continue adds +0 (issue #435) — only the enclosing `while`
10030 // (+1) counts → 1.
10031 check_metrics::<RubyParser>(
10032 "def foo\n while a\n break\n next\n end\nend\n",
10033 "foo.rb",
10034 |metric| {
10035 assert_eq!(metric.cognitive.cognitive_sum(), 1);
10036 insta::assert_json_snapshot!(metric.cognitive);
10037 },
10038 );
10039 }
10040
10041 #[cfg(feature = "ruby")]
10042 #[test]
10043 fn ruby_redo_retry_counted() {
10044 // `redo` (restart the current loop iteration) and `retry` (re-run a
10045 // rescued `begin` block) are genuinely unstructured jumps with no
10046 // structured equivalent, so each adds +1 per SonarSource §B2
10047 // (issue #435) even though `break`/`next` do not.
10048 check_metrics::<RubyParser>(
10049 "def foo\n while a\n redo\n end\n begin\n work\n rescue\n retry\n end\nend\n",
10050 "foo.rb",
10051 |metric| {
10052 // while(+1) + redo(+1) + rescue(+1) + retry(+1) = 4.
10053 assert_eq!(metric.cognitive.cognitive_sum(), 4);
10054 insta::assert_json_snapshot!(metric.cognitive);
10055 },
10056 );
10057 }
10058
10059 #[cfg(feature = "ruby")]
10060 #[test]
10061 fn ruby_else_if_chain() {
10062 // `elsif` extends the parent branch (no extra nesting). An
10063 // `if/elsif/elsif/else` chain scores strictly LESS than the
10064 // same number of nested `if` blocks. tree-sitter-ruby gives
10065 // `elsif` its own clause node, so the lesson-10 trap (a buggy
10066 // `is_else_if` that returns false makes `elsif` nest like
10067 // `if`) doesn't apply directly here — the test still pins the
10068 // chain vs nested cost difference so a future refactor that
10069 // mis-classifies `Elsif` would regress it.
10070 // expected: chain = 1 (`if`) + 2 (two `elsif`) + 1 (`else`) = 4;
10071 // nested = 1 + 2 + 3 = 6. The literal `4 < 6` asserts the
10072 // intended relationship.
10073 check_metrics::<RubyParser>(
10074 "def foo\n if a\n 1\n elsif b\n 2\n elsif c\n 3\n else\n 4\n end\nend\n",
10075 "foo.rb",
10076 |metric| {
10077 assert_eq!(metric.cognitive.cognitive_sum(), 4);
10078 insta::assert_json_snapshot!(metric.cognitive);
10079 },
10080 );
10081 check_metrics::<RubyParser>(
10082 "def foo\n if a\n if b\n if c\n 1\n end\n end\n end\nend\n",
10083 "foo.rb",
10084 |metric| {
10085 assert_eq!(metric.cognitive.cognitive_sum(), 6);
10086 },
10087 );
10088 }
10089
10090 #[cfg(feature = "ruby")]
10091 #[test]
10092 fn ruby_case_else_no_extra_increment() {
10093 // #451: the `else` arm of a `case/when` is the default arm of a
10094 // switch-like construct. The `case` node already pays nesting
10095 // (+1), so the default arm must add +0 — adding `else` to a
10096 // `case` must not change the cognitive score.
10097 //
10098 // Pre-fix, the shared `R::Elsif | R::Else` arm added +1 to the
10099 // case-`else`, scoring 2 (revert-verified). Now both forms score 1.
10100 let case_with_else = "case x\nwhen 1 then 1\nelse 0\nend\n";
10101 let case_without_else = "case x\nwhen 1 then 1\nwhen 2 then 2\nend\n";
10102 check_metrics::<RubyParser>(case_with_else, "foo.rb", |metric| {
10103 assert_eq!(metric.cognitive.cognitive_sum(), 1);
10104 insta::assert_json_snapshot!(metric.cognitive, @r#"
10105 {
10106 "sum": 1,
10107 "value": 1,
10108 "average": 1.0,
10109 "min": 1,
10110 "max": 1
10111 }
10112 "#);
10113 });
10114 check_metrics::<RubyParser>(case_without_else, "foo.rb", |metric| {
10115 assert_eq!(metric.cognitive.cognitive_sum(), 1);
10116 });
10117 }
10118
10119 #[cfg(all(feature = "java", feature = "kotlin", feature = "ruby"))]
10120 #[test]
10121 fn ruby_case_else_matches_kotlin_when_and_java_switch() {
10122 // #451 cross-language parity (lesson #11): the catch-all arm of a
10123 // switch-like construct scores identically across languages. Ruby
10124 // `case`/`else`, Kotlin `when`/`else`, and Java `switch`/`default`
10125 // must all report cognitive == 1 on the equivalent two-branch
10126 // construct (one match arm + the default arm).
10127 check_metrics::<RubyParser>("case x\nwhen 1 then 1\nelse 0\nend\n", "foo.rb", |metric| {
10128 assert_eq!(metric.cognitive.cognitive_sum(), 1);
10129 });
10130 check_metrics::<KotlinParser>(
10131 "fun f(x: Int): Int {\n return when (x) {\n 1 -> 1\n else -> 0\n }\n}\n",
10132 "foo.kt",
10133 |metric| {
10134 assert_eq!(metric.cognitive.cognitive_sum(), 1);
10135 },
10136 );
10137 check_metrics::<JavaParser>(
10138 "class C {\n int f(int x) {\n switch (x) {\n case 1: return 1;\n default: return 0;\n }\n }\n}\n",
10139 "foo.java",
10140 |metric| {
10141 assert_eq!(metric.cognitive.cognitive_sum(), 1);
10142 },
10143 );
10144 }
10145
10146 #[cfg(feature = "ruby")]
10147 #[test]
10148 fn ruby_if_else_still_counts() {
10149 // #451 over-suppression guard: the `else` of an `if`/`elsif` chain
10150 // is *not* switch-like (its parent is the `if`/`elsif` clause, not a
10151 // `case`), so it must still add +1. `if`(+1) + `else`(+1) = 2.
10152 check_metrics::<RubyParser>("if a\n 1\nelse\n 2\nend\n", "foo.rb", |metric| {
10153 assert_eq!(metric.cognitive.cognitive_sum(), 2);
10154 });
10155 // `begin`/`rescue`/`else` is the no-exception branch, mirroring
10156 // Python `try`/`except`/`else` (+1), not a switch default. The
10157 // `rescue`(+1) and `else`(+1) both count: total 2.
10158 check_metrics::<RubyParser>(
10159 "begin\n foo\nrescue\n bar\nelse\n baz\nend\n",
10160 "foo.rb",
10161 |metric| {
10162 assert_eq!(metric.cognitive.cognitive_sum(), 2);
10163 },
10164 );
10165 }
10166
10167 #[cfg(feature = "ruby")]
10168 #[test]
10169 fn ruby_stabby_and_keyword_lambda_nesting_parity() {
10170 // A stabby lambda parses as a `Lambda` node CONTAINING its own
10171 // body `Block`; the keyword form parses as a `Call` carrying the
10172 // block argument. Only the `Lambda` wrapper pays the lambda
10173 // nesting surcharge — its body block must not pay again, or the
10174 // same logical construct scores differently across the two
10175 // spellings (lesson #11; the #1257 shared discriminator).
10176 //
10177 // expected: `if`(+1) + one level of lambda nesting(+1) = 2.
10178 check_metrics::<RubyParser>("f = ->(a) { if a then 1 end }\n", "foo.rb", |metric| {
10179 assert_eq!(metric.cognitive.cognitive_sum(), 2);
10180 });
10181 // expected: identical derivation for the keyword form — the
10182 // `Block` under the `lambda` call is the sole closure node:
10183 // `if`(+1) + lambda nesting(+1) = 2.
10184 check_metrics::<RubyParser>("f = lambda { |a| if a then 1 end }\n", "foo.rb", |metric| {
10185 assert_eq!(metric.cognitive.cognitive_sum(), 2);
10186 });
10187 }
10188
10189 #[cfg(feature = "ruby")]
10190 #[test]
10191 fn ruby_if_inside_stabby_lambda_inside_method() {
10192 // expected: the method boundary resets nesting for its contents
10193 // (top-level method, so function_depth stays 0); the stabby
10194 // lambda adds exactly ONE lambda-nesting level (`Lambda` wrapper
10195 // only — its body `Block` is gated out); the `if` then charges
10196 // 1 + nesting(0 conditional + 0 function_depth + 1 lambda) = 2.
10197 // Before the gate the body block paid a second surcharge and
10198 // this scored 3.
10199 check_metrics::<RubyParser>(
10200 "def foo\n f = ->(a) { if a then 1 end }\nend\n",
10201 "foo.rb",
10202 |metric| {
10203 assert_eq!(metric.cognitive.cognitive_sum(), 2);
10204 assert_eq!(metric.cognitive.cognitive_max(), 2);
10205 },
10206 );
10207 }
10208
10209 #[cfg(feature = "ruby")]
10210 #[test]
10211 fn ruby_do_block_spelling_matches_brace_spelling() {
10212 // The lambda-nesting arm is gated on `Block | DoBlock`, and the
10213 // two alternatives are separate dispatch paths: every other Ruby
10214 // cognitive fixture uses the brace spelling, so the `do … end`
10215 // half of both the arm and its stabby-lambda-body gate went
10216 // unexercised. A block's spelling is pure syntax — the same
10217 // closure must score identically either way (lesson 11).
10218 //
10219 // expected: the `Lambda` wrapper pays the one lambda-nesting
10220 // level and its `do_block` body is gated out, so `if` charges
10221 // 1 + nesting(1) = 2 — the same figure the brace form reports in
10222 // `ruby_stabby_and_keyword_lambda_nesting_parity`.
10223 check_metrics::<RubyParser>(
10224 "f = ->(a) do\n if a then 1 end\nend\n",
10225 "foo.rb",
10226 |metric| {
10227 assert_eq!(metric.cognitive.cognitive_sum(), 2);
10228 assert_eq!(metric.cognitive.cognitive_max(), 2);
10229 },
10230 );
10231 // expected: an iterator `do … end` has a `Call` parent, not a
10232 // `Lambda`, so the gate lets it through and the `do_block` is
10233 // itself the closure: `if`(+1) + lambda nesting(+1) = 2. This is
10234 // the arm-taken direction of the same gate.
10235 check_metrics::<RubyParser>(
10236 "[1].each do |x|\n if x then 1 end\nend\n",
10237 "foo.rb",
10238 |metric| {
10239 assert_eq!(metric.cognitive.cognitive_sum(), 2);
10240 assert_eq!(metric.cognitive.cognitive_max(), 2);
10241 },
10242 );
10243 }
10244
10245 #[cfg(feature = "javascript")]
10246 #[test]
10247 fn javascript_labeled_break_continue() {
10248 // Per SonarSource Cognitive Complexity §B2 (issue #435), a labeled
10249 // `break LABEL` / `continue LABEL` is an unstructured jump and adds
10250 // +1. The JS-family grammar exposes the label as a
10251 // `statement_identifier` child of the break/continue node.
10252 check_metrics::<JavascriptParser>(
10253 "function scan(m) {
10254 outer:
10255 for (let i = 0; i < m.length; i++) { // +1
10256 for (let j = 0; j < m[i].length; j++) { // +2
10257 if (m[i][j] < 0) continue outer; // +3, +1
10258 if (m[i][j] > 100) break outer; // +3, +1
10259 }
10260 }
10261 }",
10262 "foo.js",
10263 |metric| {
10264 // outer for(+1) + inner for(+2) + if(+3) + continue outer(+1)
10265 // + if(+3) + break outer(+1) = 11.
10266 assert_eq!(metric.cognitive.cognitive_sum(), 11);
10267 assert_eq!(metric.cognitive.cognitive_max(), 11);
10268 insta::assert_json_snapshot!(
10269 metric.cognitive,
10270 @r#"
10271 {
10272 "sum": 11,
10273 "value": 0,
10274 "average": 11.0,
10275 "min": 0,
10276 "max": 11
10277 }
10278 "#
10279 );
10280 },
10281 );
10282 }
10283
10284 #[cfg(feature = "javascript")]
10285 #[test]
10286 fn javascript_unlabeled_break_continue_not_counted() {
10287 // Negative test for issue #435: plain `break;` / `continue;` are
10288 // not unstructured jumps under SonarSource §B2 and add +0. Only the
10289 // surrounding `for` + two `if`s contribute.
10290 check_metrics::<JavascriptParser>(
10291 "function scan(m) {
10292 for (let i = 0; i < m.length; i++) { // +1
10293 if (m[i] < 0) continue; // +2, +0
10294 if (m[i] > 100) break; // +2, +0
10295 }
10296 }",
10297 "foo.js",
10298 |metric| {
10299 // for(+1) + if(+2) + if(+2) = 5.
10300 assert_eq!(metric.cognitive.cognitive_sum(), 5);
10301 assert_eq!(metric.cognitive.cognitive_max(), 5);
10302 insta::assert_json_snapshot!(
10303 metric.cognitive,
10304 @r#"
10305 {
10306 "sum": 5,
10307 "value": 0,
10308 "average": 5.0,
10309 "min": 0,
10310 "max": 5
10311 }
10312 "#
10313 );
10314 },
10315 );
10316 }
10317
10318 #[cfg(feature = "typescript")]
10319 #[test]
10320 fn typescript_labeled_break_continue() {
10321 // TS parity with JS for labeled jumps (issue #435): labeled
10322 // break/continue each add +1 via the `statement_identifier` child.
10323 check_metrics::<TypescriptParser>(
10324 "function scan(m: number[][]) {
10325 outer:
10326 for (let i = 0; i < m.length; i++) { // +1
10327 for (let j = 0; j < m[i].length; j++) { // +2
10328 if (m[i][j] < 0) continue outer; // +3, +1
10329 if (m[i][j] > 100) break outer; // +3, +1
10330 }
10331 }
10332 }",
10333 "foo.ts",
10334 |metric| {
10335 assert_eq!(metric.cognitive.cognitive_sum(), 11);
10336 assert_eq!(metric.cognitive.cognitive_max(), 11);
10337 insta::assert_json_snapshot!(
10338 metric.cognitive,
10339 @r#"
10340 {
10341 "sum": 11,
10342 "value": 0,
10343 "average": 11.0,
10344 "min": 0,
10345 "max": 11
10346 }
10347 "#
10348 );
10349 },
10350 );
10351 }
10352
10353 /// Asserts the JS-family function-boundary rule over every shape
10354 /// #1159 moves, for one instantiating language.
10355 ///
10356 /// `js_cognitive!` listed `FunctionDeclaration` alone, so a
10357 /// `method_definition` or a bound `function_expression` opened its own
10358 /// `SpaceKind::Function` space — `get_space_kind` maps both — while
10359 /// inheriting the enclosing conditional nesting and skipping the
10360 /// function-depth surcharge. Its `stops` list was short by the same
10361 /// two kinds, which is a separately observable bug: the reset shows on
10362 /// a definition nested in *conditionals*, the `stops` entry on one
10363 /// nested in another *function*. Both are covered below, plus the two
10364 /// shapes the fix must leave alone.
10365 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
10366 fn check_js_function_boundary<T: MetricSuite>(filename: &str) {
10367 fn score(space: &FuncSpace, name: &str) -> u64 {
10368 function_space(space, name).metrics.cognitive.cognitive()
10369 }
10370
10371 // expected: `outer`'s two `if`s are +1 and +2, so `outer` scores
10372 // 3. The definition nested inside them restarts structural
10373 // nesting at 0, so its own `if` costs +1 base plus +1 function
10374 // depth (it is lexically inside `outer`) = 2 — the score the same
10375 // body written as a `function_declaration` already had, which is
10376 // why that form is asserted here as the control.
10377 //
10378 // Two conditional levels are load-bearing. At one level the
10379 // missing reset (+1) and the missing surcharge (-1) cancel and
10380 // both implementations report 2, so a one-level fixture cannot
10381 // discriminate.
10382 for (label, definition) in [
10383 (
10384 "function_declaration",
10385 "function inner(c) { if (c) { return 1; } }",
10386 ),
10387 (
10388 "method_definition",
10389 "class I { inner(c) { if (c) { return 1; } } }",
10390 ),
10391 (
10392 "function_expression",
10393 "const inner = function (c) { if (c) { return 1; } };",
10394 ),
10395 // Generators reach this arm since #1186. Before it,
10396 // `is_js_func!` called them closures, so neither form
10397 // reached the boundary and each scored 3.
10398 (
10399 "generator_function_declaration",
10400 "function* inner(c) { if (c) { yield 1; } }",
10401 ),
10402 (
10403 "generator_function",
10404 "const inner = function* (c) { if (c) { yield 1; } };",
10405 ),
10406 ] {
10407 let source =
10408 format!("function outer(a, b) {{ if (a) {{ if (b) {{ {definition} }} }} }}");
10409 check_func_space::<T, _>(&source, filename, |space| {
10410 assert_eq!(
10411 score(&space, "outer"),
10412 3,
10413 "{label}: enclosing function's own score",
10414 );
10415 assert_eq!(
10416 score(&space, "inner"),
10417 2,
10418 "{label}: nested definition restarts structural nesting",
10419 );
10420 });
10421 }
10422
10423 // The `stops` half, on a definition nested in another function
10424 // rather than in conditionals.
10425 // expected: `inner`'s `if` is +1 base plus +1 function depth = 2.
10426 // With the enclosing kind absent from `stops` the surcharge is 0
10427 // and `inner` scores 1.
10428 for (label, source) in [
10429 (
10430 "method_definition",
10431 "class I { m() { function inner(c) { if (c) { return 1; } } } }",
10432 ),
10433 (
10434 "function_expression",
10435 "const m = function () { function inner(c) { if (c) { return 1; } } };",
10436 ),
10437 // The independent half of #1186: a plain `function` nested
10438 // inside a *generator* got no depth surcharge, because the
10439 // generator was excluded from `stops` by the same
10440 // `is_js_func!` gate. This scored 1 before the fix while the
10441 // non-generator control above scored 2.
10442 (
10443 "generator_function_declaration",
10444 "function* m() { function inner(c) { if (c) { return 1; } } }",
10445 ),
10446 (
10447 "generator_function",
10448 "const m = function* () { function inner(c) { if (c) { return 1; } } };",
10449 ),
10450 ] {
10451 check_func_space::<T, _>(source, filename, |space| {
10452 assert_eq!(
10453 score(&space, "inner"),
10454 2,
10455 "{label}: +1 base, +1 depth from the enclosing definition",
10456 );
10457 });
10458 }
10459
10460 // An *anonymous* `function_expression` used positionally fails
10461 // `check_if_func!` and is a *closure*, so it must keep falling
10462 // through to `_` and inheriting the enclosing nesting. This is what
10463 // pins that the gate was re-derived rather than the kind list
10464 // copied flat: an ungated arm resets here and reports 2, because
10465 // `outer` is a `FunctionDeclaration` and so a `stops` entry.
10466 // Anonymity is load-bearing — `check_if_func!`'s `$extra` disjunct
10467 // makes `run(function named (c) {…})` a function, and `nom` agrees.
10468 // expected: nesting.conditional 2 from `outer`'s two `if`s, so the
10469 // callback's own `if` costs +3. The `nom` assertion states the
10470 // premise — that this shape really is on the closure side of
10471 // `is_func` / `is_closure` — rather than leaving it implied.
10472 check_func_space::<T, _>(
10473 "function outer(a, b) {
10474 if (a) { if (b) { run(function (c) { if (c) { return 1; } }); } }
10475 }",
10476 filename,
10477 |space| {
10478 assert_eq!(
10479 space.metrics.nom.closures_sum(),
10480 1,
10481 "a positional function expression is a closure",
10482 );
10483 assert_eq!(
10484 score(&space, "<anonymous>"),
10485 3,
10486 "a closure inherits the enclosing conditional nesting",
10487 );
10488 },
10489 );
10490
10491 // `ArrowFunction` was deliberately left out of the boundary set —
10492 // it owns the lambda channel in `js_cognitive!`'s `ArrowFunction`
10493 // arm — so sweeping it in is the other way to get this fix wrong.
10494 // expected: nesting.conditional 2 from `outer`'s two `if`s plus
10495 // nesting.lambda 1 from the arrow, so its `if` costs +4. A
10496 // boundary arm that swept `ArrowFunction` in would report 2.
10497 check_func_space::<T, _>(
10498 "function outer(a, b) {
10499 if (a) { if (b) { const inner = (c) => { if (c) { return 1; } }; } }
10500 }",
10501 filename,
10502 |space| {
10503 assert_eq!(
10504 score(&space, "inner"),
10505 4,
10506 "an arrow function keeps the lambda channel",
10507 );
10508 },
10509 );
10510 }
10511
10512 // One `#[test]` per language instantiating `js_cognitive!`: the macro
10513 // body is shared but each grammar's `kind_id`s are its own, so a
10514 // per-language enum drift is invisible from a single language's run.
10515 #[cfg(feature = "javascript")]
10516 #[test]
10517 fn javascript_function_boundary_covers_methods_and_function_expressions_1159() {
10518 check_js_function_boundary::<JavascriptParser>("foo.js");
10519 }
10520
10521 #[cfg(feature = "mozjs")]
10522 #[test]
10523 fn mozjs_function_boundary_covers_methods_and_function_expressions_1159() {
10524 check_js_function_boundary::<MozjsParser>("foo.js");
10525 }
10526
10527 #[cfg(feature = "typescript")]
10528 #[test]
10529 fn typescript_function_boundary_covers_methods_and_function_expressions_1159() {
10530 check_js_function_boundary::<TypescriptParser>("foo.ts");
10531 }
10532
10533 #[cfg(feature = "typescript")]
10534 #[test]
10535 fn tsx_function_boundary_covers_methods_and_function_expressions_1159() {
10536 check_js_function_boundary::<TsxParser>("foo.tsx");
10537 }
10538
10539 #[cfg(feature = "javascript")]
10540 #[test]
10541 fn javascript_compound_short_circuit_assignment_236() {
10542 // Regression for issue #236: `&&=`, `||=`, `??=` are compound
10543 // short-circuit assignments (e.g. `x ??= y` ≡ `x = x ?? y`)
10544 // and each carries one boolean-sequence decision. Each lives
10545 // inside its own `expression_statement`, so the boolean
10546 // sequence resets between them and all three count.
10547 check_metrics::<JavascriptParser>(
10548 "function f(x) {
10549 x ??= 1; // +1 (??=)
10550 x &&= 2; // +1 (&&=)
10551 x ||= 3; // +1 (||=)
10552 }",
10553 "foo.js",
10554 |metric| {
10555 assert_eq!(metric.cognitive.cognitive_sum(), 3);
10556 assert_eq!(metric.cognitive.cognitive_max(), 3);
10557 insta::assert_json_snapshot!(
10558 metric.cognitive,
10559 @r#"
10560 {
10561 "sum": 3,
10562 "value": 0,
10563 "average": 3.0,
10564 "min": 0,
10565 "max": 3
10566 }
10567 "#
10568 );
10569 },
10570 );
10571 }
10572
10573 #[cfg(feature = "typescript")]
10574 #[test]
10575 fn typescript_compound_short_circuit_assignment_236() {
10576 // Regression for issue #236: TS parity with JS for `&&=`,
10577 // `||=`, `??=`.
10578 check_metrics::<TypescriptParser>(
10579 "function f(x: number | null) {
10580 x ??= 1; // +1 (??=)
10581 x &&= 2; // +1 (&&=)
10582 x ||= 3; // +1 (||=)
10583 }",
10584 "foo.ts",
10585 |metric| {
10586 assert_eq!(metric.cognitive.cognitive_sum(), 3);
10587 assert_eq!(metric.cognitive.cognitive_max(), 3);
10588 insta::assert_json_snapshot!(
10589 metric.cognitive,
10590 @r#"
10591 {
10592 "sum": 3,
10593 "value": 0,
10594 "average": 3.0,
10595 "min": 0,
10596 "max": 3
10597 }
10598 "#
10599 );
10600 },
10601 );
10602 }
10603
10604 #[cfg(feature = "typescript")]
10605 #[test]
10606 fn tsx_compound_short_circuit_assignment_236() {
10607 // Regression for issue #236: TSX parity with JS/TS for `&&=`,
10608 // `||=`, `??=`.
10609 check_metrics::<TsxParser>(
10610 "function f(x: number | null) {
10611 x ??= 1; // +1 (??=)
10612 x &&= 2; // +1 (&&=)
10613 x ||= 3; // +1 (||=)
10614 }",
10615 "foo.tsx",
10616 |metric| {
10617 assert_eq!(metric.cognitive.cognitive_sum(), 3);
10618 assert_eq!(metric.cognitive.cognitive_max(), 3);
10619 insta::assert_json_snapshot!(
10620 metric.cognitive,
10621 @r#"
10622 {
10623 "sum": 3,
10624 "value": 0,
10625 "average": 3.0,
10626 "min": 0,
10627 "max": 3
10628 }
10629 "#
10630 );
10631 },
10632 );
10633 }
10634
10635 #[cfg(feature = "mozjs")]
10636 #[test]
10637 fn mozjs_compound_short_circuit_assignment_236() {
10638 // Regression for issue #236: Mozjs (SpiderMonkey-flavoured JS)
10639 // shares the JS macro and must score `&&=` / `||=` / `??=`
10640 // identically.
10641 check_metrics::<MozjsParser>(
10642 "function f(x) {
10643 x ??= 1; // +1 (??=)
10644 x &&= 2; // +1 (&&=)
10645 x ||= 3; // +1 (||=)
10646 }",
10647 "foo.js",
10648 |metric| {
10649 assert_eq!(metric.cognitive.cognitive_sum(), 3);
10650 assert_eq!(metric.cognitive.cognitive_max(), 3);
10651 insta::assert_json_snapshot!(
10652 metric.cognitive,
10653 @r#"
10654 {
10655 "sum": 3,
10656 "value": 0,
10657 "average": 3.0,
10658 "min": 0,
10659 "max": 3
10660 }
10661 "#
10662 );
10663 },
10664 );
10665 }
10666
10667 #[cfg(feature = "csharp")]
10668 #[test]
10669 fn csharp_compound_short_circuit_assignment_236() {
10670 // Regression for issue #236: C#'s grammar only provides `??=`
10671 // among the short-circuit assignments (no `&&=` / `||=`). The
10672 // operator lives inside `assignment_expression` rather than a
10673 // `BinaryExpression`, so without the #236 fix it was silently
10674 // skipped.
10675 check_metrics::<CsharpParser>(
10676 "class C {
10677 int? F(int? x) {
10678 x ??= 1; // +1 (??=)
10679 return x ?? 0;
10680 }
10681 }",
10682 "foo.cs",
10683 |metric| {
10684 // Outer `??` chain (+1) + `??=` (+1) = 2 at function max.
10685 assert_eq!(metric.cognitive.cognitive_sum(), 2);
10686 assert_eq!(metric.cognitive.cognitive_max(), 2);
10687 insta::assert_json_snapshot!(
10688 metric.cognitive,
10689 @r#"
10690 {
10691 "sum": 2,
10692 "value": 0,
10693 "average": 2.0,
10694 "min": 0,
10695 "max": 2
10696 }
10697 "#
10698 );
10699 },
10700 );
10701 }
10702
10703 #[cfg(feature = "php")]
10704 #[test]
10705 fn php_compound_short_circuit_assignment_236() {
10706 // Regression for issue #236: PHP's only compound short-circuit
10707 // assignment is `??=` (no `&&=` / `||=`). It lives inside
10708 // `augmented_assignment_expression` rather than a
10709 // `BinaryExpression`, so without the #236 fix it was silently
10710 // skipped.
10711 check_metrics::<PhpParser>(
10712 "<?php
10713 function f($x) {
10714 $x ??= 1; // +1 (??=)
10715 return $x ?? 0; // +1 (??)
10716 }",
10717 "foo.php",
10718 |metric| {
10719 assert_eq!(metric.cognitive.cognitive_sum(), 2);
10720 assert_eq!(metric.cognitive.cognitive_max(), 2);
10721 insta::assert_json_snapshot!(
10722 metric.cognitive,
10723 @r#"
10724 {
10725 "sum": 2,
10726 "value": 0,
10727 "average": 2.0,
10728 "min": 0,
10729 "max": 2
10730 }
10731 "#
10732 );
10733 },
10734 );
10735 }
10736
10737 /// A handler with no control flow has zero cognitive complexity.
10738 #[cfg(feature = "irules")]
10739 #[test]
10740 fn irules_no_cognitive() {
10741 check_metrics::<IrulesParser>("when X { set a 1 }\n", "foo.irule", |metric| {
10742 assert_eq!(metric.cognitive.cognitive_sum(), 0);
10743 });
10744 }
10745
10746 /// A single `if` adds one.
10747 #[cfg(feature = "irules")]
10748 #[test]
10749 fn irules_simple_function() {
10750 check_metrics::<IrulesParser>(
10751 "when X { if { $a } { log local0. \"hi\" } }\n",
10752 "foo.irule",
10753 |metric| {
10754 assert_eq!(metric.cognitive.cognitive_sum(), 1);
10755 },
10756 );
10757 }
10758
10759 /// A run of the *same* boolean operator (`$a && $b && $c`) is one
10760 /// sequence: `if` (1) + boolean sequence (1) = 2.
10761 #[cfg(feature = "irules")]
10762 #[test]
10763 fn irules_sequence_same_booleans() {
10764 check_metrics::<IrulesParser>(
10765 "when X { if { $a && $b && $c } { log local0. \"hi\" } }\n",
10766 "foo.irule",
10767 |metric| {
10768 assert_eq!(metric.cognitive.cognitive_sum(), 2);
10769 },
10770 );
10771 }
10772
10773 /// Switching operator (`$a && $b || $c`) starts a new sequence: `if` (1)
10774 /// + `&&` sequence (1) + `||` sequence (1) = 3.
10775 #[cfg(feature = "irules")]
10776 #[test]
10777 fn irules_sequence_different_booleans() {
10778 check_metrics::<IrulesParser>(
10779 "when X { if { $a && $b || $c } { log local0. \"hi\" } }\n",
10780 "foo.irule",
10781 |metric| {
10782 assert_eq!(metric.cognitive.cognitive_sum(), 3);
10783 },
10784 );
10785 }
10786
10787 /// Unary negation (`!`) does not itself add cognitive cost; only the
10788 /// boolean sequence does: `if` (1) + `&&` sequence (1) = 2.
10789 #[cfg(feature = "irules")]
10790 #[test]
10791 fn irules_not_booleans() {
10792 check_metrics::<IrulesParser>(
10793 "when X { if { !$a && !$b } { log local0. \"hi\" } }\n",
10794 "foo.irule",
10795 |metric| {
10796 assert_eq!(metric.cognitive.cognitive_sum(), 2);
10797 },
10798 );
10799 }
10800
10801 /// One level of nesting: `while` (1) + `if` (1 + nesting 1 = 2) = 3.
10802 #[cfg(feature = "irules")]
10803 #[test]
10804 fn irules_1_level_nesting() {
10805 check_metrics::<IrulesParser>(
10806 "when X { while { $a } { if { $b } { log local0. \"hi\" } } }\n",
10807 "foo.irule",
10808 |metric| {
10809 assert_eq!(metric.cognitive.cognitive_sum(), 3);
10810 },
10811 );
10812 }
10813
10814 /// Two levels: `while` (1) + `if` (2) + `foreach` (1 + nesting 2 = 3) = 6.
10815 #[cfg(feature = "irules")]
10816 #[test]
10817 fn irules_2_level_nesting() {
10818 check_metrics::<IrulesParser>(
10819 "when X { while { $a } { if { $b } { foreach z $l { log local0. \"hi\" } } } }\n",
10820 "foo.irule",
10821 |metric| {
10822 assert_eq!(metric.cognitive.cognitive_sum(), 6);
10823 },
10824 );
10825 }
10826
10827 /// The lesson-10 guard for `is_else_if`: an `if … elseif … elseif …
10828 /// else` chain (each clause +1 at the same level = 4) must score
10829 /// *lower* than the same number of `if`s nested inside one another
10830 /// (1 + 2 + 3 = 6, paying the nesting penalty). A broken `is_else_if`
10831 /// predicate that treated `elseif` like a fresh nested `if` would push
10832 /// the chain's score up toward the nested value, so the strict `<`
10833 /// assertion catches the regression that #115 found in Java/C#.
10834 #[cfg(feature = "irules")]
10835 #[test]
10836 fn irules_else_if_chain() {
10837 use std::cell::Cell;
10838
10839 let chain = "when X { if { $a } { set r 1 } elseif { $b } { set r 2 } elseif { $c } { set r 3 } else { set r 4 } }\n";
10840 let nested = "when X { if { $a } { if { $b } { if { $c } { set r 1 } } } }\n";
10841
10842 // Capture each measured sum through a `Cell` (check_func_space takes an
10843 // `Fn` closure) so the final `<` assertion compares the *actual*
10844 // values rather than restating constants.
10845 let chain_cog = Cell::new(-1.0);
10846 check_func_space::<IrulesParser, _>(chain, "chain.irule", |fs| {
10847 chain_cog.set(fs.metrics.cognitive.cognitive_sum() as f64);
10848 });
10849 let nested_cog = Cell::new(-1.0);
10850 check_func_space::<IrulesParser, _>(nested, "nested.irule", |fs| {
10851 nested_cog.set(fs.metrics.cognitive.cognitive_sum() as f64);
10852 });
10853
10854 assert_eq!(chain_cog.get(), 4.0);
10855 assert_eq!(nested_cog.get(), 6.0);
10856 assert!(
10857 chain_cog.get() < nested_cog.get(),
10858 "else-if chain ({}) must score lower than equivalently nested ifs ({})",
10859 chain_cog.get(),
10860 nested_cog.get(),
10861 );
10862 }
10863
10864 /// A `switch` nested in an `if`: `if` (1) + `switch` (1 + nesting 1 = 2)
10865 /// = 3. Confirms `switch` participates in nesting like other branches.
10866 #[cfg(feature = "irules")]
10867 #[test]
10868 fn irules_switch_nesting() {
10869 check_metrics::<IrulesParser>(
10870 "when X { if { $a } { switch $h { a { log local0. \"a\" } b { log local0. \"b\" } } } }\n",
10871 "foo.irule",
10872 |metric| {
10873 assert_eq!(metric.cognitive.cognitive_sum(), 3);
10874 },
10875 );
10876 }
10877
10878 /// `catch` is a conditional error handler — its body runs only when
10879 /// the guarded command errors — so it pays nesting like any other
10880 /// branch. Flat: 1. Nested in an `if`: `if` (1) + `catch`
10881 /// (1 + nesting 1 = 2) = 3, matching `irules_switch_nesting`.
10882 ///
10883 /// The `Catch` arm had no test before this: the whole arm measured
10884 /// zero-coverage while every other iRules branch kind was exercised.
10885 #[cfg(feature = "irules")]
10886 #[test]
10887 fn irules_catch_nesting() {
10888 check_metrics::<IrulesParser>("when X { catch { foo } }\n", "foo.irule", |metric| {
10889 assert_eq!(metric.cognitive.cognitive_sum(), 1);
10890 });
10891 check_metrics::<IrulesParser>(
10892 "when X { if { $a } { catch { foo } } }\n",
10893 "foo.irule",
10894 |metric| {
10895 assert_eq!(metric.cognitive.cognitive_sum(), 3);
10896 },
10897 );
10898 }
10899
10900 /// Objective-C straight-line method body has zero cognitive
10901 /// complexity.
10902 #[cfg(feature = "objc")]
10903 #[test]
10904 fn objc_no_cognitive() {
10905 check_metrics::<ObjcParser>(
10906 "@implementation Foo
10907- (int)bar {
10908 int a = 1;
10909 return a;
10910}
10911@end
10912",
10913 "foo.m",
10914 |metric| {
10915 assert_eq!(metric.cognitive.cognitive_sum(), 0);
10916 insta::assert_json_snapshot!(metric.cognitive, @r#"
10917 {
10918 "sum": 0,
10919 "value": 0,
10920 "average": 0.0,
10921 "min": 0,
10922 "max": 0
10923 }
10924 "#);
10925 },
10926 );
10927 }
10928
10929 /// Objective-C single `if` at method top level: +1, no nesting
10930 /// surcharge.
10931 #[cfg(feature = "objc")]
10932 #[test]
10933 fn objc_simple_if() {
10934 check_metrics::<ObjcParser>(
10935 "@implementation Foo
10936- (void)bar:(int)x {
10937 if (x > 0) {
10938 [self use:x];
10939 }
10940}
10941@end
10942",
10943 "foo.m",
10944 |metric| {
10945 assert_eq!(metric.cognitive.cognitive_sum(), 1);
10946 insta::assert_json_snapshot!(metric.cognitive, @r#"
10947 {
10948 "sum": 1,
10949 "value": 0,
10950 "average": 1.0,
10951 "min": 0,
10952 "max": 1
10953 }
10954 "#);
10955 },
10956 );
10957 }
10958
10959 /// Objective-C chained booleans `a && b && c`: SonarSource counts
10960 /// one for the first `&&` and zero for each additional same-operator
10961 /// link in the sequence, so the whole `if (a && b && c)` is +1 (if)
10962 /// + 1 (one boolean sequence) = 2.
10963 #[cfg(feature = "objc")]
10964 #[test]
10965 fn objc_sequence_same_booleans() {
10966 check_metrics::<ObjcParser>(
10967 "@implementation Foo
10968- (void)bar:(int)a b:(int)b c:(int)c {
10969 if (a && b && c) {
10970 [self use:a];
10971 }
10972}
10973@end
10974",
10975 "foo.m",
10976 |metric| {
10977 assert_eq!(metric.cognitive.cognitive_sum(), 2);
10978 insta::assert_json_snapshot!(metric.cognitive, @r#"
10979 {
10980 "sum": 2,
10981 "value": 0,
10982 "average": 2.0,
10983 "min": 0,
10984 "max": 2
10985 }
10986 "#);
10987 },
10988 );
10989 }
10990
10991 /// Objective-C nesting surcharge: an `if` nested inside a `for`
10992 /// scores `for` (+1) + `if` (+1 base +1 nesting) = 3.
10993 #[cfg(feature = "objc")]
10994 #[test]
10995 fn objc_nested() {
10996 check_metrics::<ObjcParser>(
10997 "@implementation Foo
10998- (void)bar:(NSArray *)arr {
10999 for (id x in arr) {
11000 if ([x boolValue]) {
11001 [self use:x];
11002 }
11003 }
11004}
11005@end
11006",
11007 "foo.m",
11008 |metric| {
11009 assert_eq!(metric.cognitive.cognitive_sum(), 3);
11010 insta::assert_json_snapshot!(metric.cognitive, @r#"
11011 {
11012 "sum": 3,
11013 "value": 0,
11014 "average": 3.0,
11015 "min": 0,
11016 "max": 3
11017 }
11018 "#);
11019 },
11020 );
11021 }
11022
11023 #[cfg(feature = "objc")]
11024 #[test]
11025 fn objc_block_nesting() {
11026 // A decision inside an ObjC block `^{ … }` picks up the lambda
11027 // surcharge: the `if` scores base (1) + lambda nesting (1) = 2,
11028 // exercising the `BlockLiteral => lambda += 1` path (the ObjC
11029 // closure analogue of the C++ lambda).
11030 check_metrics::<ObjcParser>(
11031 "@implementation Foo
11032- (void)bar {
11033 void (^blk)(int) = ^(int x) {
11034 if (x > 0) {
11035 [self use];
11036 }
11037 };
11038}
11039@end
11040",
11041 "foo.m",
11042 |metric| {
11043 assert_eq!(metric.cognitive.cognitive_sum(), 2);
11044 insta::assert_json_snapshot!(metric.cognitive, @r#"
11045 {
11046 "sum": 2,
11047 "value": 0,
11048 "average": 1.0,
11049 "min": 0,
11050 "max": 2
11051 }
11052 "#);
11053 },
11054 );
11055 }
11056
11057 /// Objective-C `if / else if / else if / else` chain must score
11058 /// LOWER than the same number of singly-nested `if`s, because
11059 /// else-if links add no nesting surcharge while deepening `if`s do.
11060 /// This guards the `is_else_if` predicate (a regression that failed
11061 /// to recognise the else-if extension would inflate the chain to the
11062 /// nested score).
11063 #[cfg(feature = "objc")]
11064 #[test]
11065 fn objc_else_if_chain() {
11066 use std::cell::Cell;
11067
11068 let chain_sum = Cell::new(u64::MAX);
11069 check_func_space::<ObjcParser, _>(
11070 "@implementation Foo
11071- (int)bar:(int)x {
11072 if (x == 1) {
11073 return 1;
11074 } else if (x == 2) {
11075 return 2;
11076 } else if (x == 3) {
11077 return 3;
11078 } else {
11079 return 0;
11080 }
11081}
11082@end
11083",
11084 "foo.m",
11085 |fs| chain_sum.set(fs.metrics.cognitive.cognitive_sum()),
11086 );
11087
11088 let nested_sum = Cell::new(u64::MAX);
11089 check_func_space::<ObjcParser, _>(
11090 "@implementation Foo
11091- (int)bar:(int)x {
11092 if (x == 1) {
11093 if (x == 2) {
11094 if (x == 3) {
11095 return 3;
11096 }
11097 }
11098 }
11099 return 0;
11100}
11101@end
11102",
11103 "foo.m",
11104 |fs| nested_sum.set(fs.metrics.cognitive.cognitive_sum()),
11105 );
11106
11107 // expected chain (matches the C-family else-if structure): each
11108 // `else if`/`else` adds +1 with NO nesting surcharge because
11109 // `is_else_if` recognises the else-clause-nested `if_statement` as
11110 // a branch extension — if(+1) + else-if(+1) + else-if(+1) +
11111 // else(+1) = 4. Were the predicate broken, the nested
11112 // `if_statement`s would accrue nesting (+2, +3) and the chain
11113 // would climb to 7. expected nested: if(+1) + if(+1+1) +
11114 // if(+1+2) = 6. The chain must remain strictly cheaper.
11115 assert_eq!(chain_sum.get(), 4, "else-if chain cognitive sum");
11116 assert_eq!(nested_sum.get(), 6, "triple-nested if cognitive sum");
11117 assert!(
11118 chain_sum.get() < nested_sum.get(),
11119 "else-if chain ({}) must score lower than triple-nested ifs ({})",
11120 chain_sum.get(),
11121 nested_sum.get(),
11122 );
11123 }
11124
11125 /// Pins that `function_depth` and `lambda` are distinguishable.
11126 ///
11127 /// They are summed symmetrically almost everywhere, so most inputs
11128 /// cannot tell them apart. The asymmetric operation is
11129 /// `enter_function_boundary`'s `lambda = 0`, which clears one field
11130 /// while `increment_function_depth` raises the other — since #1187
11131 /// that pair runs for every language, not only the JS macro.
11132 ///
11133 /// The doubled arrow is still load-bearing, for the reason it always
11134 /// was: a swap at the write site transposes the pair at every node
11135 /// on the way down, and the plain
11136 /// `arrow -> statement_block -> function_declaration` chain has odd
11137 /// parity and totals the same either way. A mutant writing
11138 /// `function_depth = 0` in place of `lambda = 0` leaves `lambda 2,
11139 /// function_depth 1` here and charges the `if` 4 rather than 2.
11140 #[cfg(feature = "javascript")]
11141 #[test]
11142 fn javascript_function_depth_and_lambda_are_distinguishable() {
11143 // expected: `inner` takes the boundary, so `conditional` and
11144 // `lambda` both reset to 0. `ArrowFunction` joined the `stops`
11145 // list in #1187, so `inner` earns a function-depth surcharge of
11146 // 1 — `increment_function_depth` asks whether *any* ancestor is a
11147 // stop, not how many, so two arrows still give 1. The `if` costs
11148 // 1 base + 1 depth = 2, up from 1 before the arrow entered
11149 // `stops`.
11150 check_metrics::<JavascriptParser>(
11151 "const f = () => () => { function inner() { if (a) { } } };",
11152 "nest.js",
11153 |metric| {
11154 assert_eq!(metric.cognitive.cognitive_sum(), 2);
11155 },
11156 );
11157 }
11158
11159 /// The `ArrowFunction` arm's own `lambda += 1`, pinned separately:
11160 /// with no `function_declaration` between the arrow and the `if`,
11161 /// nothing resets lambda, so the arrow's level reaches the `if`.
11162 #[cfg(feature = "javascript")]
11163 #[test]
11164 fn javascript_arrow_contributes_lambda_nesting() {
11165 // expected: 1 for the `if`, +1 for the enclosing arrow level.
11166 check_metrics::<JavascriptParser>(
11167 "const f = () => { if (a) { } };",
11168 "arrow.js",
11169 |metric| {
11170 assert_eq!(metric.cognitive.cognitive_sum(), 2);
11171 },
11172 );
11173 }
11174
11175 /// Nesting is still inherited correctly thousands of levels deep
11176 /// (#1062).
11177 ///
11178 /// `get_nesting_from_map` used to recover a node's inherited nesting
11179 /// via `node.parent()`, which is `O(depth)` — tree-sitter stores no
11180 /// parent pointer — making the metric `O(nodes × depth)`. The walker
11181 /// now seeds each child's slot from its parent's, so the lookup is
11182 /// `O(1)`.
11183 ///
11184 /// Both versions produce identical numbers, only at different
11185 /// speeds, so what this test pins is correctness at depth. The
11186 /// *cost* is pinned by the `cognitive/nested-while` probe in the
11187 /// benchmark harness (#1068), which asserts the complexity class —
11188 /// `cargo bench -p big-code-analysis-bench --bench scaling`.
11189 ///
11190 /// The wall-clock half used to live here and produced a false
11191 /// failure in four environments: `windows-latest` in CI (10.9 s
11192 /// against an 8 s absolute budget), a local `make pre-commit`
11193 /// running clippy and rustdoc alongside the suite (5.6x), the same
11194 /// host under heavy parallel load (3.9x), and `cargo llvm-cov`,
11195 /// whose instrumentation skewed even a best-of-three ratio to 3.5x.
11196 /// The `coverage` job runs in CI, so leaving it armed redded the
11197 /// build on a measurement artefact rather than on a regression. A
11198 /// ratio between two depths is host-independent but not
11199 /// load-independent, and the fix for that is interleaved
11200 /// measurement at three depths, which belongs in a bench target
11201 /// and not in the unit suite.
11202 ///
11203 /// Uses `while`, deliberately, **not** `if`: when this test was
11204 /// written `Checker::is_else_if` still called `node.parent()` for
11205 /// every `if_statement`, so nested `if`s were quadratic for reasons
11206 /// that fix did not touch. #1084 moved that predicate onto the
11207 /// walker's ancestor chain, and the harness now measures the `if`
11208 /// shape under the same linear bound as this one.
11209 #[cfg(feature = "c")]
11210 #[test]
11211 fn cognitive_nesting_is_inherited_at_depth() {
11212 // Restricted to `Cognitive` — which pulls in `Nom` as a declared
11213 // dependency, so this narrows the work rather than isolating it.
11214 fn cognitive_of(source: &str) -> u64 {
11215 crate::test_support::metrics_verbatim(
11216 crate::LANG::C,
11217 source.as_bytes(),
11218 crate::MetricsOptions::default().with_only(&[crate::Metric::Cognitive]),
11219 )
11220 .cognitive
11221 .cognitive_sum()
11222 }
11223
11224 // Each level adds its own nesting penalty, so cognitive grows as
11225 // 1 + 2 + … + n. Asserting the closed form at depth is what pins
11226 // that nesting is still inherited rather than recomputed.
11227 let nested_whiles = |n: usize| -> String {
11228 format!(
11229 "int main(){{ {} 1; {} }}\n",
11230 "while (a) { ".repeat(n),
11231 "} ".repeat(n)
11232 )
11233 };
11234 let expected = |n: u64| n * (n + 1) / 2;
11235
11236 assert_eq!(cognitive_of(&nested_whiles(3)), expected(3), "1 + 2 + 3");
11237 assert_eq!(cognitive_of(&nested_whiles(2_000)), expected(2_000));
11238 }
11239
11240 /// Function-nesting depth is still counted correctly thousands of
11241 /// levels deep (#1062).
11242 ///
11243 /// `increment_function_depth` asks whether any ancestor of a
11244 /// function node is itself a function. It used to climb with
11245 /// `node.parent()`, which is `O(depth)` per step, so `Cognitive`
11246 /// stayed `O(depth²)` on nested definitions after the nesting-map
11247 /// half of #1062 was fixed. The scan now walks the ancestor chain
11248 /// the walker hands down.
11249 ///
11250 /// Both versions answer identically, so what this pins is the
11251 /// arithmetic at depth; the *cost* is pinned by the
11252 /// `cognitive/nested-fn` probe in the benchmark harness
11253 /// (`cargo bench -p big-code-analysis-bench --bench scaling`),
11254 /// which asserts the complexity class.
11255 #[cfg(feature = "rust")]
11256 #[test]
11257 fn cognitive_function_depth_is_inherited_at_depth() {
11258 fn cognitive_of(source: &str) -> u64 {
11259 crate::test_support::metrics_verbatim(
11260 crate::LANG::Rust,
11261 source.as_bytes(),
11262 crate::MetricsOptions::default().with_only(&[crate::Metric::Cognitive]),
11263 )
11264 .cognitive
11265 .cognitive_sum()
11266 }
11267
11268 // The function at level k has k enclosing functions, so its
11269 // `if` costs k + 1 and the file totals 1 + 2 + … + n. The `if`
11270 // is what makes the depth observable: a chain of bare functions
11271 // scores zero however the depth is computed.
11272 let nested_fns = |n: usize| -> String {
11273 format!(
11274 "{}let x = 1;{}\n",
11275 "fn f() { if a {} ".repeat(n),
11276 "} ".repeat(n)
11277 )
11278 };
11279 let expected = |n: u64| n * (n + 1) / 2;
11280
11281 assert_eq!(cognitive_of(&nested_fns(3)), expected(3), "1 + 2 + 3");
11282 // Half the depth of `cognitive_nesting_is_inherited_at_depth`
11283 // because each level here also opens a `FuncSpace`. The debug
11284 // build is no longer the reason: #1122 took the `Node::parent`
11285 // re-derivation out of `Ancestors::checked`, so an unoptimised
11286 // walk is linear like the release one and this case dropped from
11287 // ~1.0 s to ~0.02 s. `make chain-audit` puts the quadratic
11288 // assertion back deliberately.
11289 assert_eq!(cognitive_of(&nested_fns(1_000)), expected(1_000));
11290 }
11291
11292 /// A function nested inside another makes its `if` cost one more
11293 /// than the same `if` at the top level, in every language that can
11294 /// express the nesting (#1062).
11295 ///
11296 /// That surcharge has exactly one source: `increment_function_depth`,
11297 /// which asks whether any ancestor of a function node is itself a
11298 /// function. #1062 moved the scan off `Node::parent` — `O(depth)` per
11299 /// step, and so quadratic over a deeply nested file — and onto the
11300 /// ancestor chain the walker hands down. The flat source in each row
11301 /// is the control: the same `if` with nothing enclosing its function,
11302 /// which must stay at 1, so the pair measures the surcharge and not
11303 /// the body.
11304 ///
11305 /// These are the languages whose function-depth arm had no test of
11306 /// its own; C++, C#, Groovy, Java, Kotlin, Perl, PHP, Python, Rust
11307 /// and Tcl are covered by dedicated tests elsewhere in this module.
11308 /// Go is deliberately absent: its stop set is `function_declaration`
11309 /// / `method_declaration` and the grammar allows neither inside a
11310 /// function body, so the surcharge is unreachable there — a nested
11311 /// Go function is a `func_literal`, which takes the `lambda` path.
11312 // Gated on the union of the rows below. Unlike its siblings this
11313 // test had *neither* half of the rule: the rows were swept without
11314 // an `is_enabled` filter, so a disabled grammar reached
11315 // `Ast::parse`, which cannot produce a tree for one — the gate alone
11316 // would still leave it failing under `--features c`, a subset the
11317 // gate admits. All three pieces landed together (#1286).
11318 #[test]
11319 #[cfg(any(
11320 feature = "bash",
11321 feature = "c",
11322 feature = "irules",
11323 feature = "javascript",
11324 feature = "lua",
11325 feature = "mozcpp",
11326 feature = "mozjs",
11327 feature = "objc",
11328 feature = "ruby",
11329 feature = "typescript"
11330 ))]
11331 fn function_depth_surcharge_holds_across_languages() {
11332 use crate::test_support::metrics_verbatim;
11333
11334 fn cognitive_of(lang: LANG, source: &str) -> u64 {
11335 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
11336 .cognitive
11337 .cognitive_sum()
11338 }
11339
11340 // Every row must parse cleanly. Several of these snippets lean
11341 // on a grammar's less-travelled corners — GNU nested functions
11342 // in C, a `function` statement inside another in Lua, a `proc`
11343 // inside a `proc` in iRules — and a grammar bump that stopped
11344 // accepting one would leave that row measuring `tree_sitter`'s
11345 // error recovery while still reporting 1 and 2. Verified: with
11346 // trailing garbage appended to the C source, the costs below
11347 // are unmoved and the test stays green without this check.
11348 fn parses_cleanly(lang: LANG, source: &str) -> bool {
11349 crate::Ast::parse(crate::Source::new(lang, source.as_bytes()))
11350 .is_ok_and(|ast| !ast.as_tree_sitter().root_node().has_error())
11351 }
11352
11353 // C: a GNU nested function definition.
11354 const C_FLAT: &str = "void f(int a) { if (a) { } }\n";
11355 const C_NESTED: &str = "void f(int a) { void g(int b) { if (b) { } } }\n";
11356 // Objective-C reuses C's `function_definition` stop but adds
11357 // `method_definition`, which only a real `@implementation`
11358 // reaches — running the C source here would duplicate the row
11359 // above and leave that extra stop untested.
11360 const OBJC_FLAT: &str = "@implementation A\n- (void)m:(int)a { if (a) { } }\n@end\n";
11361 const OBJC_NESTED: &str =
11362 "@implementation A\n- (void)m:(int)a { void g(int b) { if (b) { } } }\n@end\n";
11363 // C++ has no nested function definitions; a method on a local
11364 // struct is the nesting the grammar does admit (see
11365 // `cpp_nested_function_resets_nesting_and_adds_depth`).
11366 const CPP_FLAT: &str = "struct S { void f(bool a) { if (a) { } } };\n";
11367 const CPP_NESTED: &str =
11368 "struct S { void f(bool a) { struct I { void g(bool b) { if (b) { } } }; } };\n";
11369 const JS_FLAT: &str = "function f(a) { if (a) { } }\n";
11370 const JS_NESTED: &str = "function f(a) { function g(b) { if (b) { } } }\n";
11371 const RUBY_FLAT: &str = "def f\nif a\nend\nend\n";
11372 const RUBY_NESTED: &str = "def f\ndef g\nif a\nend\nend\nend\n";
11373 const LUA_FLAT: &str = "function f() if a then end end\n";
11374 const LUA_NESTED: &str = "function f() function g() if a then end end end\n";
11375 const BASH_FLAT: &str = "f() {\nif [ -n \"$a\" ]; then :; fi\n}\n";
11376 const BASH_NESTED: &str = "f() {\ng() {\nif [ -n \"$a\" ]; then :; fi\n}\n}\n";
11377 const TCL_FLAT: &str = "proc outer {x} {\nif {$x > 0} {\nputs positive\n}\n}\n";
11378 const TCL_NESTED: &str =
11379 "proc outer {x} {\nproc inner {y} {\nif {$y > 0} {\nputs positive\n}\n}\n}\n";
11380
11381 // Filtered rather than swept whole: `parses_cleanly` and
11382 // `cognitive_of` both hand the row to the parser, and a grammar
11383 // this build did not compile in has none, so an unfiltered
11384 // sweep fails on every feature subset that omits one row.
11385 let rows: Vec<(LANG, &str, &str)> = [
11386 (LANG::C, C_FLAT, C_NESTED),
11387 (LANG::Objc, OBJC_FLAT, OBJC_NESTED),
11388 (LANG::Mozcpp, CPP_FLAT, CPP_NESTED),
11389 (LANG::Javascript, JS_FLAT, JS_NESTED),
11390 (LANG::Mozjs, JS_FLAT, JS_NESTED),
11391 (LANG::Typescript, JS_FLAT, JS_NESTED),
11392 (LANG::Tsx, JS_FLAT, JS_NESTED),
11393 (LANG::Ruby, RUBY_FLAT, RUBY_NESTED),
11394 (LANG::Lua, LUA_FLAT, LUA_NESTED),
11395 (LANG::Bash, BASH_FLAT, BASH_NESTED),
11396 (LANG::Irules, TCL_FLAT, TCL_NESTED),
11397 ]
11398 .into_iter()
11399 .filter(|&(lang, ..)| lang.is_enabled())
11400 .collect();
11401 // The non-vacuity half: the `cfg` above makes the test absent
11402 // when no row's feature is on, so reaching here with an empty
11403 // sweep means `is_enabled` stopped agreeing with it.
11404 assert!(
11405 !rows.is_empty(),
11406 "no language enabled; this test asserted nothing"
11407 );
11408
11409 // Whole vectors rather than a per-row `assert_eq!`: when this
11410 // shared walk breaks it breaks for every language at once, and
11411 // comparing the columns shows all of them instead of stopping
11412 // at the first. Hand-rolling that diagnostic with a `wrong`
11413 // accumulator would work too, but its `push` arm is a branch no
11414 // passing run ever takes — dead weight that reads as a coverage
11415 // hole and never gets exercised.
11416 let measured: Vec<(LANG, u64, u64)> = rows
11417 .iter()
11418 .map(|&(lang, flat, nested)| {
11419 for (label, source) in [("flat", flat), ("nested", nested)] {
11420 assert!(
11421 parses_cleanly(lang, source),
11422 "{lang:?}: the {label} source must parse without an ERROR node:\n{source}",
11423 );
11424 }
11425 (lang, cognitive_of(lang, flat), cognitive_of(lang, nested))
11426 })
11427 .collect();
11428 let expected: Vec<(LANG, u64, u64)> = rows.iter().map(|&(lang, ..)| (lang, 1, 2)).collect();
11429
11430 assert_eq!(
11431 measured, expected,
11432 "an `if` must cost 1 in a top-level function and 2 one function deeper",
11433 );
11434 }
11435
11436 /// Every [`Nesting`] channel contributes to `total()`.
11437 ///
11438 /// Distinct powers of two, so dropping a channel or summing one
11439 /// twice — the failure modes of the open-coded
11440 /// `conditional + function_depth + lambda` this method replaced at
11441 /// its two sites (#1086) — is distinguishable from the total alone
11442 /// rather than just reading as a bad number.
11443 #[test]
11444 fn nesting_total_sums_every_channel() {
11445 assert_eq!(
11446 Nesting {
11447 conditional: 1,
11448 function_depth: 2,
11449 lambda: 4,
11450 }
11451 .total(),
11452 7,
11453 "1/2/4 encoding: short by 1 means `conditional` was dropped, \
11454 by 2 `function_depth`, by 4 `lambda`; over by the same \
11455 amount means that channel was summed twice",
11456 );
11457 // Weak on its own — every field is zero, so this survives any
11458 // linear combination of them. It only rules out a `total()` that
11459 // returns a nonzero constant.
11460 assert_eq!(Nesting::default().total(), 0);
11461 }
11462
11463 /// `increase_nesting` charges the *summed* level but advances only
11464 /// the `conditional` channel.
11465 ///
11466 /// Before #1086 this helper took `&mut usize` for the conditional
11467 /// channel and `depth` / `lambda` as by-value non-`mut` params, so
11468 /// bumping the wrong one was *inert* rather than unrepresentable:
11469 /// `depth += 1` needed a `mut` added to compile, and then wrote to a
11470 /// copy nobody read. It now holds the whole struct, which makes
11471 /// `function_depth += 1` a one-character slip that persists —
11472 /// invisible in the *charge* itself, since `total()` is symmetric,
11473 /// and detectable only downstream where the channels are read apart.
11474 ///
11475 /// Perturbing the production line to `function_depth += 1` fails this
11476 /// test plus five nested-function tests (`java_nested_method_…`,
11477 /// `cpp_nested_function_…`, `groovy_…`, `php_…`,
11478 /// `csharp_local_function_in_if_…`) — those catch it only because a
11479 /// function boundary resets `conditional` alone, leaving the misplaced
11480 /// increment behind. This test pins it at the helper, where the
11481 /// mistake is, rather than five languages away from it.
11482 #[test]
11483 fn increase_nesting_charges_the_total_but_advances_only_conditional() {
11484 let mut nesting = Nesting {
11485 conditional: 1,
11486 function_depth: 2,
11487 lambda: 4,
11488 };
11489 // Both fields are seeded rather than defaulted. From
11490 // `Stats::default()` the `boolean_seq` assertion holds even with
11491 // `reset()` deleted, and the `structural` assertion cannot tell
11492 // `increment`'s `+=` from a plain `=`, since both start at zero.
11493 let mut stats = Stats {
11494 structural: 5,
11495 boolean_seq: BoolSequence {
11496 boolean_op: Some((1, 0)),
11497 },
11498 ..Stats::default()
11499 };
11500
11501 increase_nesting(&mut stats, &mut nesting);
11502
11503 // Charged at the inherited level (7), and `increment`
11504 // accumulates `nesting + 1` onto the seeded 5.
11505 assert_eq!(stats.nesting, 7);
11506 assert_eq!(stats.structural, 13);
11507 assert_eq!(stats.boolean_seq, BoolSequence::default());
11508 assert_eq!(
11509 nesting,
11510 Nesting {
11511 conditional: 2,
11512 function_depth: 2,
11513 lambda: 4,
11514 }
11515 );
11516 }
11517
11518 /// A fold applies its operator once, so it is one boolean sequence,
11519 /// as cyclomatic counts it one decision: each scores what its spelled
11520 /// twin does, binary folds and folds inside a chain included. Every
11521 /// fold scored 0 (#1533).
11522 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
11523 fn assert_cpp_fold_scores_like_its_twin<P: MetricSuite>() {
11524 for (fold, twin, want) in [
11525 ("return (... && a);", "return a && a;", 1),
11526 ("return (a || ...);", "return a || a;", 1),
11527 ("return (true && ... && a);", "return true && a;", 1),
11528 ("return (... and a) || b;", "return (a and a) || b;", 2),
11529 ("return x && (... && a);", "return x && (a && a);", 1),
11530 (
11531 "if ((... || a)) { g(); } return 0;",
11532 "if (a || a) { g(); } return 0;",
11533 2,
11534 ),
11535 ("return (... == a);", "return a == a;", 0),
11536 ] {
11537 for body in [fold, twin] {
11538 let src = format!("template <class... T> bool f(T... a) {{ {body} }}");
11539 check_func_space::<P, _>(&src, "foo.cpp", |space| {
11540 assert_eq!(space.metrics.cognitive.cognitive_sum(), want, "{src}");
11541 });
11542 }
11543 }
11544 }
11545
11546 #[cfg(feature = "cpp")]
11547 #[test]
11548 fn cpp_fold_scores_like_its_twin() {
11549 assert_cpp_fold_scores_like_its_twin::<CppParser>();
11550 }
11551
11552 /// Mozcpp owns no file extension; this pins its clone of the arm.
11553 #[cfg(feature = "mozcpp")]
11554 #[test]
11555 fn mozcpp_fold_scores_like_its_twin() {
11556 assert_cpp_fold_scores_like_its_twin::<MozcppParser>();
11557 }
11558
11559 /// A requires clause starts no boolean sequence and opens no nesting,
11560 /// however it is written: unparenthesised it is a
11561 /// `constraint_conjunction`, parenthesised an ordinary
11562 /// `binary_expression`, and a requires-expression's requirements are
11563 /// never evaluated (#1533). The twins that still count are the
11564 /// `static_assert` in `g`, the concept body `C`, and the `&&` in
11565 /// `h`'s body, which pins that the clause's reach ends where the
11566 /// clause does. So the file reads 1 + 1 + 1, every `f` zero.
11567 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
11568 const CPP_REQUIRES_FIXTURE: &str = "\
11569template<class T> requires A<T> && B<T> void f1(T) {}
11570template<class T> requires (A<T> && B<T>) void f2(T) {}
11571template<class T> requires (!(A<T> || B<T>)) void f3(T) {}
11572template<class... T> requires (... && A<T>) void f4(T...) {}
11573template<class T> requires (A<T> && B<T>) && C<T> void f5(T) {}
11574template<class T> requires (X ? A<T> : B<T>) void f6(T) {}
11575template<class T> requires requires(T a) { a && a; } void f7(T) {}
11576template<class T> requires ([] { return A<T> && B<T>; }()) void f8(T) {}
11577template<class T> void g(T) { static_assert(A<T> && B<T>); }
11578template<class T> concept C = A<T> && B<T>;
11579template<class T> bool h(T a) requires (A<T> || B<T>) { return a && a; }
11580";
11581
11582 #[cfg(feature = "cpp")]
11583 #[test]
11584 fn cpp_requires_constraint_scores_nothing() {
11585 check_metrics::<CppParser>(CPP_REQUIRES_FIXTURE, "foo.cpp", |metric| {
11586 assert_eq!(metric.cognitive.cognitive_sum(), 3);
11587 });
11588 }
11589
11590 /// Mozcpp owns no file extension; this pins its clone of the arm.
11591 #[cfg(feature = "mozcpp")]
11592 #[test]
11593 fn mozcpp_requires_constraint_scores_nothing() {
11594 check_metrics::<MozcppParser>(CPP_REQUIRES_FIXTURE, "foo.cpp", |metric| {
11595 assert_eq!(metric.cognitive.cognitive_sum(), 3);
11596 });
11597 }
11598}
11599
11600/// The nameless constructs from #1184 are function boundaries, so a
11601/// deeply-nested one must score what the same body scores as an
11602/// ordinary method in the same position (#1184).
11603///
11604/// Each opens a `FuncSpace`, and without a cognitive boundary arm it
11605/// reached none and inherited the enclosing conditional nesting: a
11606/// Kotlin accessor nested two `if`s deep scored 7 where the method
11607/// beside it scored 5.
11608///
11609/// **Two levels of nesting are load-bearing.** At one level the fixture
11610/// reports the same number either way, which is the trap the issue's own
11611/// checklist warns about — a first draft of this test used one `if` and
11612/// could not discriminate the fix from its absence.
11613///
11614/// The comparison is against a *sibling method* rather than an absolute
11615/// number, so the assertion states the property (these are ordinary
11616/// function boundaries) rather than a value that moves with any
11617/// unrelated re-tuning. The absolute is pinned too, so a regression
11618/// moving both equally still fails.
11619// Gated on the union of the three languages `cases()` has a row for, so
11620// a build enabling none of them drops the module rather than tripping
11621// its `checked > 0` guard (`.claude/rules/testing.md`, #1286).
11622#[cfg(test)]
11623#[cfg(any(feature = "java", feature = "javascript", feature = "kotlin"))]
11624mod nameless_construct_boundaries {
11625 use crate::test_support::space_verbatim;
11626 use crate::{FuncSpace, LANG, MetricsOptions};
11627
11628 fn score(lang: LANG, source: &str, name: &str) -> u64 {
11629 fn find(s: &FuncSpace, name: &str) -> Option<u64> {
11630 if s.name.as_deref() == Some(name) {
11631 return Some(s.metrics.cognitive.cognitive());
11632 }
11633 s.spaces.iter().find_map(|c| find(c, name))
11634 }
11635 let root = space_verbatim(lang, source.as_bytes(), MetricsOptions::default());
11636 find(&root, name)
11637 .unwrap_or_else(|| panic!("{lang:?}: no space named {name:?} in the fixture"))
11638 }
11639
11640 /// `(language, source, construct name, sibling method name)`. Each
11641 /// fixture nests a class two `if`s deep and gives it both the
11642 /// nameless construct and an ordinary method with a byte-identical
11643 /// body.
11644 fn cases() -> Vec<(LANG, &'static str, &'static str, &'static str)> {
11645 vec![
11646 (
11647 LANG::Kotlin,
11648 "fun outer(a: Boolean) { if (a) { if (a) { class D {\n\
11649 \x20 var q: Int = 0\n\
11650 \x20 get() { if (q > 0) { if (q > 1) { return 2 } }; return 0 }\n\
11651 \x20 fun m(): Int { if (q > 0) { if (q > 1) { return 2 } }; return 0 }\n\
11652 } } } }\n",
11653 "<get>",
11654 "m",
11655 ),
11656 (
11657 LANG::Java,
11658 "class K { void outer(boolean a) { if (a) { if (a) { class D {\n\
11659 \x20 static int x;\n\
11660 \x20 static { if (x > 0) { if (x > 1) { x = 2; } } }\n\
11661 \x20 void m() { if (x > 0) { if (x > 1) { x = 2; } } }\n\
11662 } } } } }\n",
11663 "<static-init>",
11664 "m",
11665 ),
11666 (
11667 LANG::Javascript,
11668 "function outer(a) { if (a) { if (a) { class D {\n\
11669 \x20 static x;\n\
11670 \x20 static { if (D.x > 0) { if (D.x > 1) { D.x = 2; } } }\n\
11671 \x20 m() { if (D.x > 0) { if (D.x > 1) { D.x = 2; } } }\n\
11672 } } } }\n",
11673 "<static-init>",
11674 "m",
11675 ),
11676 ]
11677 }
11678
11679 #[test]
11680 fn a_nested_nameless_construct_scores_like_a_sibling_method() {
11681 let mut checked = 0;
11682 for (lang, source, construct, method) in cases() {
11683 if !lang.is_enabled() {
11684 continue;
11685 }
11686 checked += 1;
11687 let (got, want) = (score(lang, source, construct), score(lang, source, method));
11688 assert_eq!(
11689 got, want,
11690 "{lang:?}: {construct} scored {got} where the sibling method scored {want}; \
11691 the construct is inheriting the enclosing nesting",
11692 );
11693 // expected: two `if`s at +1 and +2 = 3, plus +1 each for the
11694 // function-depth surcharge from `outer` = 5. Pinned so a
11695 // regression that moved both sides equally still fails.
11696 assert_eq!(want, 5, "{lang:?}: the baseline itself moved");
11697 }
11698 assert!(
11699 checked > 0,
11700 "no language enabled; this test asserted nothing"
11701 );
11702 }
11703}