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