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