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