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