big_code_analysis/metrics/cyclomatic.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(clippy::wildcard_imports, clippy::enum_glob_use)]
8// Metric counts (token, function, branch, argument, etc.) are stored as
9// `usize` and crossed with `f64` averages, ratios, and Halstead scores
10// across the cyclomatic / MI / Halstead computations. The `usize as f64`
11// and `f64 as usize` casts are intentional and snapshot-anchored — every
12// site is bounded by the count it came from. Allowing the lints at the
13// module level keeps the metric arithmetic legible.
14#![allow(
15 clippy::cast_precision_loss,
16 clippy::cast_possible_truncation,
17 clippy::cast_sign_loss
18)]
19
20use std::fmt;
21
22use crate::checker::Checker;
23use crate::macros::implement_metric_trait;
24use crate::*;
25
26/// The `Cyclomatic` metric.
27#[derive(Debug, Clone, PartialEq)]
28#[non_exhaustive]
29pub struct Stats {
30 cyclomatic_sum: f64,
31 cyclomatic: f64,
32 /// Number of function/closure spaces in this subtree, used as the
33 /// per-function divisor for the cyclomatic averages.
34 ///
35 /// Seeded to `1` for a [`SpaceKind::Function`][crate::SpaceKind]
36 /// space and `0` otherwise (see [`Stats::note_function_space`]), then
37 /// summed across child spaces in [`Stats::merge`]. This is the
38 /// per-function divisor convention shared with `cognitive`/`exit`/
39 /// `nargs`, sourced independently of whether the `Nom` metric was
40 /// selected (#512).
41 ///
42 /// It counts the function/closure *spaces* — the spaces that each
43 /// contribute a base cyclomatic value to the sum — so it equals
44 /// `nom.total()` wherever every function and closure opens its own
45 /// space (the common case). The known exception is a closure form
46 /// that opens no space, such as a Python `lambda`: `nom` counts it
47 /// but it folds its decisions into the enclosing space, so
48 /// `function_spaces` does not count it as a separate divisor unit.
49 function_spaces: usize,
50 cyclomatic_max: f64,
51 cyclomatic_min: f64,
52 cyclomatic_modified_sum: f64,
53 cyclomatic_modified: f64,
54 cyclomatic_modified_max: f64,
55 cyclomatic_modified_min: f64,
56}
57
58impl Default for Stats {
59 fn default() -> Self {
60 Self {
61 cyclomatic_sum: 0.,
62 cyclomatic: 1.,
63 function_spaces: 0,
64 cyclomatic_max: 0.,
65 cyclomatic_min: f64::MAX,
66 cyclomatic_modified_sum: 0.,
67 cyclomatic_modified: 1.,
68 cyclomatic_modified_max: 0.,
69 cyclomatic_modified_min: f64::MAX,
70 }
71 }
72}
73
74impl fmt::Display for Stats {
75 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
76 write!(
77 f,
78 "sum: {}, average: {}, min: {}, max: {}, \
79 modified_sum: {}, modified_average: {}, modified_min: {}, modified_max: {}",
80 self.cyclomatic_sum(),
81 self.cyclomatic_average(),
82 self.cyclomatic_min(),
83 self.cyclomatic_max(),
84 self.cyclomatic_modified_sum(),
85 self.cyclomatic_modified_average(),
86 self.cyclomatic_modified_min(),
87 self.cyclomatic_modified_max(),
88 )
89 }
90}
91
92impl Stats {
93 /// Merges a second `Cyclomatic` metric into the first one
94 pub fn merge(&mut self, other: &Stats) {
95 self.cyclomatic_max = self.cyclomatic_max.max(other.cyclomatic_max);
96 self.cyclomatic_min = self.cyclomatic_min.min(other.cyclomatic_min);
97 self.cyclomatic_sum += other.cyclomatic_sum;
98 self.function_spaces += other.function_spaces;
99
100 self.cyclomatic_modified_max = self
101 .cyclomatic_modified_max
102 .max(other.cyclomatic_modified_max);
103 self.cyclomatic_modified_min = self
104 .cyclomatic_modified_min
105 .min(other.cyclomatic_modified_min);
106 self.cyclomatic_modified_sum += other.cyclomatic_modified_sum;
107 }
108
109 /// Returns the `Cyclomatic` metric value for the current space.
110 #[must_use]
111 pub fn cyclomatic(&self) -> u64 {
112 self.cyclomatic as u64
113 }
114
115 /// Returns the sum of standard cyclomatic values across all spaces.
116 #[must_use]
117 pub fn cyclomatic_sum(&self) -> u64 {
118 self.cyclomatic_sum as u64
119 }
120
121 /// Returns the average standard cyclomatic complexity.
122 ///
123 /// The divisor is the number of function/closure spaces in the
124 /// subtree (`function_spaces`), guarded with `.max(1)` via the shared
125 /// `average` helper. This is the per-function convention shared with
126 /// `cognitive`/`exit`/`nargs`; before #512 the divisor was the
127 /// per-space count `n`, which also counted classes, structs, and the
128 /// file unit and so reported a different — lower — average.
129 #[must_use]
130 pub fn cyclomatic_average(&self) -> f64 {
131 crate::metrics::average(self.cyclomatic_sum() as f64, self.function_spaces)
132 }
133
134 /// Returns the maximum standard cyclomatic complexity.
135 #[must_use]
136 pub fn cyclomatic_max(&self) -> u64 {
137 self.cyclomatic_max as u64
138 }
139
140 /// Returns the minimum standard cyclomatic complexity.
141 ///
142 /// Collapses the `f64::MAX` sentinel that `Stats::default()` plants
143 /// into `cyclomatic_min` to `0`, so a never-observed space
144 /// serializes to a meaningful number rather than `1.7976931e308`.
145 #[allow(clippy::float_cmp)]
146 #[must_use]
147 pub fn cyclomatic_min(&self) -> u64 {
148 if self.cyclomatic_min == f64::MAX {
149 0
150 } else {
151 self.cyclomatic_min as u64
152 }
153 }
154
155 /// Returns the modified cyclomatic complexity for the current space.
156 ///
157 /// Modified cyclomatic counts each switch/match/when/select container as
158 /// one decision point regardless of how many case arms it contains. All
159 /// other branching constructs are weighted identically to standard CCN.
160 ///
161 /// Edge case: an empty switch (`switch (x) {}`) yields modified = 1
162 /// and standard = 0, so modified can exceed standard for arm-less
163 /// containers. This matches Lizard's `-m` convention, which keys on
164 /// the switch keyword rather than the presence of arms.
165 #[must_use]
166 pub fn cyclomatic_modified(&self) -> u64 {
167 self.cyclomatic_modified as u64
168 }
169
170 /// Returns the sum of modified cyclomatic values across all spaces.
171 #[must_use]
172 pub fn cyclomatic_modified_sum(&self) -> u64 {
173 self.cyclomatic_modified_sum as u64
174 }
175
176 /// Returns the average modified cyclomatic complexity.
177 ///
178 /// Uses the same per-function divisor (`function_spaces`, guarded by
179 /// the shared `average` helper) as [`Stats::cyclomatic_average`].
180 #[must_use]
181 pub fn cyclomatic_modified_average(&self) -> f64 {
182 crate::metrics::average(self.cyclomatic_modified_sum() as f64, self.function_spaces)
183 }
184
185 /// Returns the maximum modified cyclomatic complexity.
186 #[must_use]
187 pub fn cyclomatic_modified_max(&self) -> u64 {
188 self.cyclomatic_modified_max as u64
189 }
190
191 /// Returns the minimum modified cyclomatic complexity.
192 ///
193 /// Same `f64::MAX` sentinel collapse as `cyclomatic_min`.
194 #[allow(clippy::float_cmp)]
195 #[must_use]
196 pub fn cyclomatic_modified_min(&self) -> u64 {
197 if self.cyclomatic_modified_min == f64::MAX {
198 0
199 } else {
200 self.cyclomatic_modified_min as u64
201 }
202 }
203
204 /// Marks this space as a function/closure space, seeding the
205 /// per-function divisor (`function_spaces`) with `1`.
206 ///
207 /// Called once at space construction for every
208 /// [`SpaceKind::Function`][crate::SpaceKind] space; non-function
209 /// spaces leave the seed at its `0` default. [`Stats::merge`] then
210 /// sums the seeds so each space's `function_spaces` reflects the
211 /// function/closure count of its whole subtree — independently of
212 /// the `Nom` metric (#512).
213 #[inline]
214 pub(crate) fn note_function_space(&mut self) {
215 self.function_spaces = 1;
216 }
217
218 #[inline]
219 pub(crate) fn compute_sum(&mut self) {
220 self.cyclomatic_sum += self.cyclomatic;
221 self.cyclomatic_modified_sum += self.cyclomatic_modified;
222 }
223
224 #[inline]
225 pub(crate) fn compute_minmax(&mut self) {
226 self.cyclomatic_max = self.cyclomatic_max.max(self.cyclomatic);
227 self.cyclomatic_min = self.cyclomatic_min.min(self.cyclomatic);
228 self.cyclomatic_modified_max = self.cyclomatic_modified_max.max(self.cyclomatic_modified);
229 self.cyclomatic_modified_min = self.cyclomatic_modified_min.min(self.cyclomatic_modified);
230 self.compute_sum();
231 }
232}
233
234#[doc(hidden)]
235/// Per-language computation of cyclomatic complexity.
236pub(crate) trait Cyclomatic
237where
238 Self: Checker,
239{
240 /// Walk `node` and update `stats` with this metric for the language
241 /// implementing the trait.
242 ///
243 /// `code` is the source bytes the node spans, so that languages
244 /// whose branching constructs surface as untyped `Call` nodes
245 /// (Elixir's `if`/`unless`/`for`/`while`/`with`/`case`/`cond`,
246 /// for example) can identify them by inspecting the call target's
247 /// text. Most languages discard the parameter with `_`.
248 fn compute<'a>(
249 node: &Node<'a>,
250 code: &'a [u8],
251 _ancestors: Ancestors<'a, '_>,
252 stats: &mut Stats,
253 );
254
255 /// Like [`Cyclomatic::compute`], but honors per-traversal options.
256 ///
257 /// `count_try` toggles whether Rust's `?` operator (the
258 /// `try_expression` grammar node) contributes to cyclomatic
259 /// complexity. The default body ignores `count_try` and delegates
260 /// to [`Cyclomatic::compute`], so every language whose grammar has
261 /// no `try_expression` node keeps its existing behaviour with no
262 /// per-language edit. Only [`RustCode`] overrides this to act on
263 /// the flag (#409).
264 #[inline]
265 fn compute_with_options<'a>(
266 node: &Node<'a>,
267 code: &'a [u8],
268 ancestors: Ancestors<'a, '_>,
269 stats: &mut Stats,
270 count_try: bool,
271 ) {
272 let _ = count_try;
273 Self::compute(node, code, ancestors, stats);
274 }
275}
276
277/// C-family cyclomatic: `Case` adds standard, `SwitchStatement` adds
278/// modified, and the shared branching kinds add both. The ternary token
279/// name varies (`TernaryExpression` for JS-family, `ConditionalExpression`
280/// for Cpp), so it's a parameter. The short-circuit operator list is
281/// also a parameter because JS-family languages include nullish
282/// coalescing (`??`, token `QMARKQMARK`) and the three compound short-
283/// circuit assignment forms `&&=` (`AMPAMPEQ`), `||=` (`PIPEPIPEEQ`),
284/// `??=` (`QMARKQMARKEQ`) on top of `&&` and `||`, while C++ has only
285/// `&&` and `||` (issues #226, #231, #248).
286///
287/// **`If` / `For` / `While` are keyword tokens in the per-language
288/// enums (e.g. `Cpp::While == "while"`), not statement nodes.** The
289/// `while` token therefore fires once inside both `WhileStatement` AND
290/// `DoStatement` (the `while` keyword of `do { … } while (…)`), and
291/// the `for` token fires once inside `ForStatement`, C++
292/// `ForRangeLoop`, Java `EnhancedForStatement`, and any other
293/// grammar-specific loop form that spells the keyword `for`. So
294/// adding the statement nodes themselves would double-count those
295/// loops — see issue #284 for the false-positive analysis. The
296/// regression tests `cpp_do_statement_counts_in_cyclomatic`,
297/// `cpp_for_range_loop_counts_in_cyclomatic`,
298/// `java_do_statement_counts_in_cyclomatic`, and
299/// `java_enhanced_for_statement_counts_in_cyclomatic` pin the
300/// correct keyword-driven counts.
301macro_rules! impl_cyclomatic_c_family {
302 ($code:ty, $lang:ident, $ternary:ident, [$($short_circuit:ident),+ $(,)?]) => {
303 impl Cyclomatic for $code {
304 fn compute<'a>(
305 node: &Node<'a>,
306 _code: &'a [u8],
307 _ancestors: Ancestors<'a, '_>,
308 stats: &mut Stats,
309 ) {
310 use $lang::*;
311 match node.kind_id().into() {
312 Case => stats.cyclomatic += 1.,
313 SwitchStatement => stats.cyclomatic_modified += 1.,
314 If | For | While | Catch | $ternary $(| $short_circuit)+ => {
315 stats.cyclomatic += 1.;
316 stats.cyclomatic_modified += 1.;
317 }
318 _ => {}
319 }
320 }
321 }
322 };
323}
324
325// JS-family: include nullish coalescing (`??`) and the three compound
326// short-circuit assignments `&&=`, `||=`, `??=` as short-circuit
327// decisions in addition to `&&` and `||` (issues #226, #231, #248).
328// Each `op=` is semantically `x = x op y` — one short-circuit decision
329// edge, same as the bare operator. Cognitive parity comes from #236.
330//
331// Optional chaining `?.` is also short-circuit (it skips the rest of
332// the chain when the LHS is nullish) and adds one decision point per
333// occurrence (issue #281). The token varies across grammars:
334// JS/MozJS expose only `OptionalChain` (which IS the `?.` token in
335// those grammars), while TS/TSX expose both an `optional_chain`
336// wrapper and a child `?.` token (`QMARKDOT`); counting `QMARKDOT`
337// matches every textual `?.` exactly once in TS/TSX.
338macro_rules! impl_cyclomatic_js_family {
339 ($code:ty, $lang:ident, $opt_chain:ident) => {
340 impl_cyclomatic_c_family!(
341 $code,
342 $lang,
343 TernaryExpression,
344 [
345 AMPAMP,
346 PIPEPIPE,
347 QMARKQMARK,
348 AMPAMPEQ,
349 PIPEPIPEEQ,
350 QMARKQMARKEQ,
351 $opt_chain
352 ]
353 );
354 };
355}
356
357// Java and Groovy share the same decision-kind set for cyclomatic
358// complexity; Groovy adds `Assert` as an extra branch (its `assert`
359// keyword is a runtime check that branches on its condition,
360// matching Sonar's standard-CCN treatment). `impl_cyclomatic_java_like!`
361// emits the same match body against each enum, with an
362// `[$($extra:ident),*]` list for any language-specific decision kinds
363// (issue #300; mirrors `impl_npm_java_like!` / `impl_npa_java_like!`).
364//
365// Why a dedicated macro instead of reusing `impl_cyclomatic_c_family!`:
366// the C-family macro uses `SwitchStatement` (the wrapping node) as the
367// modified-CCN container marker, whereas Java/Groovy use the `Switch`
368// keyword token — which fires exactly once per switch (both classic
369// switch statements and Java 14+ switch expressions). Counting the
370// keyword keeps the modified-CCN tally aligned with the standard-CCN
371// `Case` arms.
372//
373// Keyword-vs-statement (issue #284): `If` / `For` / `While` here are
374// the *keyword* tokens (`Java::While == "while"`, etc.), not the
375// statement nodes. The `while` keyword therefore fires inside both
376// `WhileStatement` and `DoStatement`, and the `for` keyword fires
377// inside both `ForStatement` and `EnhancedForStatement`. The
378// grammar-specific loop forms are already counted via their inner
379// keyword tokens; listing the statement nodes here would
380// double-count. The regression tests
381// `java_do_statement_counts_in_cyclomatic`,
382// `java_enhanced_for_statement_counts_in_cyclomatic`,
383// `groovy_do_statement_counts_in_cyclomatic`, and
384// `groovy_enhanced_for_statement_counts_in_cyclomatic` pin the
385// correct keyword-driven counts.
386//
387// Groovy note: under the pinned dekobon grammar (root Cargo.toml),
388// Elvis `?:` and the safe-navigation operators `?.` / `??.` all parse
389// cleanly to dedicated nodes with real lexer tokens, so they are
390// counted as branches via the GroovyCode extra-token list below (see
391// the per-call rationale at that invocation). This differs from
392// amaanq's grammar, which emitted ERROR nodes for those constructs.
393macro_rules! impl_cyclomatic_java_like {
394 ($code:ty, $lang:ident, [$($extra:ident),* $(,)?]) => {
395 impl Cyclomatic for $code {
396 fn compute<'a>(
397 node: &Node<'a>,
398 _code: &'a [u8],
399 _ancestors: Ancestors<'a, '_>,
400 stats: &mut Stats,
401 ) {
402 use $lang::*;
403
404 match node.kind_id().into() {
405 Case => {
406 stats.cyclomatic += 1.;
407 }
408 Switch => {
409 stats.cyclomatic_modified += 1.;
410 }
411 If | For | While | Catch | TernaryExpression | AMPAMP | PIPEPIPE
412 $(| $extra)* => {
413 stats.cyclomatic += 1.;
414 stats.cyclomatic_modified += 1.;
415 }
416 _ => {}
417 }
418 }
419 }
420 };
421}
422
423// Real defaults — no executable branches. Audited in #188.
424implement_metric_trait!(Cyclomatic, PreprocCode, CcommentCode);
425
426/// Detects C# `switch_expression_arm`s whose pattern is a bare discard
427/// (`_` or `var _`) and which carry no `when` guard — the analogue of
428/// the C-family `default:` arm. Such arms must NOT contribute to
429/// standard CCN, mirroring Rust's `_ =>` and Java/C#'s `default:`
430/// treatment (lesson 11 / parity family 5). A guarded discard
431/// (`_ when g => …`) still counts because the guard introduces a
432/// non-trivial decision, matching Rust's `_ if g` rule.
433pub(crate) fn csharp_switch_expression_arm_is_bare_discard(node: &Node) -> bool {
434 use Csharp::*;
435
436 /// Classification of a `switch_expression_arm`'s pattern child.
437 /// `BareDiscard` means `_` or `var _` (the C# analogue of
438 /// `default:`); any concrete type test, constant, or composite
439 /// pattern is `NotDiscard` and still contributes to standard CCN.
440 enum PatternKind {
441 BareDiscard,
442 NotDiscard,
443 }
444
445 fn classify_pattern(child: &Node) -> PatternKind {
446 match child.kind_id().into() {
447 // `pattern` is a supertype: tree-sitter flattens it to the
448 // concrete subtype in the parse tree, so a bare `_` arm
449 // surfaces as a direct `discard` child.
450 Discard => PatternKind::BareDiscard,
451 // `var _` parses as a `declaration_pattern` with children
452 // `implicit_type` (`var`) and `discard` (`_`) rather than
453 // as a `var_pattern` — tree-sitter-c-sharp treats `var` as
454 // an implicit type designator. A `declaration_pattern`
455 // whose only named children are `implicit_type` and
456 // `discard` is therefore semantically the bare discard.
457 // A non-implicit type (`int _`) is NOT excluded — the
458 // type test is still a non-trivial decision.
459 DeclarationPattern => {
460 let mut saw_discard = false;
461 let mut saw_implicit_type = false;
462 for sub in child.children().filter(Node::is_named) {
463 match sub.kind_id().into() {
464 Discard => saw_discard = true,
465 ImplicitType => saw_implicit_type = true,
466 _ => return PatternKind::NotDiscard,
467 }
468 }
469 if saw_discard && saw_implicit_type {
470 PatternKind::BareDiscard
471 } else {
472 PatternKind::NotDiscard
473 }
474 }
475 _ => PatternKind::NotDiscard,
476 }
477 }
478
479 let mut named = node.children().filter(Node::is_named);
480 let Some(pattern) = named.next() else {
481 return false;
482 };
483 let PatternKind::BareDiscard = classify_pattern(&pattern) else {
484 return false;
485 };
486 // A guarded discard (`_ when g => …`) still counts because the
487 // guard introduces a non-trivial decision, matching Rust's
488 // `_ if g` rule.
489 !named.any(|c| c.kind_id() == WhenClause)
490}
491
492/// Detects Kotlin `when_entry` nodes that are `else -> …` arms — the
493/// analogue of the C-family `default:` arm. tree-sitter-kotlin-ng
494/// attaches a `condition` field to every case-style entry; the `else`
495/// arm has no `condition` field (only an anonymous `else` keyword
496/// child). Such arms must NOT contribute to standard CCN.
497pub(crate) fn kotlin_when_entry_is_else(node: &Node) -> bool {
498 node.child_by_field_name("condition").is_none()
499}
500
501/// Detects Bash `*)` catch-all arms inside `case … esac`. Returns
502/// `true` when the case_item has exactly one `value` field whose
503/// source text is the literal `*`. Multi-value patterns (`a|b`,
504/// `*|b`) are NOT bare and still count as decisions.
505pub(crate) fn bash_case_item_is_bare_wildcard(node: &Node, code: &[u8]) -> bool {
506 // tree-sitter-bash attaches the `value` field to each alternation
507 // in the case pattern (`a|b)` produces two `value` children).
508 // Walk via a single `TreeCursor`: `field_name()` exposes the field
509 // for the current position and `goto_next_sibling()` is O(1), so
510 // total cost is linear in child count — avoiding the per-call
511 // O(i) `Node::child(i)` access that an index-based loop would
512 // pay on every iteration.
513 let mut cursor = node.as_tree_sitter().walk();
514 if !cursor.goto_first_child() {
515 return false;
516 }
517 let mut value_count = 0usize;
518 let mut sole_value_is_star = false;
519 loop {
520 if cursor.field_name() == Some("value") {
521 value_count += 1;
522 if value_count > 1 {
523 return false;
524 }
525 sole_value_is_star = cursor.node().utf8_text(code).is_ok_and(|s| s.trim() == "*");
526 }
527 if !cursor.goto_next_sibling() {
528 break;
529 }
530 }
531 value_count == 1 && sole_value_is_star
532}
533
534// Per-language `Cyclomatic` impls live in sibling modules. The `mod`
535// declarations sit after the local `macro_rules!` so textual macro
536// scoping reaches the child files (mirrors `getter.rs` and
537// `metrics::abc`).
538mod bash;
539mod c;
540mod cpp;
541mod csharp;
542mod elixir;
543mod go;
544mod groovy;
545mod irules;
546mod java;
547mod javascript;
548mod kotlin;
549mod lua;
550mod mozcpp;
551mod mozjs;
552mod objc;
553mod perl;
554mod php;
555mod python;
556mod ruby;
557mod rust;
558mod tcl;
559mod tsx;
560mod typescript;
561
562#[cfg(test)]
563#[allow(
564 clippy::float_cmp,
565 clippy::cast_precision_loss,
566 clippy::cast_possible_truncation,
567 clippy::cast_sign_loss,
568 clippy::similar_names,
569 clippy::doc_markdown,
570 clippy::needless_raw_string_hashes,
571 clippy::too_many_lines
572)]
573mod tests {
574 use crate::test_support::{ast_has_kind_id, check_metrics_only_shim};
575
576 use super::*;
577
578 check_metrics_only_shim!(check_metrics, Cyclomatic);
579 // Two tests reconcile the cyclomatic per-function divisor against
580 // `nom.total()` (#512). Cyclomatic deliberately does *not* declare
581 // Nom as a dependency — `cyclomatic_average_per_function_without_nom_512`
582 // exists to pin that the divisor comes from the space kind, not from
583 // Nom — so those two ask for Nom explicitly.
584 check_metrics_only_shim!(check_cyclomatic_and_nom, Cyclomatic, Nom);
585
586 /// A `Stats::default()` that never sees an
587 /// observation must not leak the `f64::MAX` sentinel for
588 /// `cyclomatic_min` or `cyclomatic_modified_min`. Both getters
589 /// collapse the sentinel to `0.0` so JSON never emits
590 /// `1.7976931e308`.
591 #[test]
592 fn cyclomatic_empty_file_min_is_zero() {
593 let stats = Stats::default();
594 assert_eq!(stats.cyclomatic_min(), 0);
595 assert_eq!(stats.cyclomatic_modified_min(), 0);
596 }
597
598 /// A `Stats::default()` with no function spaces and an unguarded
599 /// divisor would divide by zero. The shared `average` helper guards
600 /// the divisor with `.max(1)`, so a function-less aggregate yields a
601 /// finite `0.0` rather than `NaN` (#512 — the guard `cognitive`/
602 /// `exit`/`nargs` already had, now applied to cyclomatic too).
603 #[test]
604 fn cyclomatic_no_function_spaces_average_is_finite() {
605 let stats = Stats::default();
606 assert_eq!(stats.cyclomatic_average(), 0.0);
607 assert_eq!(stats.cyclomatic_modified_average(), 0.0);
608 }
609
610 /// #512: the cyclomatic average divisor is now the per-function count
611 /// (`function_spaces`), reconciled with the `cognitive`/`exit`/`nargs`
612 /// convention, *not* the per-space count `n` it used before. For a
613 /// file with one class holding two methods the spaces are
614 /// `{unit, class, method, method}` (4) but only the two methods are
615 /// functions, so the divisor is 2.
616 ///
617 /// Here every function/closure opens its own space, so
618 /// `function_spaces == nom.total()` and
619 /// `cyclomatic_average == cyclomatic_sum / nom.total()` — the same
620 /// denominator `cognitive_average` divides by. (That equality can
621 /// break for closure forms that open no space, e.g. Python lambdas —
622 /// see `cyclomatic_python_lambda_divisor_excludes_spaceless_closure`.)
623 /// Before #512 the divisor was 4 (every space, base 1 each) and the
624 /// averages were two-thirds of these values (`6 / 4 == 1.5`).
625 #[test]
626 fn cyclomatic_average_is_per_function_512() {
627 check_cyclomatic_and_nom::<CsharpParser>(
628 "class A {
629 int f(int x) { return x > 0 ? 1 : 2; }
630 int g(int x) { return x > 0 ? 1 : 2; }
631 }",
632 "foo.cs",
633 |metric| {
634 // Sum is over every space's base 1 plus its decisions:
635 // unit(1) + class(1) + f(1 + ternary 1) + g(1 + ternary 1)
636 // = 6.
637 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 6);
638 // Divisor is the two function spaces, not the four total
639 // spaces: 6 / 2 = 3.0 (was 6 / 4 = 1.5 before #512).
640 assert_eq!(metric.cyclomatic.cyclomatic_average(), 3.0);
641 assert_eq!(metric.cyclomatic.cyclomatic_modified_average(), 3.0);
642 // Reconciliation invariant: cyclomatic divides by the same
643 // function/closure count cognitive does.
644 assert_eq!(
645 metric.cyclomatic.cyclomatic_average(),
646 metric.cyclomatic.cyclomatic_sum() as f64 / metric.nom.total() as f64
647 );
648 assert_eq!(metric.nom.total(), 2);
649 },
650 );
651 }
652
653 /// #512: the per-function divisor is sourced from the space kind, not
654 /// from the `Nom` metric, so selecting `cyclomatic` *alone* (which
655 /// does not pull `Nom` in via the metric-selection dependency graph)
656 /// still divides by the function count. This guards the load-bearing
657 /// "one selected metric emits exactly that metric" contract: coupling
658 /// cyclomatic to `nom` to obtain the divisor would have leaked a
659 /// `nom` block into a cyclomatic-only selection.
660 ///
661 /// `nom.total()` is `0.0` here (Nom was never computed) yet the
662 /// average is still the correct per-function `6 / 2 == 3.0` — proof
663 /// the divisor does not read `nom`.
664 #[test]
665 fn cyclomatic_average_per_function_without_nom_512() {
666 let space = crate::analyze(
667 crate::Source::new(
668 crate::LANG::Csharp,
669 b"class A {\n int f(int x) { return x > 0 ? 1 : 2; }\n int g(int x) { return x > 0 ? 1 : 2; }\n}",
670 )
671 .with_name(Some("foo.cs".to_owned())),
672 crate::MetricsOptions::default().with_only(&[crate::Metric::Cyclomatic]),
673 )
674 .expect("analyze must succeed on a well-formed C# fixture");
675
676 let c = &space.metrics.cyclomatic;
677 assert_eq!(c.cyclomatic_sum(), 6);
678 assert_eq!(c.cyclomatic_average(), 3.0);
679 assert_eq!(c.cyclomatic_modified_average(), 3.0);
680 // Nom was not selected, so its count stays at the zero default —
681 // the cyclomatic divisor must not depend on it.
682 assert_eq!(space.metrics.nom.total(), 0);
683 }
684
685 /// #512 edge case: a Python `lambda` is counted by `nom` (as a
686 /// closure) but opens **no** function space — it folds its decisions
687 /// into the enclosing space. So `function_spaces` counts only the
688 /// spaces that actually carry a cyclomatic value (here the single
689 /// `def`), and the cyclomatic divisor is `1`, not `nom.total()`'s `2`.
690 ///
691 /// This documents that the per-function reconciliation with
692 /// `cognitive` is exact only where every function/closure opens its
693 /// own space; for spaceless closures `function_spaces` is the divisor
694 /// that matches the spaces contributing to `cyclomatic_sum`. The
695 /// behaviour is intentional, not a bug — pinning it so a future change
696 /// to lambda space-handling is a deliberate, visible decision.
697 #[test]
698 fn cyclomatic_python_lambda_divisor_excludes_spaceless_closure() {
699 check_cyclomatic_and_nom::<PythonParser>(
700 "def f(x):\n return x if x > 0 else -x\ng = lambda y: y if y else 0\n",
701 "p.py",
702 |metric| {
703 // sum: unit(1 + lambda's ternary 1) + f(1 + ternary 1) = 4.
704 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
705 // nom counts the lambda as a closure, so total == 2 …
706 assert_eq!(metric.nom.total(), 2);
707 // … but only the `def` opens a function space, so the
708 // cyclomatic divisor is 1: 4 / 1 = 4.0, which deliberately
709 // differs from cyclomatic_sum / nom.total() (4 / 2 = 2.0).
710 assert_eq!(metric.cyclomatic.cyclomatic_average(), 4.0);
711 },
712 );
713 }
714
715 /// A plain `if/else` must not be credited
716 /// as a loop-`else`. The `Else` arm of `impl Cyclomatic for
717 /// PythonCode` previously fired for every `else_clause` because
718 /// the old `has_ancestors` helper only verified the second
719 /// predicate; the rewritten `parent_grandparent_match` requires
720 /// the grandparent to be `for/while/try`.
721 ///
722 /// Expected: unit(1) + fn(1) + if(1) = 3. No contribution from
723 /// `else`.
724 #[test]
725 fn python_if_else_does_not_overcount_229() {
726 check_metrics::<PythonParser>(
727 "def f(x):
728 if x > 0:
729 y = 1
730 else:
731 y = 2
732",
733 "foo.py",
734 |metric| {
735 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
736 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 3);
737 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
738 insta::assert_json_snapshot!(
739 metric.cyclomatic,
740 @r#"
741 {
742 "sum": 3,
743 "value": 1,
744 "average": 3.0,
745 "min": 1,
746 "max": 2,
747 "modified": {
748 "sum": 3,
749 "value": 1,
750 "average": 3.0,
751 "min": 1,
752 "max": 2
753 }
754 }
755 "#
756 );
757 },
758 );
759 }
760
761 /// Companion to #229: a chained `if/elif/else` must count one
762 /// per `if` and per `elif`, never the bare `else`.
763 ///
764 /// Expected: unit(1) + fn(1) + if(1) + elif(1) + elif(1) = 5.
765 #[test]
766 fn python_if_elif_else_chain_229() {
767 check_metrics::<PythonParser>(
768 "def f(x):
769 if x == 1:
770 return 10
771 elif x == 2:
772 return 20
773 elif x == 3:
774 return 30
775 else:
776 return 0
777",
778 "foo.py",
779 |metric| {
780 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
781 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 5);
782 assert_eq!(metric.cyclomatic.cyclomatic_max(), 4);
783 },
784 );
785 }
786
787 /// The for/else feature must still count: the `else` body runs
788 /// only when the loop completes without `break`, which is a
789 /// distinct decision point.
790 ///
791 /// Expected: unit(1) + fn(1) + for(1) + else(1) = 4.
792 #[test]
793 fn python_for_else_still_counts_229() {
794 check_metrics::<PythonParser>(
795 "def f(xs):
796 for x in xs:
797 if x < 0:
798 break
799 else:
800 return True
801 return False
802",
803 "foo.py",
804 |metric| {
805 // fn body has: for(1) + if(1) + for/else(1) = 3 over base 1 -> max = 4
806 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
807 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 5);
808 assert_eq!(metric.cyclomatic.cyclomatic_max(), 4);
809 },
810 );
811 }
812
813 /// Symmetric to for/else: while/else also runs only on normal
814 /// completion of the loop.
815 ///
816 /// Expected: unit(1) + fn(1) + while(1) + else(1) = 4.
817 #[test]
818 fn python_while_else_still_counts_229() {
819 check_metrics::<PythonParser>(
820 "def f(n):
821 while n > 0:
822 n -= 1
823 else:
824 return True
825 return False
826",
827 "foo.py",
828 |metric| {
829 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
830 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
831 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
832 },
833 );
834 }
835
836 /// try/except/else: the `else` body runs only when no exception
837 /// was raised in `try`, mirroring loop-`else` semantics. Counts
838 /// alongside the `except` arm.
839 ///
840 /// Expected: unit(1) + fn(1) + except(1) + try/else(1) = 4.
841 #[test]
842 fn python_try_except_else_counts_229() {
843 check_metrics::<PythonParser>(
844 "def f():
845 try:
846 x = risky()
847 except ValueError:
848 x = -1
849 else:
850 x = x + 1
851 return x
852",
853 "foo.py",
854 |metric| {
855 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
856 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
857 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
858 },
859 );
860 }
861
862 /// `with` is unconditional resource management, not a branch, so it
863 /// must not add to cyclomatic complexity — matching the C-family
864 /// `using` sibling and textbook McCabe. Regression test for #418.
865 ///
866 /// Expected: unit(1) + fn(1) = 2; the `with` adds nothing.
867 #[test]
868 fn python_with_is_not_a_decision_point_418() {
869 check_metrics::<PythonParser>(
870 "def f():
871 with open('a') as fp:
872 return fp.read()
873",
874 "foo.py",
875 |metric| {
876 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 2);
877 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 2);
878 assert_eq!(metric.cyclomatic.cyclomatic_max(), 1);
879 },
880 );
881 }
882
883 /// A `with` managing multiple context managers (`with a, b:`) parses
884 /// as a single `with_statement` with one `with` keyword token, so it
885 /// stays uncounted just like the single-manager form. Companion to
886 /// #418.
887 ///
888 /// Expected: unit(1) + fn(1) = 2.
889 #[test]
890 fn python_with_multiple_managers_is_not_a_decision_point_418() {
891 check_metrics::<PythonParser>(
892 "def f(a, b):
893 with a, b:
894 return 1
895",
896 "foo.py",
897 |metric| {
898 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 2);
899 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 2);
900 },
901 );
902 }
903
904 /// `async with` reuses the same `with` keyword token as plain
905 /// `with`, so dropping `With` from the decision arm stops counting
906 /// it too. Companion to #418.
907 ///
908 /// Expected: unit(1) + fn(1) = 2; neither `async` nor `with` counts.
909 #[test]
910 fn python_async_with_is_not_a_decision_point_418() {
911 check_metrics::<PythonParser>(
912 "async def f():
913 async with open('a') as fp:
914 return fp.read()
915",
916 "foo.py",
917 |metric| {
918 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 2);
919 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 2);
920 },
921 );
922 }
923
924 /// Dropping `With` must not suppress real branches *inside* a `with`
925 /// body: an `if` in the body still counts. Guards against an
926 /// over-broad fix. Companion to #418.
927 ///
928 /// Expected: unit(1) + fn(1) + if(1) = 3; the `with` adds nothing.
929 #[test]
930 fn python_with_body_branch_still_counts_418() {
931 check_metrics::<PythonParser>(
932 "def f(x):
933 with open('a') as fp:
934 if x:
935 return fp.read()
936 return None
937",
938 "foo.py",
939 |metric| {
940 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
941 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 3);
942 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
943 },
944 );
945 }
946
947 #[test]
948 fn python_simple_function() {
949 check_metrics::<PythonParser>(
950 "def f(a, b): # +2 (+1 unit space)
951 if a and b: # +2 (+1 and)
952 return 1
953 if c and d: # +2 (+1 and)
954 return 1",
955 "foo.py",
956 |metric| {
957 // nspace = 2 (func and unit)
958 insta::assert_json_snapshot!(
959 metric.cyclomatic,
960 @r#"
961 {
962 "sum": 6,
963 "value": 1,
964 "average": 6.0,
965 "min": 1,
966 "max": 5,
967 "modified": {
968 "sum": 6,
969 "value": 1,
970 "average": 6.0,
971 "min": 1,
972 "max": 5
973 }
974 }
975 "#
976 );
977 },
978 );
979 }
980
981 /// Python `match`/`case` (PEP 634, 3.10+): each non-bare-wildcard
982 /// arm contributes one standard decision; the containing
983 /// `match_statement` contributes one modified decision. A bare
984 /// `case _:` (no guard) is skipped, mirroring Rust's `MatchArm`
985 /// bare-wildcard filter. Regression test for #212.
986 #[test]
987 fn python_match_two_arm_wildcard() {
988 check_metrics::<PythonParser>(
989 "def f(x):
990 match x:
991 case 1:
992 return 'one'
993 case _:
994 return 'other'
995",
996 "foo.py",
997 |metric| {
998 // standard: 1 (unit) + 1 (fn) + 1 (case 1; case _ skipped) = 3
999 // modified: 1 (unit) + 1 (fn) + 1 (match_statement) = 3
1000 // function space alone holds 1 decision -> max = 2
1001 insta::assert_json_snapshot!(
1002 metric.cyclomatic,
1003 @r#"
1004 {
1005 "sum": 3,
1006 "value": 1,
1007 "average": 3.0,
1008 "min": 1,
1009 "max": 2,
1010 "modified": {
1011 "sum": 3,
1012 "value": 1,
1013 "average": 3.0,
1014 "min": 1,
1015 "max": 2
1016 }
1017 }
1018 "#
1019 );
1020 },
1021 );
1022 }
1023
1024 /// `case _ if guard:` still counts because the guard is an
1025 /// `if_clause` sibling on the `case_clause`, escaping the bare-
1026 /// wildcard filter. The guard's own `if` keyword token is also
1027 /// counted via the existing `If` arm (every `if` keyword in
1028 /// Python contributes a decision) — long-standing behaviour
1029 /// shared with regular `if` statements. Companion to the
1030 /// `python_match_case_guarded_wildcard_counts` test in `abc.rs`.
1031 #[test]
1032 fn python_match_guarded_wildcard_counts() {
1033 check_metrics::<PythonParser>(
1034 "def f(x):
1035 match x:
1036 case 1:
1037 return 'one'
1038 case _ if x > 0:
1039 return 'positive'
1040 case _:
1041 return 'other'
1042",
1043 "foo.py",
1044 |metric| {
1045 // standard: 1 (unit) + 1 (fn) + 1 (case 1)
1046 // + 1 (guarded `case _ if ...` — bare-_ filter
1047 // escaped by the guard)
1048 // + 1 (`if` keyword inside the guard)
1049 // = 5; bare `case _:` is filtered.
1050 // modified: 1 (unit) + 1 (fn) + 1 (match_statement)
1051 // + 1 (`if` keyword in the guard) = 4.
1052 insta::assert_json_snapshot!(
1053 metric.cyclomatic,
1054 @r#"
1055 {
1056 "sum": 5,
1057 "value": 1,
1058 "average": 5.0,
1059 "min": 1,
1060 "max": 4,
1061 "modified": {
1062 "sum": 4,
1063 "value": 1,
1064 "average": 4.0,
1065 "min": 1,
1066 "max": 3
1067 }
1068 }
1069 "#
1070 );
1071 },
1072 );
1073 }
1074
1075 #[test]
1076 fn python_1_level_nesting() {
1077 check_metrics::<PythonParser>(
1078 "def f(a, b): # +2 (+1 unit space)
1079 if a: # +1
1080 for i in range(b): # +1
1081 return 1",
1082 "foo.py",
1083 |metric| {
1084 // nspace = 2 (func and unit)
1085 insta::assert_json_snapshot!(
1086 metric.cyclomatic,
1087 @r#"
1088 {
1089 "sum": 4,
1090 "value": 1,
1091 "average": 4.0,
1092 "min": 1,
1093 "max": 3,
1094 "modified": {
1095 "sum": 4,
1096 "value": 1,
1097 "average": 4.0,
1098 "min": 1,
1099 "max": 3
1100 }
1101 }
1102 "#
1103 );
1104 },
1105 );
1106 }
1107
1108 #[test]
1109 fn rust_1_level_nesting() {
1110 check_metrics::<RustParser>(
1111 "fn f() { // +2 (+1 unit space)
1112 if true { // +1
1113 match true {
1114 true => println!(\"test\"), // +1
1115 false => println!(\"test\"), // +1
1116 }
1117 }
1118 }",
1119 "foo.rs",
1120 |metric| {
1121 // nspace = 2 (func and unit)
1122 insta::assert_json_snapshot!(
1123 metric.cyclomatic,
1124 @r#"
1125 {
1126 "sum": 5,
1127 "value": 1,
1128 "average": 5.0,
1129 "min": 1,
1130 "max": 4,
1131 "modified": {
1132 "sum": 4,
1133 "value": 1,
1134 "average": 4.0,
1135 "min": 1,
1136 "max": 3
1137 }
1138 }
1139 "#
1140 );
1141 },
1142 );
1143 }
1144
1145 /// Modified CCN: a match with N arms counts as 1 decision, not N.
1146 /// Bare `_ =>` wildcard arm does not count toward standard CCN (same
1147 /// as C-family `default:`).
1148 #[test]
1149 fn rust_match_modified() {
1150 check_metrics::<RustParser>(
1151 "fn f(x: u8) -> &'static str { // standard: +1 (unit) +1 (fn) +2 (arms 1,2) = 4; modified: +1 (unit) +1 (fn) +1 (MatchExpr) = 3
1152 match x {
1153 1 => \"one\",
1154 2 => \"two\",
1155 _ => \"other\",
1156 }
1157 }",
1158 "foo.rs",
1159 |metric| {
1160 insta::assert_json_snapshot!(
1161 metric.cyclomatic,
1162 @r#"
1163 {
1164 "sum": 4,
1165 "value": 1,
1166 "average": 4.0,
1167 "min": 1,
1168 "max": 3,
1169 "modified": {
1170 "sum": 3,
1171 "value": 1,
1172 "average": 3.0,
1173 "min": 1,
1174 "max": 2
1175 }
1176 }
1177 "#
1178 );
1179 },
1180 );
1181 }
1182
1183 // The `?` operator (TryExpression) is the configurable arm (#409).
1184 // Fixture has exactly N=3 `?` operators in a single function. With
1185 // counting on (the default) each adds +1 to both standard and
1186 // modified; with counting off they add nothing. The two runs must
1187 // therefore differ by exactly N on both sub-metrics.
1188 const RUST_TRY_FIXTURE: &str = "fn f(s: &str) -> Result<i64, std::num::ParseIntError> {
1189 let a: i64 = s.parse()?;
1190 let b: i64 = s.parse()?;
1191 let c: i64 = s.parse()?;
1192 Ok(a + b + c)
1193 }";
1194 const RUST_TRY_COUNT: u64 = 3;
1195
1196 fn rust_cyclomatic_with_try(count_try: bool) -> super::Stats {
1197 let func_space = crate::analyze(
1198 crate::Source::new(crate::LANG::Rust, RUST_TRY_FIXTURE.as_bytes())
1199 .with_name(Some("try.rs".to_owned())),
1200 crate::MetricsOptions::default().with_count_cyclomatic_try(count_try),
1201 )
1202 .expect("analyze must succeed on a well-formed Rust fixture");
1203 func_space.metrics.cyclomatic.clone()
1204 }
1205
1206 #[test]
1207 fn rust_try_toggle_differs_by_exactly_n() {
1208 let with = rust_cyclomatic_with_try(true);
1209 let without = rust_cyclomatic_with_try(false);
1210
1211 // Headline acceptance (#409): the toggle's whole effect is the N
1212 // `?` operators, on both standard and modified cyclomatic.
1213 assert_eq!(
1214 with.cyclomatic_sum() - without.cyclomatic_sum(),
1215 RUST_TRY_COUNT,
1216 "standard cyclomatic must drop by exactly N when `?` is not counted"
1217 );
1218 assert_eq!(
1219 with.cyclomatic_modified_sum() - without.cyclomatic_modified_sum(),
1220 RUST_TRY_COUNT,
1221 "modified cyclomatic must drop by exactly N when `?` is not counted"
1222 );
1223 // Guard against a no-op toggle: the two runs must actually differ.
1224 assert_ne!(with.cyclomatic_sum(), without.cyclomatic_sum());
1225 }
1226
1227 #[test]
1228 fn rust_try_default_counts() {
1229 // The default (no options) must keep counting `?`, preserving
1230 // every published metric value (#409). Equivalent to the
1231 // `count_try == true` run above.
1232 let default_path = {
1233 let func_space = crate::analyze(
1234 crate::Source::new(crate::LANG::Rust, RUST_TRY_FIXTURE.as_bytes())
1235 .with_name(Some("try.rs".to_owned())),
1236 crate::MetricsOptions::default(),
1237 )
1238 .expect("analyze must succeed on a well-formed Rust fixture");
1239 func_space.metrics.cyclomatic.clone()
1240 };
1241 let explicit_on = rust_cyclomatic_with_try(true);
1242 assert_eq!(default_path.cyclomatic_sum(), explicit_on.cyclomatic_sum());
1243 assert_eq!(
1244 default_path.cyclomatic_modified_sum(),
1245 explicit_on.cyclomatic_modified_sum()
1246 );
1247 // unit(1) + fn(entry 1 + 3*`?` = 4) = 5 standard; modified same
1248 // shape (no match container here): 1 + 4 = 5.
1249 assert_eq!(default_path.cyclomatic_sum(), 5);
1250 assert_eq!(default_path.cyclomatic_modified_sum(), 5);
1251 }
1252
1253 /// `Cyclomatic::compute` for Rust is the trait's plain entry point,
1254 /// and the metric walk never reaches it: `compute_per_node` calls
1255 /// `compute_with_options` directly so it can pass the
1256 /// `count_cyclomatic_try` option through. The plain form is still
1257 /// the documented default for any caller that goes through the
1258 /// trait, and #409 fixes that default as "`?` counts" — so it needs
1259 /// a test of its own rather than inheriting the option tests above,
1260 /// which exercise the other entry point.
1261 ///
1262 /// Asserting only that the two forms agree would be satisfied by a
1263 /// `compute` that delegated with `false` *and* a
1264 /// `compute_with_options` that ignored the flag, so the count is
1265 /// pinned against the opted-out run as well.
1266 #[test]
1267 fn rust_compute_delegates_with_try_counting_on() {
1268 use crate::traits::ParserTrait;
1269
1270 let parser = crate::RustParser::new(
1271 RUST_TRY_FIXTURE.as_bytes().to_vec(),
1272 std::path::Path::new("try.rs"),
1273 None,
1274 );
1275 let code = parser.code();
1276
1277 let mut plain = Stats::default();
1278 let mut opted_in = Stats::default();
1279 let mut opted_out = Stats::default();
1280 for node in parser.root().preorder() {
1281 RustCode::compute(&node, code, Ancestors::unknown(), &mut plain);
1282 RustCode::compute_with_options(&node, code, Ancestors::unknown(), &mut opted_in, true);
1283 RustCode::compute_with_options(
1284 &node,
1285 code,
1286 Ancestors::unknown(),
1287 &mut opted_out,
1288 false,
1289 );
1290 }
1291
1292 assert_eq!(
1293 (plain.cyclomatic(), plain.cyclomatic_modified()),
1294 (opted_in.cyclomatic(), opted_in.cyclomatic_modified()),
1295 "`compute` must be `compute_with_options(.., true)`"
1296 );
1297 assert_eq!(
1298 plain.cyclomatic() - opted_out.cyclomatic(),
1299 RUST_TRY_COUNT,
1300 "the fixture's {RUST_TRY_COUNT} `?` operators are what the default counts"
1301 );
1302 }
1303
1304 #[test]
1305 fn c_switch() {
1306 check_metrics::<CParser>(
1307 "void f() { // +2 (+1 unit space)
1308 switch (1) {
1309 case 1: // +1
1310 printf(\"one\");
1311 break;
1312 case 2: // +1
1313 printf(\"two\");
1314 break;
1315 case 3: // +1
1316 printf(\"three\");
1317 break;
1318 default:
1319 printf(\"all\");
1320 break;
1321 }
1322 }",
1323 "foo.c",
1324 |metric| {
1325 // nspace = 2 (func and unit)
1326 insta::assert_json_snapshot!(
1327 metric.cyclomatic,
1328 @r#"
1329 {
1330 "sum": 5,
1331 "value": 1,
1332 "average": 5.0,
1333 "min": 1,
1334 "max": 4,
1335 "modified": {
1336 "sum": 3,
1337 "value": 1,
1338 "average": 3.0,
1339 "min": 1,
1340 "max": 2
1341 }
1342 }
1343 "#
1344 );
1345 },
1346 );
1347 }
1348
1349 /// Modified CCN: 3 case arms in one switch collapse to 1 decision.
1350 #[test]
1351 fn c_switch_modified() {
1352 check_metrics::<CParser>(
1353 "void f() {
1354 switch (x) {
1355 case 1: break;
1356 case 2: break;
1357 case 3: break;
1358 default: break;
1359 }
1360 }",
1361 "foo.c",
1362 |metric| {
1363 // standard: unit(1) + fn(1) + 3 cases = 5
1364 // modified: unit(1) + fn(1) + switch(1) = 3
1365 insta::assert_json_snapshot!(
1366 metric.cyclomatic,
1367 @r#"
1368 {
1369 "sum": 5,
1370 "value": 1,
1371 "average": 5.0,
1372 "min": 1,
1373 "max": 4,
1374 "modified": {
1375 "sum": 3,
1376 "value": 1,
1377 "average": 3.0,
1378 "min": 1,
1379 "max": 2
1380 }
1381 }
1382 "#
1383 );
1384 },
1385 );
1386 }
1387
1388 #[test]
1389 fn c_real_function() {
1390 check_metrics::<CParser>(
1391 "int sumOfPrimes(int max) { // +2 (+1 unit space)
1392 int total = 0;
1393 OUT: for (int i = 1; i <= max; ++i) { // +1
1394 for (int j = 2; j < i; ++j) { // +1
1395 if (i % j == 0) { // +1
1396 continue OUT;
1397 }
1398 }
1399 total += i;
1400 }
1401 return total;
1402 }",
1403 "foo.c",
1404 |metric| {
1405 // nspace = 2 (func and unit)
1406 insta::assert_json_snapshot!(
1407 metric.cyclomatic,
1408 @r#"
1409 {
1410 "sum": 5,
1411 "value": 1,
1412 "average": 5.0,
1413 "min": 1,
1414 "max": 4,
1415 "modified": {
1416 "sum": 5,
1417 "value": 1,
1418 "average": 5.0,
1419 "min": 1,
1420 "max": 4
1421 }
1422 }
1423 "#
1424 );
1425 },
1426 );
1427 }
1428
1429 #[test]
1430 fn c_unit_before() {
1431 check_metrics::<CParser>(
1432 "
1433 int a=42;
1434 if(a==42) //+2(+1 unit space)
1435 {
1436
1437 }
1438 if(a==34) //+1
1439 {
1440
1441 }
1442 int sumOfPrimes(int max) { // +1
1443 int total = 0;
1444 OUT: for (int i = 1; i <= max; ++i) { // +1
1445 for (int j = 2; j < i; ++j) { // +1
1446 if (i % j == 0) { // +1
1447 continue OUT;
1448 }
1449 }
1450 total += i;
1451 }
1452 return total;
1453 }",
1454 "foo.c",
1455 |metric| {
1456 // nspace = 2 (func and unit)
1457 insta::assert_json_snapshot!(
1458 metric.cyclomatic,
1459 @r#"
1460 {
1461 "sum": 7,
1462 "value": 3,
1463 "average": 7.0,
1464 "min": 3,
1465 "max": 4,
1466 "modified": {
1467 "sum": 7,
1468 "value": 3,
1469 "average": 7.0,
1470 "min": 3,
1471 "max": 4
1472 }
1473 }
1474 "#
1475 );
1476 },
1477 );
1478 }
1479
1480 /// Test to handle the case of min and max when merge happen before the final value of one module are set.
1481 /// In this case the min value should be 3 because the unit space has 2 branches and a complexity of 3
1482 /// while the function sumOfPrimes has a complexity of 4.
1483 #[test]
1484 fn c_unit_after() {
1485 check_metrics::<CParser>(
1486 "
1487 int sumOfPrimes(int max) { // +1
1488 int total = 0;
1489 OUT: for (int i = 1; i <= max; ++i) { // +1
1490 for (int j = 2; j < i; ++j) { // +1
1491 if (i % j == 0) { // +1
1492 continue OUT;
1493 }
1494 }
1495 total += i;
1496 }
1497 return total;
1498 }
1499
1500 int a=42;
1501 if(a==42) //+2(+1 unit space)
1502 {
1503
1504 }
1505 if(a==34) //+1
1506 {
1507
1508 }",
1509 "foo.c",
1510 |metric| {
1511 // nspace = 2 (func and unit)
1512 insta::assert_json_snapshot!(
1513 metric.cyclomatic,
1514 @r#"
1515 {
1516 "sum": 7,
1517 "value": 3,
1518 "average": 7.0,
1519 "min": 3,
1520 "max": 4,
1521 "modified": {
1522 "sum": 7,
1523 "value": 3,
1524 "average": 7.0,
1525 "min": 3,
1526 "max": 4
1527 }
1528 }
1529 "#
1530 );
1531 },
1532 );
1533 }
1534
1535 #[test]
1536 fn java_simple_class() {
1537 check_metrics::<JavaParser>(
1538 "
1539 public class Example { // +2 (+1 unit space)
1540 int a = 10;
1541 boolean b = (a > 5) ? true : false; // +1
1542 boolean c = b && true; // +1
1543
1544 public void m1() { // +1
1545 if (a % 2 == 0) { // +1
1546 b = b || c; // +1
1547 }
1548 }
1549 public void m2() { // +1
1550 while (a > 3) { // +1
1551 m1();
1552 a--;
1553 }
1554 }
1555 }",
1556 "foo.java",
1557 |metric| {
1558 // nspace = 4 (unit, class and 2 methods)
1559 insta::assert_json_snapshot!(
1560 metric.cyclomatic,
1561 @r#"
1562 {
1563 "sum": 9,
1564 "value": 1,
1565 "average": 4.5,
1566 "min": 1,
1567 "max": 3,
1568 "modified": {
1569 "sum": 9,
1570 "value": 1,
1571 "average": 4.5,
1572 "min": 1,
1573 "max": 3
1574 }
1575 }
1576 "#
1577 );
1578 },
1579 );
1580 }
1581
1582 #[test]
1583 fn java_real_class() {
1584 check_metrics::<JavaParser>(
1585 "
1586 public class Matrix { // +2 (+1 unit space)
1587 private int[][] m = new int[5][5];
1588
1589 public void init() { // +1
1590 for (int i = 0; i < m.length; i++) { // +1
1591 for (int j = 0; j < m[i].length; j++) { // +1
1592 m[i][j] = i * j;
1593 }
1594 }
1595 }
1596 public int compute(int i, int j) { // +1
1597 try {
1598 return m[i][j] / m[j][i];
1599 } catch (ArithmeticException e) { // +1
1600 return -1;
1601 } catch (ArrayIndexOutOfBoundsException e) { // +1
1602 return -2;
1603 }
1604 }
1605 public void print(int result) { // +1
1606 switch (result) {
1607 case -1: // +1
1608 System.out.println(\"Division by zero\");
1609 break;
1610 case -2: // +1
1611 System.out.println(\"Wrong index number\");
1612 break;
1613 default:
1614 System.out.println(\"The result is \" + result);
1615 }
1616 }
1617 }",
1618 "foo.java",
1619 |metric| {
1620 // nspace = 5 (unit, class and 3 methods)
1621 insta::assert_json_snapshot!(
1622 metric.cyclomatic,
1623 @r#"
1624 {
1625 "sum": 11,
1626 "value": 1,
1627 "average": 3.6666666666666665,
1628 "min": 1,
1629 "max": 3,
1630 "modified": {
1631 "sum": 10,
1632 "value": 1,
1633 "average": 3.3333333333333335,
1634 "min": 1,
1635 "max": 3
1636 }
1637 }
1638 "#
1639 );
1640 },
1641 );
1642 }
1643
1644 /// Modified CCN: Java switch with 2 cases counts as 1 (not 2).
1645 #[test]
1646 fn java_switch_modified() {
1647 check_metrics::<JavaParser>(
1648 "public class A {
1649 public void print(int result) {
1650 switch (result) {
1651 case -1:
1652 System.out.println(\"minus one\");
1653 break;
1654 case -2:
1655 System.out.println(\"minus two\");
1656 break;
1657 default:
1658 System.out.println(\"other\");
1659 }
1660 }
1661 }",
1662 "foo.java",
1663 |metric| {
1664 // standard: unit(1) + class(1) + fn(1) + 2 cases = 5
1665 // modified: unit(1) + class(1) + fn(1) + switch(1) = 4
1666 insta::assert_json_snapshot!(
1667 metric.cyclomatic,
1668 @r#"
1669 {
1670 "sum": 5,
1671 "value": 1,
1672 "average": 5.0,
1673 "min": 1,
1674 "max": 3,
1675 "modified": {
1676 "sum": 4,
1677 "value": 1,
1678 "average": 4.0,
1679 "min": 1,
1680 "max": 2
1681 }
1682 }
1683 "#
1684 );
1685 },
1686 );
1687 }
1688
1689 #[test]
1690 fn csharp_simple_class() {
1691 check_metrics::<CsharpParser>(
1692 "public class Example {
1693 int a = 10;
1694 bool b = (a > 5) ? true : false;
1695 bool c = b && true;
1696
1697 public void M1() {
1698 if (a % 2 == 0) {
1699 b = b || c;
1700 }
1701 }
1702 public void M2() {
1703 while (a > 3) {
1704 M1();
1705 a--;
1706 }
1707 }
1708 }",
1709 "foo.cs",
1710 |metric| {
1711 insta::assert_json_snapshot!(
1712 metric.cyclomatic,
1713 @r#"
1714 {
1715 "sum": 9,
1716 "value": 1,
1717 "average": 4.5,
1718 "min": 1,
1719 "max": 3,
1720 "modified": {
1721 "sum": 9,
1722 "value": 1,
1723 "average": 4.5,
1724 "min": 1,
1725 "max": 3
1726 }
1727 }
1728 "#
1729 );
1730 },
1731 );
1732 }
1733
1734 #[test]
1735 fn csharp_real_class() {
1736 check_metrics::<CsharpParser>(
1737 "public class Matrix {
1738 private int[,] m = new int[5, 5];
1739
1740 public void Init() {
1741 for (int i = 0; i < 5; i++) {
1742 for (int j = 0; j < 5; j++) {
1743 m[i, j] = i * j;
1744 }
1745 }
1746 }
1747 public int Compute(int i, int j) {
1748 try {
1749 return m[i, j] / m[j, i];
1750 } catch (System.DivideByZeroException) {
1751 return -1;
1752 } catch (System.IndexOutOfRangeException) {
1753 return -2;
1754 }
1755 }
1756 public void Print(int result) {
1757 switch (result) {
1758 case -1:
1759 System.Console.WriteLine(\"Division by zero\");
1760 break;
1761 case -2:
1762 System.Console.WriteLine(\"Wrong index number\");
1763 break;
1764 default:
1765 System.Console.WriteLine(\"The result is \" + result);
1766 break;
1767 }
1768 }
1769 }",
1770 "foo.cs",
1771 |metric| {
1772 insta::assert_json_snapshot!(
1773 metric.cyclomatic,
1774 @r#"
1775 {
1776 "sum": 11,
1777 "value": 1,
1778 "average": 3.6666666666666665,
1779 "min": 1,
1780 "max": 3,
1781 "modified": {
1782 "sum": 10,
1783 "value": 1,
1784 "average": 3.3333333333333335,
1785 "min": 1,
1786 "max": 3
1787 }
1788 }
1789 "#
1790 );
1791 },
1792 );
1793 }
1794
1795 #[test]
1796 fn csharp_anonymous_method() {
1797 check_metrics::<CsharpParser>(
1798 "public class A {
1799 public void M() {
1800 System.Action f = delegate(int x) {
1801 if (x > 0) {
1802 System.Console.WriteLine(x);
1803 }
1804 };
1805 }
1806 }",
1807 "foo.cs",
1808 |metric| {
1809 insta::assert_json_snapshot!(
1810 metric.cyclomatic,
1811 @r#"
1812 {
1813 "sum": 5,
1814 "value": 1,
1815 "average": 2.5,
1816 "min": 1,
1817 "max": 2,
1818 "modified": {
1819 "sum": 5,
1820 "value": 1,
1821 "average": 2.5,
1822 "min": 1,
1823 "max": 2
1824 }
1825 }
1826 "#
1827 );
1828 },
1829 );
1830 }
1831
1832 #[test]
1833 fn csharp_switch_expression_arms() {
1834 // Each non-default arm of a switch_expression contributes +1.
1835 // The discard arm `_ =>` is excluded (issue #282), mirroring
1836 // Rust's `_ =>` and Java/C#'s `default:` treatment.
1837 check_metrics::<CsharpParser>(
1838 "public class A {
1839 public string Name(int n) =>
1840 n switch {
1841 1 => \"one\",
1842 2 => \"two\",
1843 3 => \"three\",
1844 _ => \"other\"
1845 };
1846 }",
1847 "foo.cs",
1848 |metric| {
1849 // expected: unit(1) + class(1) + fn(base 1 + 3 explicit arms;
1850 // `_ =>` skipped) = sum 6, max 4. modified =
1851 // unit(1) + class(1) + fn(base 1 + switch expr 1) = 4.
1852 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 6);
1853 assert_eq!(metric.cyclomatic.cyclomatic_max(), 4);
1854 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
1855 insta::assert_json_snapshot!(
1856 metric.cyclomatic,
1857 @r#"
1858 {
1859 "sum": 6,
1860 "value": 1,
1861 "average": 6.0,
1862 "min": 1,
1863 "max": 4,
1864 "modified": {
1865 "sum": 4,
1866 "value": 1,
1867 "average": 4.0,
1868 "min": 1,
1869 "max": 2
1870 }
1871 }
1872 "#
1873 );
1874 },
1875 );
1876 }
1877
1878 /// Regression #282: the bare discard arm `_ =>` in a C# switch
1879 /// expression must NOT contribute to standard CCN, mirroring the
1880 /// C-family `default:` rule.
1881 #[test]
1882 fn csharp_switch_expression_discard_arm_not_counted() {
1883 check_metrics::<CsharpParser>(
1884 "public class A {
1885 public string Name(int n) =>
1886 n switch {
1887 1 => \"one\",
1888 _ => \"other\"
1889 };
1890 }",
1891 "foo.cs",
1892 |metric| {
1893 // expected: unit(1) + class(1) + fn(base 1 + 1 explicit;
1894 // `_ =>` skipped) = 4, max 2.
1895 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
1896 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
1897 },
1898 );
1899 }
1900
1901 /// Regression #282: `var _` is also a discard pattern and must be
1902 /// excluded from standard CCN.
1903 #[test]
1904 fn csharp_switch_expression_var_underscore_not_counted() {
1905 check_metrics::<CsharpParser>(
1906 "public class A {
1907 public string Name(int n) =>
1908 n switch {
1909 1 => \"one\",
1910 var _ => \"other\"
1911 };
1912 }",
1913 "foo.cs",
1914 |metric| {
1915 // expected: unit(1) + class(1) + fn(base 1 + 1 explicit;
1916 // `var _ =>` skipped) = 4, max 2.
1917 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
1918 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
1919 },
1920 );
1921 }
1922
1923 /// Regression #282: a guarded discard arm `_ when g => …` is NOT a
1924 /// bare wildcard — the `when` guard adds a non-trivial decision —
1925 /// so the arm still contributes one standard decision, mirroring
1926 /// Rust's `_ if g` rule.
1927 #[test]
1928 fn csharp_switch_expression_guarded_discard_still_counts() {
1929 check_metrics::<CsharpParser>(
1930 "public class A {
1931 public string Name(int n) =>
1932 n switch {
1933 1 => \"one\",
1934 _ when n > 10 => \"big\",
1935 _ => \"other\"
1936 };
1937 }",
1938 "foo.cs",
1939 |metric| {
1940 // expected: unit(1) + class(1) + fn(base 1 + 1 explicit +
1941 // 1 guarded discard; bare `_ =>` skipped) = 5,
1942 // max 3.
1943 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
1944 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
1945 },
1946 );
1947 }
1948
1949 /// Regression #303 / #282: a typed-discard arm `int _ =>` is NOT
1950 /// a bare discard — the type test (`predefined_type`) is a
1951 /// non-trivial decision — so the arm still contributes one
1952 /// standard decision. Locks in the
1953 /// `DeclarationPattern → _ => return NotDiscard` catch-all in
1954 /// `csharp_switch_expression_arm_is_bare_discard`.
1955 #[test]
1956 fn csharp_switch_expression_typed_discard_still_counts() {
1957 check_metrics::<CsharpParser>(
1958 "public class A {
1959 public string Name(object n) =>
1960 n switch {
1961 1 => \"one\",
1962 int _ => \"int\",
1963 _ => \"other\"
1964 };
1965 }",
1966 "foo.cs",
1967 |metric| {
1968 // expected: unit(1) + class(1) + fn(base 1 + 1 explicit `1` +
1969 // 1 typed-discard `int _`; bare `_ =>` skipped) = 5,
1970 // max 3.
1971 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
1972 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
1973 },
1974 );
1975 }
1976
1977 /// Regression #303 / #282: a guarded `var _ when g =>` is NOT a
1978 /// bare discard — the `when` guard adds a non-trivial decision —
1979 /// so the arm still contributes one standard decision. Exercises
1980 /// the `DeclarationPattern` arm of `classify_pattern` combined
1981 /// with the post-pattern `WhenClause` sweep.
1982 #[test]
1983 fn csharp_switch_expression_guarded_var_underscore_still_counts() {
1984 check_metrics::<CsharpParser>(
1985 "public class A {
1986 public string Name(int n) =>
1987 n switch {
1988 1 => \"one\",
1989 var _ when n > 10 => \"big\",
1990 _ => \"other\"
1991 };
1992 }",
1993 "foo.cs",
1994 |metric| {
1995 // expected: unit(1) + class(1) + fn(base 1 + 1 explicit `1` +
1996 // 1 guarded `var _`; bare `_ =>` skipped) = 5,
1997 // max 3.
1998 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
1999 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
2000 },
2001 );
2002 }
2003
2004 /// Modified CCN: C# switch statement with 2 cases counts as 1.
2005 #[test]
2006 fn csharp_switch_modified() {
2007 check_metrics::<CsharpParser>(
2008 "public class A {
2009 public string Describe(int n) {
2010 switch (n) {
2011 case 1:
2012 return \"one\";
2013 case 2:
2014 return \"two\";
2015 default:
2016 return \"other\";
2017 }
2018 }
2019 }",
2020 "foo.cs",
2021 |metric| {
2022 // standard: unit(1) + class(1) + fn(1) + 2 cases = 5
2023 // modified: unit(1) + class(1) + fn(1) + switch(1) = 4
2024 insta::assert_json_snapshot!(
2025 metric.cyclomatic,
2026 @r#"
2027 {
2028 "sum": 5,
2029 "value": 1,
2030 "average": 5.0,
2031 "min": 1,
2032 "max": 3,
2033 "modified": {
2034 "sum": 4,
2035 "value": 1,
2036 "average": 4.0,
2037 "min": 1,
2038 "max": 2
2039 }
2040 }
2041 "#
2042 );
2043 },
2044 );
2045 }
2046
2047 #[test]
2048 fn csharp_null_coalescing_and_conditional_access() {
2049 // Each `??` and `?.` is +1 cyclomatic.
2050 check_metrics::<CsharpParser>(
2051 "public class A {
2052 public int? Get(string s, A b) {
2053 return s?.Length ?? b?.Get(null, null) ?? 0;
2054 }
2055 }",
2056 "foo.cs",
2057 |metric| {
2058 insta::assert_json_snapshot!(
2059 metric.cyclomatic,
2060 @r#"
2061 {
2062 "sum": 7,
2063 "value": 1,
2064 "average": 7.0,
2065 "min": 1,
2066 "max": 5,
2067 "modified": {
2068 "sum": 7,
2069 "value": 1,
2070 "average": 7.0,
2071 "min": 1,
2072 "max": 5
2073 }
2074 }
2075 "#
2076 );
2077 },
2078 );
2079 }
2080
2081 #[test]
2082 fn javascript_simple_function() {
2083 check_metrics::<JavascriptParser>(
2084 "function f(a, b) { // +2 (+1 unit space)
2085 if (a) { // +1
2086 return a;
2087 } else if (b) { // +1
2088 return b;
2089 }
2090 return 0;
2091 }",
2092 "foo.js",
2093 |metric| {
2094 insta::assert_json_snapshot!(
2095 metric.cyclomatic,
2096 @r#"
2097 {
2098 "sum": 4,
2099 "value": 1,
2100 "average": 4.0,
2101 "min": 1,
2102 "max": 3,
2103 "modified": {
2104 "sum": 4,
2105 "value": 1,
2106 "average": 4.0,
2107 "min": 1,
2108 "max": 3
2109 }
2110 }
2111 "#
2112 );
2113 },
2114 );
2115 }
2116
2117 #[test]
2118 fn javascript_switch() {
2119 check_metrics::<JavascriptParser>(
2120 "function f() { // +2 (+1 unit space)
2121 switch (x) {
2122 case 1: // +1
2123 console.log(\"one\");
2124 break;
2125 case 2: // +1
2126 console.log(\"two\");
2127 break;
2128 case 3: // +1
2129 console.log(\"three\");
2130 break;
2131 default:
2132 console.log(\"other\");
2133 break;
2134 }
2135 }",
2136 "foo.js",
2137 |metric| {
2138 insta::assert_json_snapshot!(
2139 metric.cyclomatic,
2140 @r#"
2141 {
2142 "sum": 5,
2143 "value": 1,
2144 "average": 5.0,
2145 "min": 1,
2146 "max": 4,
2147 "modified": {
2148 "sum": 3,
2149 "value": 1,
2150 "average": 3.0,
2151 "min": 1,
2152 "max": 2
2153 }
2154 }
2155 "#
2156 );
2157 },
2158 );
2159 }
2160
2161 /// Modified CCN: JS switch with 3 cases collapses to 1.
2162 #[test]
2163 fn javascript_switch_modified() {
2164 check_metrics::<JavascriptParser>(
2165 "function f(x) {
2166 switch (x) {
2167 case 1: return 'one';
2168 case 2: return 'two';
2169 case 3: return 'three';
2170 }
2171 }",
2172 "foo.js",
2173 |metric| {
2174 // standard: unit(1) + fn(1) + 3 cases = 5
2175 // modified: unit(1) + fn(1) + switch(1) = 3
2176 insta::assert_json_snapshot!(
2177 metric.cyclomatic,
2178 @r#"
2179 {
2180 "sum": 5,
2181 "value": 1,
2182 "average": 5.0,
2183 "min": 1,
2184 "max": 4,
2185 "modified": {
2186 "sum": 3,
2187 "value": 1,
2188 "average": 3.0,
2189 "min": 1,
2190 "max": 2
2191 }
2192 }
2193 "#
2194 );
2195 },
2196 );
2197 }
2198
2199 #[test]
2200 fn go_simple_function() {
2201 check_metrics::<GoParser>(
2202 "package main
2203 func f() {}",
2204 "foo.go",
2205 |metric| {
2206 // nspace = 2 (file unit + func), each base 1.
2207 insta::assert_json_snapshot!(
2208 metric.cyclomatic,
2209 @r#"
2210 {
2211 "sum": 2,
2212 "value": 1,
2213 "average": 2.0,
2214 "min": 1,
2215 "max": 1,
2216 "modified": {
2217 "sum": 2,
2218 "value": 1,
2219 "average": 2.0,
2220 "min": 1,
2221 "max": 1
2222 }
2223 }
2224 "#
2225 );
2226 },
2227 );
2228 }
2229
2230 #[test]
2231 fn go_if_else() {
2232 check_metrics::<GoParser>(
2233 "package main
2234 func f(x bool) { // +2 (+1 unit)
2235 if x { // +1
2236 } else {
2237 }
2238 }",
2239 "foo.go",
2240 |metric| {
2241 // `else` clause attaches to the same if_statement node and is
2242 // not counted again.
2243 insta::assert_json_snapshot!(
2244 metric.cyclomatic,
2245 @r#"
2246 {
2247 "sum": 3,
2248 "value": 1,
2249 "average": 3.0,
2250 "min": 1,
2251 "max": 2,
2252 "modified": {
2253 "sum": 3,
2254 "value": 1,
2255 "average": 3.0,
2256 "min": 1,
2257 "max": 2
2258 }
2259 }
2260 "#
2261 );
2262 },
2263 );
2264 }
2265
2266 #[test]
2267 fn go_else_if_chain() {
2268 check_metrics::<GoParser>(
2269 "package main
2270 func f(x int) { // +2 (+1 unit)
2271 if x > 0 { // +1
2272 } else if x < 0 { // +1 (nested if_statement)
2273 } else if x == 0 { // +1 (nested if_statement)
2274 } else {
2275 }
2276 }",
2277 "foo.go",
2278 |metric| {
2279 // tree-sitter-go represents `else if` as a nested
2280 // if_statement under the parent's `else` clause; each nested
2281 // if contributes +1.
2282 insta::assert_json_snapshot!(
2283 metric.cyclomatic,
2284 @r#"
2285 {
2286 "sum": 5,
2287 "value": 1,
2288 "average": 5.0,
2289 "min": 1,
2290 "max": 4,
2291 "modified": {
2292 "sum": 5,
2293 "value": 1,
2294 "average": 5.0,
2295 "min": 1,
2296 "max": 4
2297 }
2298 }
2299 "#
2300 );
2301 },
2302 );
2303 }
2304
2305 #[test]
2306 fn go_for_loop() {
2307 check_metrics::<GoParser>(
2308 "package main
2309 func f() { // +2 (+1 unit)
2310 for i := 0; i < 10; i++ { // +1
2311 }
2312 }",
2313 "foo.go",
2314 |metric| {
2315 insta::assert_json_snapshot!(
2316 metric.cyclomatic,
2317 @r#"
2318 {
2319 "sum": 3,
2320 "value": 1,
2321 "average": 3.0,
2322 "min": 1,
2323 "max": 2,
2324 "modified": {
2325 "sum": 3,
2326 "value": 1,
2327 "average": 3.0,
2328 "min": 1,
2329 "max": 2
2330 }
2331 }
2332 "#
2333 );
2334 },
2335 );
2336 }
2337
2338 #[test]
2339 fn go_for_range() {
2340 check_metrics::<GoParser>(
2341 "package main
2342 func f(xs []int) { // +2 (+1 unit)
2343 for _, v := range xs { // +1
2344 _ = v
2345 }
2346 }",
2347 "foo.go",
2348 |metric| {
2349 // range_clause is a child of for_statement; only the
2350 // for_statement contributes.
2351 insta::assert_json_snapshot!(
2352 metric.cyclomatic,
2353 @r#"
2354 {
2355 "sum": 3,
2356 "value": 1,
2357 "average": 3.0,
2358 "min": 1,
2359 "max": 2,
2360 "modified": {
2361 "sum": 3,
2362 "value": 1,
2363 "average": 3.0,
2364 "min": 1,
2365 "max": 2
2366 }
2367 }
2368 "#
2369 );
2370 },
2371 );
2372 }
2373
2374 #[test]
2375 fn go_switch() {
2376 check_metrics::<GoParser>(
2377 "package main
2378 func f(x int) { // +2 (+1 unit)
2379 switch x {
2380 case 1: // +1
2381 case 2: // +1
2382 default: // not counted
2383 }
2384 }",
2385 "foo.go",
2386 |metric| {
2387 insta::assert_json_snapshot!(
2388 metric.cyclomatic,
2389 @r#"
2390 {
2391 "sum": 4,
2392 "value": 1,
2393 "average": 4.0,
2394 "min": 1,
2395 "max": 3,
2396 "modified": {
2397 "sum": 3,
2398 "value": 1,
2399 "average": 3.0,
2400 "min": 1,
2401 "max": 2
2402 }
2403 }
2404 "#
2405 );
2406 },
2407 );
2408 }
2409
2410 /// Modified CCN: Go switch with 3 cases collapses to 1.
2411 #[test]
2412 fn go_switch_modified() {
2413 check_metrics::<GoParser>(
2414 "package main
2415 func f(x int) {
2416 switch x {
2417 case 1:
2418 println(\"one\")
2419 case 2:
2420 println(\"two\")
2421 case 3:
2422 println(\"three\")
2423 }
2424 }",
2425 "foo.go",
2426 |metric| {
2427 // standard: unit(1) + fn(1) + 3 cases = 5
2428 // modified: unit(1) + fn(1) + switch(1) = 3
2429 insta::assert_json_snapshot!(
2430 metric.cyclomatic,
2431 @r#"
2432 {
2433 "sum": 5,
2434 "value": 1,
2435 "average": 5.0,
2436 "min": 1,
2437 "max": 4,
2438 "modified": {
2439 "sum": 3,
2440 "value": 1,
2441 "average": 3.0,
2442 "min": 1,
2443 "max": 2
2444 }
2445 }
2446 "#
2447 );
2448 },
2449 );
2450 }
2451
2452 #[test]
2453 fn go_type_switch() {
2454 check_metrics::<GoParser>(
2455 "package main
2456 func f(x interface{}) { // +2 (+1 unit)
2457 switch x.(type) {
2458 case int: // +1
2459 case string: // +1
2460 }
2461 }",
2462 "foo.go",
2463 |metric| {
2464 insta::assert_json_snapshot!(
2465 metric.cyclomatic,
2466 @r#"
2467 {
2468 "sum": 4,
2469 "value": 1,
2470 "average": 4.0,
2471 "min": 1,
2472 "max": 3,
2473 "modified": {
2474 "sum": 3,
2475 "value": 1,
2476 "average": 3.0,
2477 "min": 1,
2478 "max": 2
2479 }
2480 }
2481 "#
2482 );
2483 },
2484 );
2485 }
2486
2487 #[test]
2488 fn go_select() {
2489 check_metrics::<GoParser>(
2490 "package main
2491 func f(c1, c2 chan int) { // +2 (+1 unit)
2492 select {
2493 case <-c1: // +1
2494 case <-c2: // +1
2495 default: // not counted
2496 }
2497 }",
2498 "foo.go",
2499 |metric| {
2500 insta::assert_json_snapshot!(
2501 metric.cyclomatic,
2502 @r#"
2503 {
2504 "sum": 4,
2505 "value": 1,
2506 "average": 4.0,
2507 "min": 1,
2508 "max": 3,
2509 "modified": {
2510 "sum": 3,
2511 "value": 1,
2512 "average": 3.0,
2513 "min": 1,
2514 "max": 2
2515 }
2516 }
2517 "#
2518 );
2519 },
2520 );
2521 }
2522
2523 #[test]
2524 fn go_logical_operators() {
2525 check_metrics::<GoParser>(
2526 "package main
2527 func f(a, b, c bool) { // +2 (+1 unit)
2528 if a && b || c { // +1 if, +1 &&, +1 ||
2529 }
2530 }",
2531 "foo.go",
2532 |metric| {
2533 insta::assert_json_snapshot!(
2534 metric.cyclomatic,
2535 @r#"
2536 {
2537 "sum": 5,
2538 "value": 1,
2539 "average": 5.0,
2540 "min": 1,
2541 "max": 4,
2542 "modified": {
2543 "sum": 5,
2544 "value": 1,
2545 "average": 5.0,
2546 "min": 1,
2547 "max": 4
2548 }
2549 }
2550 "#
2551 );
2552 },
2553 );
2554 }
2555
2556 #[test]
2557 fn go_defer_and_go_do_not_count() {
2558 check_metrics::<GoParser>(
2559 "package main
2560 func f() { // +2 (+1 unit)
2561 defer cleanup()
2562 go work()
2563 }",
2564 "foo.go",
2565 |metric| {
2566 // defer_statement and go_statement are not branches.
2567 insta::assert_json_snapshot!(
2568 metric.cyclomatic,
2569 @r#"
2570 {
2571 "sum": 2,
2572 "value": 1,
2573 "average": 2.0,
2574 "min": 1,
2575 "max": 1,
2576 "modified": {
2577 "sum": 2,
2578 "value": 1,
2579 "average": 2.0,
2580 "min": 1,
2581 "max": 1
2582 }
2583 }
2584 "#
2585 );
2586 },
2587 );
2588 }
2589
2590 // As reported here:
2591 // https://github.com/sebastianbergmann/php-code-coverage/issues/607
2592 // An anonymous class declaration is not considered when computing the Cyclomatic Complexity metric for Java
2593 // Only the complexity of the anonymous class content is considered for the computation
2594 #[test]
2595 fn java_anonymous_class() {
2596 check_metrics::<JavaParser>(
2597 "
2598 abstract class A { // +2 (+1 unit space)
2599 public abstract boolean m1(int n); // +1
2600 public abstract boolean m2(int n); // +1
2601 }
2602 public class B { // +1
2603 public void test() { // +1
2604 A a = new A() {
2605 public boolean m1(int n) { // +1
2606 if (n % 2 == 0) { // +1
2607 return true;
2608 }
2609 return false;
2610 }
2611 public boolean m2(int n) { // +1
2612 if (n % 5 == 0) { // +1
2613 return true;
2614 }
2615 return false;
2616 }
2617 };
2618 }
2619 }",
2620 "foo.java",
2621 |metric| {
2622 // nspace = 9: unit, the two named classes (A, B), the
2623 // anonymous class (`new A() { ... }`, now its own Class
2624 // space — #463), and 5 methods. The anonymous class adds a
2625 // base +1 to the cyclomatic sum exactly like a named class,
2626 // so the file-level sum is 11 (was 10 before the anonymous
2627 // body opened its own space). Each method's complexity is
2628 // counted once — the +1 is the new class space, not a
2629 // re-count of any method.
2630 insta::assert_json_snapshot!(
2631 metric.cyclomatic,
2632 @r#"
2633 {
2634 "sum": 11,
2635 "value": 1,
2636 "average": 2.2,
2637 "min": 1,
2638 "max": 2,
2639 "modified": {
2640 "sum": 11,
2641 "value": 1,
2642 "average": 2.2,
2643 "min": 1,
2644 "max": 2
2645 }
2646 }
2647 "#
2648 );
2649 },
2650 );
2651 }
2652
2653 /// Java `do { … } while (…)` contributes exactly +1 to both
2654 /// standard and modified CCN. The +1 comes from the `while`
2655 /// keyword token (`Java::While`) inside the do-statement, which
2656 /// the dedicated `JavaCode` impl already counts. Adding
2657 /// `Java::DoStatement` would double-count — see issue #284. This
2658 /// test pins the correct keyword-driven count.
2659 #[test]
2660 fn java_do_statement_counts_in_cyclomatic() {
2661 check_metrics::<JavaParser>(
2662 "class Parity {
2663 static void f() {
2664 int i = 0;
2665 do { // +1 (via inner `while` keyword)
2666 ++i;
2667 } while (i < 10);
2668 }
2669 }",
2670 "foo.java",
2671 |metric| {
2672 // standard: unit(1) + class(1) + method(1) + do(1) = 4
2673 let s = &metric.cyclomatic;
2674 assert_eq!(s.cyclomatic_sum(), 4);
2675 assert_eq!(s.cyclomatic_max(), 2);
2676 assert_eq!(s.cyclomatic_modified_sum(), 4);
2677 insta::assert_json_snapshot!(
2678 metric.cyclomatic,
2679 @r#"
2680 {
2681 "sum": 4,
2682 "value": 1,
2683 "average": 4.0,
2684 "min": 1,
2685 "max": 2,
2686 "modified": {
2687 "sum": 4,
2688 "value": 1,
2689 "average": 4.0,
2690 "min": 1,
2691 "max": 2
2692 }
2693 }
2694 "#
2695 );
2696 },
2697 );
2698 }
2699
2700 /// Java enhanced-for `for (T x : xs)` contributes exactly +1 to
2701 /// both standard and modified CCN — the `for` keyword token
2702 /// (`Java::For`) fires inside the `EnhancedForStatement` node
2703 /// just like inside a classic `ForStatement`. Pinning this
2704 /// prevents reintroducing the double-count from issue #284's
2705 /// incorrect fix proposal.
2706 #[test]
2707 fn java_enhanced_for_statement_counts_in_cyclomatic() {
2708 check_metrics::<JavaParser>(
2709 "class Parity {
2710 static void f(int[] xs) {
2711 for (int x : xs) { // +1 (via `for` keyword)
2712 g(x);
2713 }
2714 }
2715 }",
2716 "foo.java",
2717 |metric| {
2718 // standard: unit(1) + class(1) + method(1) + enhanced-for(1) = 4
2719 let s = &metric.cyclomatic;
2720 assert_eq!(s.cyclomatic_sum(), 4);
2721 assert_eq!(s.cyclomatic_max(), 2);
2722 assert_eq!(s.cyclomatic_modified_sum(), 4);
2723 insta::assert_json_snapshot!(
2724 metric.cyclomatic,
2725 @r#"
2726 {
2727 "sum": 4,
2728 "value": 1,
2729 "average": 4.0,
2730 "min": 1,
2731 "max": 2,
2732 "modified": {
2733 "sum": 4,
2734 "value": 1,
2735 "average": 4.0,
2736 "min": 1,
2737 "max": 2
2738 }
2739 }
2740 "#
2741 );
2742 },
2743 );
2744 }
2745
2746 #[test]
2747 fn groovy_simple_class() {
2748 check_metrics::<GroovyParser>(
2749 "
2750 class Example {
2751 int a = 10
2752 boolean b = (a > 5) ? true : false
2753 boolean c = b && true
2754
2755 void m1() {
2756 if (a % 2 == 0) {
2757 b = b || c
2758 }
2759 }
2760 void m2() {
2761 while (a > 3) {
2762 m1()
2763 a--
2764 }
2765 }
2766 }",
2767 "foo.groovy",
2768 |metric| {
2769 // Same shape as `java_simple_class`. nspace = 4
2770 // (unit, class, 2 methods); branches mirror Java's.
2771 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 9);
2772 },
2773 );
2774 }
2775
2776 #[test]
2777 fn groovy_nested_control_flow() {
2778 check_metrics::<GroovyParser>(
2779 "void f(int x) {
2780 if (x > 0) {
2781 while (x < 100) {
2782 x = x + 1
2783 }
2784 }
2785 }",
2786 "foo.groovy",
2787 |metric| {
2788 // unit(1) + fn(1) + if(1) + while(1) = 4
2789 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
2790 },
2791 );
2792 }
2793
2794 #[test]
2795 fn groovy_switch_with_cases() {
2796 check_metrics::<GroovyParser>(
2797 "void print(int result) {
2798 switch (result) {
2799 case -1:
2800 println 'minus one'
2801 break
2802 case -2:
2803 println 'minus two'
2804 break
2805 default:
2806 println 'other'
2807 }
2808 }",
2809 "foo.groovy",
2810 |metric| {
2811 // standard: unit(1) + fn(1) + 2 cases = 4
2812 // modified: unit(1) + fn(1) + switch(1) = 3
2813 // (default does NOT add a branch — same as Java/lesson #106)
2814 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
2815 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 3);
2816 },
2817 );
2818 }
2819
2820 #[test]
2821 fn groovy_try_catch() {
2822 check_metrics::<GroovyParser>(
2823 "void f() {
2824 try {
2825 risky()
2826 } catch (Exception e) {
2827 handle(e)
2828 }
2829 }",
2830 "foo.groovy",
2831 |metric| {
2832 // unit(1) + fn(1) + catch(1) = 3
2833 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
2834 },
2835 );
2836 }
2837
2838 #[test]
2839 fn groovy_closure_body_short_circuit() {
2840 // Top-level `def pred = { … }` collapses the closure into the
2841 // unit scope (no class wrapper), so the `&&` inside still
2842 // contributes one branch but no extra function space is
2843 // created. Mirrors Java's top-level-lambda behavior.
2844 check_metrics::<GroovyParser>(
2845 "def pred = { x -> x > 0 && x < 100 }",
2846 "foo.groovy",
2847 |metric| {
2848 // unit(1) + && (1) = 2
2849 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 2);
2850 },
2851 );
2852 }
2853
2854 #[test]
2855 fn groovy_assert_adds_branch() {
2856 // Groovy `assert` is a runtime check that branches on its
2857 // condition; mirror Sonar's standard-CCN treatment.
2858 check_metrics::<GroovyParser>(
2859 "void check(int x) {
2860 assert x > 0
2861 }",
2862 "foo.groovy",
2863 |metric| {
2864 // unit(1) + fn(1) + assert(1) = 3
2865 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
2866 },
2867 );
2868 }
2869
2870 /// Groovy `do { … } while (…)` contributes exactly +1 to both
2871 /// standard and modified CCN — the `while` keyword token
2872 /// (`Groovy::While`) inside the do-statement is already counted
2873 /// by the dedicated `GroovyCode` impl. Adding `Groovy::DoStatement`
2874 /// would double-count (issue #284). This test pins the correct
2875 /// keyword-driven count.
2876 #[test]
2877 fn groovy_do_statement_counts_in_cyclomatic() {
2878 check_metrics::<GroovyParser>(
2879 "def f() {
2880 int i = 0
2881 do { // +1 (via inner `while` keyword)
2882 ++i
2883 } while (i < 10)
2884 }",
2885 "foo.groovy",
2886 |metric| {
2887 // standard: unit(1) + fn(1) + do(1) = 3
2888 let s = &metric.cyclomatic;
2889 assert_eq!(s.cyclomatic_sum(), 3);
2890 assert_eq!(s.cyclomatic_max(), 2);
2891 assert_eq!(s.cyclomatic_modified_sum(), 3);
2892 },
2893 );
2894 }
2895
2896 /// Groovy enhanced-for `for (T x : xs)` contributes exactly +1 to
2897 /// both standard and modified CCN — the `for` keyword token
2898 /// (`Groovy::For`) fires inside `EnhancedForStatement` just like
2899 /// inside a classic `ForStatement`. Pinning this prevents
2900 /// reintroducing the double-count from issue #284's incorrect fix
2901 /// proposal.
2902 #[test]
2903 fn groovy_enhanced_for_statement_counts_in_cyclomatic() {
2904 check_metrics::<GroovyParser>(
2905 "def f(int[] xs) {
2906 for (int x : xs) { // +1 (via `for` keyword)
2907 println(x)
2908 }
2909 }",
2910 "foo.groovy",
2911 |metric| {
2912 // standard: unit(1) + fn(1) + enhanced-for(1) = 3
2913 let s = &metric.cyclomatic;
2914 assert_eq!(s.cyclomatic_sum(), 3);
2915 assert_eq!(s.cyclomatic_max(), 2);
2916 assert_eq!(s.cyclomatic_modified_sum(), 3);
2917 },
2918 );
2919 }
2920
2921 #[test]
2922 fn groovy_safe_navigation_cyclomatic() {
2923 // Issue #452: Groovy's safe-navigation `?.` (QMARKDOT) is a
2924 // short-circuit decision point per link, mirroring the
2925 // Kotlin/PHP/JS/C# treatment of `?.` (#281). The chain
2926 // `a?.b?.c` adds +2 to both standard and modified CCN.
2927 check_metrics::<GroovyParser>("def read(a){ return a?.b?.c }", "foo.groovy", |metric| {
2928 // unit(1) + fn(base 1 + ?. 1 + ?. 1) = sum 4, max 3.
2929 let s = &metric.cyclomatic;
2930 assert_eq!(s.cyclomatic_sum(), 4);
2931 assert_eq!(s.cyclomatic_max(), 3);
2932 assert_eq!(s.cyclomatic_modified_sum(), 4);
2933 assert_eq!(s.cyclomatic_modified_max(), 3);
2934 });
2935 }
2936
2937 #[test]
2938 fn groovy_safe_chain_dot_cyclomatic() {
2939 // Issue #452: Groovy's `??.` (QMARKQMARKDOT, the spread-safe
2940 // chain-dot operator) is also a short-circuit decision point,
2941 // counted once per occurrence like `?.`.
2942 check_metrics::<GroovyParser>("def read(a){ return a??.b }", "foo.groovy", |metric| {
2943 // unit(1) + fn(base 1 + ??. 1) = sum 3, max 2.
2944 let s = &metric.cyclomatic;
2945 assert_eq!(s.cyclomatic_sum(), 3);
2946 assert_eq!(s.cyclomatic_max(), 2);
2947 assert_eq!(s.cyclomatic_modified_sum(), 3);
2948 assert_eq!(s.cyclomatic_modified_max(), 2);
2949 });
2950 }
2951
2952 #[test]
2953 fn perl_nested_control_flow() {
2954 check_metrics::<PerlParser>(
2955 "sub f { # +1 (unit) +1 (sub)
2956 for my $i (1..10) { # +1 for_statement_2
2957 if ($i % 2) { # +1 if_statement
2958 print $i;
2959 }
2960 }
2961 }",
2962 "foo.pl",
2963 |metric| {
2964 insta::assert_json_snapshot!(
2965 metric.cyclomatic,
2966 @r#"
2967 {
2968 "sum": 4,
2969 "value": 1,
2970 "average": 4.0,
2971 "min": 1,
2972 "max": 3,
2973 "modified": {
2974 "sum": 4,
2975 "value": 1,
2976 "average": 4.0,
2977 "min": 1,
2978 "max": 3
2979 }
2980 }
2981 "#
2982 );
2983 },
2984 );
2985 }
2986
2987 #[test]
2988 fn perl_postfix_conditionals() {
2989 check_metrics::<PerlParser>(
2990 "sub f { # +1 (unit) +1 (sub)
2991 return 1 if $_[0]; # +1 if_simple_statement
2992 return 0 unless $_[1]; # +1 unless_simple_statement
2993 }",
2994 "foo.pl",
2995 |metric| {
2996 insta::assert_json_snapshot!(
2997 metric.cyclomatic,
2998 @r#"
2999 {
3000 "sum": 4,
3001 "value": 1,
3002 "average": 4.0,
3003 "min": 1,
3004 "max": 3,
3005 "modified": {
3006 "sum": 4,
3007 "value": 1,
3008 "average": 4.0,
3009 "min": 1,
3010 "max": 3
3011 }
3012 }
3013 "#
3014 );
3015 },
3016 );
3017 }
3018
3019 #[test]
3020 fn perl_unless_and_until() {
3021 check_metrics::<PerlParser>(
3022 "sub f { # +1 (unit) +1 (sub)
3023 unless ($x) { # +1 unless_statement
3024 print 'a';
3025 }
3026 until ($n == 0) { # +1 until_statement
3027 $n--;
3028 }
3029 }",
3030 "foo.pl",
3031 |metric| {
3032 insta::assert_json_snapshot!(
3033 metric.cyclomatic,
3034 @r#"
3035 {
3036 "sum": 4,
3037 "value": 1,
3038 "average": 4.0,
3039 "min": 1,
3040 "max": 3,
3041 "modified": {
3042 "sum": 4,
3043 "value": 1,
3044 "average": 4.0,
3045 "min": 1,
3046 "max": 3
3047 }
3048 }
3049 "#
3050 );
3051 },
3052 );
3053 }
3054
3055 #[test]
3056 fn perl_logical_operators_and_ternary() {
3057 check_metrics::<PerlParser>(
3058 "sub f { # +1 (unit) +1 (sub)
3059 my $x = $a && $b; # +1 (&&)
3060 my $y = $c || $d; # +1 (||)
3061 my $z = $e // $f; # +1 (//)
3062 my $t = $g ? 1 : 0; # +1 ternary
3063 }",
3064 "foo.pl",
3065 |metric| {
3066 insta::assert_json_snapshot!(
3067 metric.cyclomatic,
3068 @r#"
3069 {
3070 "sum": 6,
3071 "value": 1,
3072 "average": 6.0,
3073 "min": 1,
3074 "max": 5,
3075 "modified": {
3076 "sum": 6,
3077 "value": 1,
3078 "average": 6.0,
3079 "min": 1,
3080 "max": 5
3081 }
3082 }
3083 "#
3084 );
3085 },
3086 );
3087 }
3088
3089 #[test]
3090 fn perl_word_logical_operators() {
3091 check_metrics::<PerlParser>(
3092 "sub f { # +1 (unit) +1 (sub)
3093 my $x = $a and $b; # +1 (and)
3094 my $y = $c or $d; # +1 (or)
3095 }",
3096 "foo.pl",
3097 |metric| {
3098 insta::assert_json_snapshot!(
3099 metric.cyclomatic,
3100 @r#"
3101 {
3102 "sum": 4,
3103 "value": 1,
3104 "average": 4.0,
3105 "min": 1,
3106 "max": 3,
3107 "modified": {
3108 "sum": 4,
3109 "value": 1,
3110 "average": 4.0,
3111 "min": 1,
3112 "max": 3
3113 }
3114 }
3115 "#
3116 );
3117 },
3118 );
3119 }
3120
3121 #[test]
3122 fn perl_compound_short_circuit_assignment_249() {
3123 // Regression for issue #249: `&&=`, `||=`, `//=` are each one
3124 // short-circuit decision edge — semantically `$x = $x op $y`.
3125 // Perl exposes the operator token inside `binary_expression`,
3126 // so adding the three `*EQ` tokens to the cyclomatic arm picks
3127 // them up alongside the bare `&&` / `||` / `//`.
3128 check_metrics::<PerlParser>(
3129 "sub f { # +1 (unit) +1 (sub)
3130 my ($x, $y, $z) = @_;
3131 $x ||= 1; # +1 (||=)
3132 $y &&= 2; # +1 (&&=)
3133 $z //= 3; # +1 (//=)
3134 return $x;
3135 }",
3136 "foo.pl",
3137 |metric| {
3138 // unit(1) + fn(entry 1 + 3 assignments = 4) = sum 5, max 4.
3139 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
3140 assert_eq!(metric.cyclomatic.cyclomatic_max(), 4);
3141 insta::assert_json_snapshot!(
3142 metric.cyclomatic,
3143 @r#"
3144 {
3145 "sum": 5,
3146 "value": 1,
3147 "average": 5.0,
3148 "min": 1,
3149 "max": 4,
3150 "modified": {
3151 "sum": 5,
3152 "value": 1,
3153 "average": 5.0,
3154 "min": 1,
3155 "max": 4
3156 }
3157 }
3158 "#
3159 );
3160 },
3161 );
3162 }
3163
3164 #[test]
3165 fn perl_foreach_loop() {
3166 check_metrics::<PerlParser>(
3167 "sub f { # +1 (unit) +1 (sub)
3168 foreach my $i (@list) { # +1 for_statement_2
3169 print $i;
3170 }
3171 }",
3172 "foo.pl",
3173 |metric| {
3174 insta::assert_json_snapshot!(metric.cyclomatic, @r#"
3175 {
3176 "sum": 3,
3177 "value": 1,
3178 "average": 3.0,
3179 "min": 1,
3180 "max": 2,
3181 "modified": {
3182 "sum": 3,
3183 "value": 1,
3184 "average": 3.0,
3185 "min": 1,
3186 "max": 2
3187 }
3188 }
3189 "#);
3190 },
3191 );
3192 }
3193
3194 #[test]
3195 fn perl_else_does_not_count_but_elsif_does() {
3196 check_metrics::<PerlParser>(
3197 "sub f { # +1 (unit) +1 (sub)
3198 if ($x) { # +1 if_statement
3199 print 'a';
3200 } elsif ($y) { # +1 elsif_clause
3201 print 'b';
3202 } else {
3203 print 'c';
3204 }
3205 }",
3206 "foo.pl",
3207 |metric| {
3208 insta::assert_json_snapshot!(
3209 metric.cyclomatic,
3210 @r#"
3211 {
3212 "sum": 4,
3213 "value": 1,
3214 "average": 4.0,
3215 "min": 1,
3216 "max": 3,
3217 "modified": {
3218 "sum": 4,
3219 "value": 1,
3220 "average": 4.0,
3221 "min": 1,
3222 "max": 3
3223 }
3224 }
3225 "#
3226 );
3227 },
3228 );
3229 }
3230
3231 #[test]
3232 fn tsx_simple_function() {
3233 check_metrics::<TsxParser>(
3234 "function f(a: number, b: number) { // +2 (+1 unit space)
3235 if (a > 0) { // +1
3236 return a;
3237 } else if (b > 0) { // +1
3238 return b;
3239 }
3240 return 0;
3241 }",
3242 "foo.tsx",
3243 |metric| {
3244 insta::assert_json_snapshot!(
3245 metric.cyclomatic,
3246 @r#"
3247 {
3248 "sum": 4,
3249 "value": 1,
3250 "average": 4.0,
3251 "min": 1,
3252 "max": 3,
3253 "modified": {
3254 "sum": 4,
3255 "value": 1,
3256 "average": 4.0,
3257 "min": 1,
3258 "max": 3
3259 }
3260 }
3261 "#
3262 );
3263 },
3264 );
3265 }
3266
3267 #[test]
3268 fn typescript_if_else_and_switch() {
3269 check_metrics::<TypescriptParser>(
3270 "function classify(value: number): string {
3271 if (value < 0) { // +1
3272 return 'negative';
3273 } else if (value === 0) { // +1
3274 return 'zero';
3275 }
3276 switch (value) {
3277 case 1: // +1
3278 return 'one';
3279 case 2: // +1
3280 return 'two';
3281 default:
3282 return 'other';
3283 }
3284 }",
3285 "foo.ts",
3286 |metric| {
3287 insta::assert_json_snapshot!(
3288 metric.cyclomatic,
3289 @r#"
3290 {
3291 "sum": 6,
3292 "value": 1,
3293 "average": 6.0,
3294 "min": 1,
3295 "max": 5,
3296 "modified": {
3297 "sum": 5,
3298 "value": 1,
3299 "average": 5.0,
3300 "min": 1,
3301 "max": 4
3302 }
3303 }
3304 "#
3305 );
3306 },
3307 );
3308 }
3309
3310 /// Modified CCN: TypeScript switch with 3 cases collapses to 1.
3311 #[test]
3312 fn typescript_switch_modified() {
3313 check_metrics::<TypescriptParser>(
3314 "function f(x: number): string {
3315 switch (x) {
3316 case 1: return 'one';
3317 case 2: return 'two';
3318 case 3: return 'three';
3319 default: return 'other';
3320 }
3321 }",
3322 "foo.ts",
3323 |metric| {
3324 // standard: unit(1) + fn(1) + 3 cases = 5
3325 // modified: unit(1) + fn(1) + switch(1) = 3
3326 insta::assert_json_snapshot!(
3327 metric.cyclomatic,
3328 @r#"
3329 {
3330 "sum": 5,
3331 "value": 1,
3332 "average": 5.0,
3333 "min": 1,
3334 "max": 4,
3335 "modified": {
3336 "sum": 3,
3337 "value": 1,
3338 "average": 3.0,
3339 "min": 1,
3340 "max": 2
3341 }
3342 }
3343 "#
3344 );
3345 },
3346 );
3347 }
3348
3349 #[test]
3350 fn mozjs_if_else_and_switch() {
3351 check_metrics::<MozjsParser>(
3352 "function f(x) { // +2 (+1 unit space)
3353 if (x > 0) { // +1
3354 return 1;
3355 } else if (x < 0) { // +1
3356 return -1;
3357 }
3358 switch (x) {
3359 case 0: // +1
3360 return 0;
3361 case 42: // +1
3362 return 42;
3363 default:
3364 return -2;
3365 }
3366 }",
3367 "foo.js",
3368 |metric| {
3369 insta::assert_json_snapshot!(
3370 metric.cyclomatic,
3371 @r#"
3372 {
3373 "sum": 6,
3374 "value": 1,
3375 "average": 6.0,
3376 "min": 1,
3377 "max": 5,
3378 "modified": {
3379 "sum": 5,
3380 "value": 1,
3381 "average": 5.0,
3382 "min": 1,
3383 "max": 4
3384 }
3385 }
3386 "#
3387 );
3388 },
3389 );
3390 }
3391
3392 /// Modified CCN: MozJS switch with 2 cases collapses to 1.
3393 #[test]
3394 fn mozjs_switch_modified() {
3395 check_metrics::<MozjsParser>(
3396 "function f(x) {
3397 switch (x) {
3398 case 1: return 1;
3399 case 2: return 2;
3400 }
3401 }",
3402 "foo.js",
3403 |metric| {
3404 // standard: unit(1) + fn(1) + 2 cases = 4
3405 // modified: unit(1) + fn(1) + switch(1) = 3
3406 insta::assert_json_snapshot!(
3407 metric.cyclomatic,
3408 @r#"
3409 {
3410 "sum": 4,
3411 "value": 1,
3412 "average": 4.0,
3413 "min": 1,
3414 "max": 3,
3415 "modified": {
3416 "sum": 3,
3417 "value": 1,
3418 "average": 3.0,
3419 "min": 1,
3420 "max": 2
3421 }
3422 }
3423 "#
3424 );
3425 },
3426 );
3427 }
3428
3429 #[test]
3430 fn kotlin_cyclomatic_mixed() {
3431 check_metrics::<KotlinParser>(
3432 "class Calc {
3433 fun compute(x: Int, y: Int): Int {
3434 if (x > 0) { // +1
3435 for (i in 1..x) { // +1
3436 println(i)
3437 }
3438 }
3439 when (y) {
3440 1 -> println(\"one\") // +1 (WhenEntry)
3441 2 -> println(\"two\") // +1
3442 else -> println(\"?\") // skipped (else is default)
3443 }
3444 val ok = x > 0 && y > 0 // +1
3445 try {
3446 println(x / y)
3447 } catch (e: Exception) { // +1
3448 println(\"err\")
3449 }
3450 return x + y
3451 }
3452 }",
3453 "foo.kt",
3454 |metric| {
3455 // expected: unit(1) + class(1) + fn(base 1 + if 1 + for 1 +
3456 // 2 explicit when arms; else skipped + && 1 +
3457 // catch 1) = sum 9, max 7.
3458 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 9);
3459 assert_eq!(metric.cyclomatic.cyclomatic_max(), 7);
3460 insta::assert_json_snapshot!(
3461 metric.cyclomatic,
3462 @r#"
3463 {
3464 "sum": 9,
3465 "value": 1,
3466 "average": 9.0,
3467 "min": 1,
3468 "max": 7,
3469 "modified": {
3470 "sum": 8,
3471 "value": 1,
3472 "average": 8.0,
3473 "min": 1,
3474 "max": 6
3475 }
3476 }
3477 "#
3478 );
3479 },
3480 );
3481 }
3482
3483 /// Modified CCN: Kotlin when with 3 entries collapses to 1.
3484 #[test]
3485 fn kotlin_when_modified() {
3486 check_metrics::<KotlinParser>(
3487 "fun describe(x: Int): String {
3488 return when (x) {
3489 1 -> \"one\"
3490 2 -> \"two\"
3491 3 -> \"three\"
3492 else -> \"other\"
3493 }
3494 }",
3495 "foo.kt",
3496 |metric| {
3497 // standard: unit(1) + fn(base 1 + 3 explicit when arms;
3498 // else skipped per #282) = 5
3499 // modified: unit(1) + fn(1) + WhenExpression(1) = 3
3500 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
3501 assert_eq!(metric.cyclomatic.cyclomatic_max(), 4);
3502 insta::assert_json_snapshot!(
3503 metric.cyclomatic,
3504 @r#"
3505 {
3506 "sum": 5,
3507 "value": 1,
3508 "average": 5.0,
3509 "min": 1,
3510 "max": 4,
3511 "modified": {
3512 "sum": 3,
3513 "value": 1,
3514 "average": 3.0,
3515 "min": 1,
3516 "max": 2
3517 }
3518 }
3519 "#
3520 );
3521 },
3522 );
3523 }
3524
3525 /// Regression #282: the `else -> …` arm in a Kotlin `when`
3526 /// expression must NOT contribute to standard CCN, mirroring the
3527 /// C-family `default:` rule.
3528 #[test]
3529 fn kotlin_when_else_arm_not_counted() {
3530 check_metrics::<KotlinParser>(
3531 "fun describe(x: Int): String {
3532 return when (x) {
3533 1 -> \"one\"
3534 else -> \"other\"
3535 }
3536 }",
3537 "foo.kt",
3538 |metric| {
3539 // expected: unit(1) + fn(base 1 + 1 explicit; else skipped) = 3, max 2.
3540 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
3541 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
3542 },
3543 );
3544 }
3545
3546 /// Cross-check #282: every case-style arm in a Kotlin `when`
3547 /// contributes one standard decision; only the `else ->` arm is
3548 /// skipped. Pairs with `kotlin_when_else_arm_not_counted` (which
3549 /// pins the single-explicit case) to confirm the count scales
3550 /// linearly with explicit arms and is not accidentally hard-coded
3551 /// to one.
3552 #[test]
3553 fn kotlin_when_multiple_explicit_arms_each_count() {
3554 check_metrics::<KotlinParser>(
3555 "fun describe(x: Int): String {
3556 return when (x) {
3557 1 -> \"one\"
3558 2 -> \"two\"
3559 3 -> \"three\"
3560 else -> \"other\"
3561 }
3562 }",
3563 "foo.kt",
3564 |metric| {
3565 // expected: unit(1) + fn(base 1 + 3 explicit; else skipped) = 5, max 4.
3566 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
3567 assert_eq!(metric.cyclomatic.cyclomatic_max(), 4);
3568 },
3569 );
3570 }
3571
3572 #[test]
3573 fn lua_1_level_nesting() {
3574 // chunk: base=1; f: base=1 + for=1 + if=1 = 3; sum=4
3575 check_metrics::<LuaParser>(
3576 "local function f(t)
3577 for i = 1, #t do
3578 if t[i] > 0 then
3579 return t[i]
3580 end
3581 end
3582 return 0
3583end",
3584 "foo.lua",
3585 |metric| {
3586 insta::assert_json_snapshot!(metric.cyclomatic, @r#"
3587 {
3588 "sum": 4,
3589 "value": 1,
3590 "average": 4.0,
3591 "min": 1,
3592 "max": 3,
3593 "modified": {
3594 "sum": 4,
3595 "value": 1,
3596 "average": 4.0,
3597 "min": 1,
3598 "max": 3
3599 }
3600 }
3601 "#);
3602 },
3603 );
3604 }
3605
3606 #[test]
3607 fn lua_elseif_branches() {
3608 // chunk: base=1; classify: base=1 + if=1 + elseif=1 + elseif=1 = 4
3609 // else does NOT add a branch; sum=5
3610 check_metrics::<LuaParser>(
3611 "local function classify(x)
3612 if x > 0 then
3613 return 1
3614 elseif x < 0 then
3615 return -1
3616 elseif x == 0 then
3617 return 0
3618 else
3619 return 0
3620 end
3621end",
3622 "foo.lua",
3623 |metric| {
3624 insta::assert_json_snapshot!(metric.cyclomatic, @r#"
3625 {
3626 "sum": 5,
3627 "value": 1,
3628 "average": 5.0,
3629 "min": 1,
3630 "max": 4,
3631 "modified": {
3632 "sum": 5,
3633 "value": 1,
3634 "average": 5.0,
3635 "min": 1,
3636 "max": 4
3637 }
3638 }
3639 "#);
3640 },
3641 );
3642 }
3643
3644 #[test]
3645 fn lua_logical_operators() {
3646 // chunk: base=1; f: base=1 + if=1 + and=1 + or=1 = 4; sum=5
3647 check_metrics::<LuaParser>(
3648 "local function f(a, b, c)
3649 if a and b or c then
3650 return 1
3651 end
3652 return 0
3653end",
3654 "foo.lua",
3655 |metric| {
3656 insta::assert_json_snapshot!(metric.cyclomatic, @r#"
3657 {
3658 "sum": 5,
3659 "value": 1,
3660 "average": 5.0,
3661 "min": 1,
3662 "max": 4,
3663 "modified": {
3664 "sum": 5,
3665 "value": 1,
3666 "average": 5.0,
3667 "min": 1,
3668 "max": 4
3669 }
3670 }
3671 "#);
3672 },
3673 );
3674 }
3675
3676 #[test]
3677 fn bash_nested_control_flow() {
3678 check_metrics::<BashParser>(
3679 "#!/bin/bash
3680f() {
3681 if [ $1 -eq 1 ]; then
3682 for i in 1 2 3; do
3683 echo $i
3684 done
3685 elif [ $1 -eq 2 ]; then
3686 echo 'two'
3687 fi
3688}",
3689 "foo.sh",
3690 |metric| {
3691 insta::assert_json_snapshot!(
3692 metric.cyclomatic,
3693 {".sum" => insta::rounded_redaction(2)}
3694 );
3695 },
3696 );
3697 }
3698
3699 /// Regression test for #107: case…esac must not double-count the container.
3700 /// Standard CCN counts only arms (matching C-family `switch` semantics).
3701 /// Modified CCN counts only the container.
3702 #[test]
3703 fn bash_case_modified() {
3704 check_metrics::<BashParser>(
3705 "#!/bin/bash
3706f() {
3707 case $1 in
3708 one) echo 1 ;;
3709 two) echo 2 ;;
3710 three) echo 3 ;;
3711 esac
3712}",
3713 "foo.sh",
3714 |metric| {
3715 // standard: unit(1) + fn(1) + 3 case_items = 5
3716 // modified: unit(1) + fn(1) + case_stmt(1) = 3
3717 insta::assert_json_snapshot!(
3718 metric.cyclomatic,
3719 @r#"
3720 {
3721 "sum": 5,
3722 "value": 1,
3723 "average": 5.0,
3724 "min": 1,
3725 "max": 4,
3726 "modified": {
3727 "sum": 3,
3728 "value": 1,
3729 "average": 3.0,
3730 "min": 1,
3731 "max": 2
3732 }
3733 }
3734 "#
3735 );
3736 },
3737 );
3738 }
3739
3740 #[test]
3741 fn tcl_1_level_nesting() {
3742 // chunk: base=1; f: base=1 + while=1 + if=1 = 3; sum=4
3743 check_metrics::<TclParser>(
3744 "proc f {x} {
3745 while {$x > 0} {
3746 if {$x > 10} {
3747 set x [expr {$x - 1}]
3748 }
3749 }
3750}",
3751 "foo.tcl",
3752 |metric| {
3753 // unit(1) + proc(base 1 + while 1 + if 1) = sum 4, max 3.
3754 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
3755 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
3756 insta::assert_json_snapshot!(metric.cyclomatic);
3757 },
3758 );
3759 }
3760
3761 #[test]
3762 fn tcl_elseif_branch() {
3763 // if=1, elseif=1; else does NOT add a branch; sum=3 (chunk base=1)
3764 check_metrics::<TclParser>(
3765 "proc f {x} {
3766 if {$x > 10} {
3767 puts big
3768 } elseif {$x > 5} {
3769 puts medium
3770 } else {
3771 puts small
3772 }
3773}",
3774 "foo.tcl",
3775 |metric| {
3776 // unit(1) + proc(base 1 + if 1 + elseif 1) = sum 4, max 3.
3777 // else does NOT add a branch.
3778 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
3779 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
3780 insta::assert_json_snapshot!(metric.cyclomatic);
3781 },
3782 );
3783 }
3784
3785 #[test]
3786 fn tcl_logical_operators() {
3787 check_metrics::<TclParser>(
3788 "proc f {x y z} {
3789 if {$x > 0 && $y > 0 || $z > 0} {
3790 puts ok
3791 }
3792}",
3793 "foo.tcl",
3794 |metric| {
3795 // unit(1) + proc(base 1 + if 1 + && 1 + || 1) = sum 5, max 4.
3796 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
3797 assert_eq!(metric.cyclomatic.cyclomatic_max(), 4);
3798 insta::assert_json_snapshot!(metric.cyclomatic);
3799 },
3800 );
3801 }
3802
3803 #[test]
3804 fn tcl_catch_branch() {
3805 // `catch` command adds +1 (conditional handler); `try` does NOT add a branch.
3806 // source_file(1) + proc_space(base=1 + catch=1 = 2) = sum=3
3807 check_metrics::<TclParser>(
3808 "proc f {} {
3809 catch {
3810 expr {1 / 0}
3811 } msg
3812}",
3813 "foo.tcl",
3814 |metric| {
3815 // unit(1) + proc(base 1 + catch 1) = sum 3, max 2.
3816 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
3817 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
3818 insta::assert_json_snapshot!(metric.cyclomatic);
3819 },
3820 );
3821 }
3822
3823 #[test]
3824 fn tcl_try_no_branch() {
3825 // `try` is NOT a conditional construct; it does not add cyclomatic complexity.
3826 // Only the base counts: source_file(1) + proc_space(base=1) = sum=2, average=1.
3827 check_metrics::<TclParser>(
3828 "proc f {} {
3829 try {
3830 expr {1 / 0}
3831 } finally {
3832 puts done
3833 }
3834}",
3835 "foo.tcl",
3836 |metric| {
3837 insta::assert_json_snapshot!(
3838 metric.cyclomatic,
3839 @r#"
3840 {
3841 "sum": 2,
3842 "value": 1,
3843 "average": 2.0,
3844 "min": 1,
3845 "max": 1,
3846 "modified": {
3847 "sum": 2,
3848 "value": 1,
3849 "average": 2.0,
3850 "min": 1,
3851 "max": 1
3852 }
3853 }
3854 "#
3855 );
3856 },
3857 );
3858 }
3859
3860 #[test]
3861 fn tcl_switch_cyclomatic() {
3862 // Tcl `switch` is a generic command; each non-`default` arm is a
3863 // decision point in standard CCN, while modified CCN counts the
3864 // construct once (issue #467). Three arms (1, 2, default): the two
3865 // non-default arms add +2 standard; `default` is free.
3866 check_metrics::<TclParser>(
3867 "proc f {x} {
3868 switch $x {
3869 1 { puts a }
3870 2 { puts b }
3871 default { puts c }
3872 }
3873}",
3874 "foo.tcl",
3875 |metric| {
3876 // unit(1) + proc(base 1 + arm 1 + arm 2) = standard sum 4, max 3.
3877 // modified collapses arms to one container: unit(1) +
3878 // proc(base 1 + switch 1) = sum 3, max 2.
3879 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
3880 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
3881 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 3);
3882 assert_eq!(metric.cyclomatic.cyclomatic_modified_max(), 2);
3883 },
3884 );
3885 }
3886
3887 #[test]
3888 fn tcl_switch_cyclomatic_no_default_with_options() {
3889 // No `default` arm, and leading `switch` options (`-exact --`) precede
3890 // the value: the arm list is still the trailing braced word, so both
3891 // arms count. Guards the option-form arm-list location (issue #467).
3892 check_metrics::<TclParser>(
3893 "proc f {x} {
3894 switch -exact -- $x {
3895 1 { puts a }
3896 2 { puts b }
3897 }
3898}",
3899 "foo.tcl",
3900 |metric| {
3901 // unit(1) + proc(base 1 + arm 1 + arm 2) = standard sum 4, max 3.
3902 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
3903 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
3904 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 3);
3905 assert_eq!(metric.cyclomatic.cyclomatic_modified_max(), 2);
3906 },
3907 );
3908 }
3909
3910 #[test]
3911 fn tcl_switch_split_form_stays_uncounted() {
3912 // The split arm form (`switch $x a {…} b {…}`) passes each arm
3913 // body as its own sibling `braced_word` argument instead of
3914 // wrapping the whole arm list in one, so there is no arm node to
3915 // count and `tcl_switch_arm_list` deliberately declines it
3916 // (issue #467). The construct is then left uncounted in BOTH
3917 // tiers, exactly as an unrecognised command is — not counted as a
3918 // container in modified CCN.
3919 check_metrics::<TclParser>(
3920 "proc f {x} {
3921 switch $x a { puts a } b { puts b }
3922}",
3923 "foo.tcl",
3924 |metric| {
3925 // unit(1) + proc(base 1) and nothing else, in either
3926 // tier; the brace-list spelling of the same two-arm
3927 // switch scores 4 / 3 (see `tcl_switch_cyclomatic`).
3928 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 2);
3929 assert_eq!(metric.cyclomatic.cyclomatic_max(), 1);
3930 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 2);
3931 assert_eq!(metric.cyclomatic.cyclomatic_modified_max(), 1);
3932 },
3933 );
3934 }
3935
3936 #[test]
3937 fn tcl_for_cyclomatic() {
3938 // Tcl `for` is a generic command — the grammar has no `for` rule —
3939 // so it is detected by leading word (issue #1264): one loop decision
3940 // in both standard and modified CCN, matching `foreach`/`while`.
3941 check_metrics::<TclParser>(
3942 "proc f {n} {
3943 for {set i 0} {$i < $n} {incr i} {
3944 puts $i
3945 }
3946}",
3947 "foo.tcl",
3948 |metric| {
3949 // unit(1) + proc(base 1 + for 1) = sum 3, max 2, both tiers.
3950 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
3951 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
3952 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 3);
3953 assert_eq!(metric.cyclomatic.cyclomatic_modified_max(), 2);
3954 },
3955 );
3956 }
3957
3958 #[test]
3959 fn tcl_for_cyclomatic_name_gate() {
3960 // The detection reads the command's `name` field: a command whose
3961 // name merely starts with "for" (`format`, with `for`-shaped braced
3962 // arguments) and a `for` word in argument position (`puts for`) must
3963 // both stay at zero (issue #1264).
3964 check_metrics::<TclParser>(
3965 "proc f {} {
3966 format {a} {b} {c} {d}
3967 puts for
3968}",
3969 "foo.tcl",
3970 |metric| {
3971 // unit(1) + proc(base 1) only; no decision points.
3972 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 2);
3973 assert_eq!(metric.cyclomatic.cyclomatic_max(), 1);
3974 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 2);
3975 },
3976 );
3977 }
3978
3979 #[test]
3980 fn tcl_irules_for_parity() {
3981 // iRules models `for` as a dedicated kind counted by the kind
3982 // dispatch; Tcl detects it by leading word (issue #1264). The same
3983 // loop must score identically in both — and the iRules figure also
3984 // pins that its dedicated kind is not double-counted through the
3985 // Tcl command-name path.
3986 // unit(1) + container(base 1 + for 1) = sum 3, max 2, both tiers.
3987 check_metrics::<TclParser>(
3988 "proc f {} {
3989 for {set i 0} {$i < 10} {incr i} {
3990 puts $i
3991 }
3992}",
3993 "foo.tcl",
3994 |metric| {
3995 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
3996 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
3997 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 3);
3998 },
3999 );
4000 check_metrics::<IrulesParser>(
4001 "when HTTP_REQUEST {
4002 for {set i 0} {$i < 10} {incr i} {
4003 puts $i
4004 }
4005}
4006",
4007 "foo.irule",
4008 |metric| {
4009 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
4010 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
4011 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 3);
4012 },
4013 );
4014 }
4015
4016 #[test]
4017 fn tcl_try_on_error_cyclomatic() {
4018 // Tcl `try` is a dedicated kind whose single permitted `on error`
4019 // handler is a flat token run (issue #1266): the handler is one
4020 // decision point in both tiers, matching `catch`; `finally` is
4021 // unconditional and free.
4022 check_metrics::<TclParser>(
4023 "proc f {} {
4024 try {
4025 risky
4026 } on error {msg} {
4027 puts $msg
4028 } finally {
4029 cleanup
4030 }
4031}",
4032 "foo.tcl",
4033 |metric| {
4034 // unit(1) + proc(base 1 + handler 1) = sum 3, max 2, both tiers.
4035 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
4036 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
4037 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 3);
4038 assert_eq!(metric.cyclomatic.cyclomatic_modified_max(), 2);
4039 },
4040 );
4041 }
4042
4043 #[test]
4044 fn tcl_try_finally_only_cyclomatic() {
4045 // A `try` with no handler has no decision point: `finally` is
4046 // unconditional cleanup and must stay +0 (issue #1266, the
4047 // cross-language `finally` convention of #416).
4048 check_metrics::<TclParser>(
4049 "proc f {} {
4050 try {
4051 risky
4052 } finally {
4053 cleanup
4054 }
4055}",
4056 "foo.tcl",
4057 |metric| {
4058 // unit(1) + proc(base 1) only.
4059 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 2);
4060 assert_eq!(metric.cyclomatic.cyclomatic_max(), 1);
4061 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 2);
4062 },
4063 );
4064 }
4065
4066 #[test]
4067 fn irules_try_handlers_cyclomatic() {
4068 // iRules wraps each `try` handler in a dedicated `on_handler` /
4069 // `trap_handler` node (unlike Tcl's flat tokens): one decision
4070 // point each in both tiers (issue #1266). The figure also pins
4071 // that the handlers no longer open anonymous function spaces —
4072 // as spaces each would carry its own base 1, making the sum 6.
4073 check_metrics::<IrulesParser>(
4074 "proc f {} {
4075 try {
4076 risky
4077 } on error {msg} {
4078 puts $msg
4079 } trap {POSIX} {msg} {
4080 puts $msg
4081 } finally {
4082 cleanup
4083 }
4084}",
4085 "foo.irule",
4086 |metric| {
4087 // unit(1) + proc(base 1 + on 1 + trap 1) = sum 4, max 3.
4088 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
4089 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
4090 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
4091 assert_eq!(metric.cyclomatic.cyclomatic_modified_max(), 3);
4092 },
4093 );
4094 }
4095
4096 #[test]
4097 fn tcl_irules_try_parity() {
4098 // The same single-handler `try` must score identically in Tcl
4099 // (flat `on`/`error` tokens under `try`) and iRules (a dedicated
4100 // `on_handler` wrapper) — issue #1266.
4101 // unit(1) + proc(base 1 + handler 1) = sum 3, max 2, both tiers.
4102 let source = "proc f {} {
4103 try {
4104 risky
4105 } on error {msg} {
4106 puts $msg
4107 } finally {
4108 cleanup
4109 }
4110}";
4111 check_metrics::<TclParser>(source, "foo.tcl", |metric| {
4112 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
4113 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
4114 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 3);
4115 });
4116 check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
4117 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
4118 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
4119 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 3);
4120 });
4121 }
4122
4123 #[test]
4124 fn mozjs_for_loop() {
4125 check_metrics::<MozjsParser>(
4126 "function f(n) { // +2 (+1 unit)
4127 var s = 0;
4128 for (var i = 0; i < n; i++) { // +1
4129 s += i;
4130 }
4131 return s;
4132 }",
4133 "foo.js",
4134 |metric| {
4135 // unit(1) + fn(base 1 + for 1) = sum 3, max 2.
4136 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
4137 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
4138 insta::assert_json_snapshot!(metric.cyclomatic);
4139 },
4140 );
4141 }
4142
4143 #[test]
4144 fn mozjs_logical_operators() {
4145 check_metrics::<MozjsParser>(
4146 "function f(a, b, c) { // +2 (+1 unit)
4147 if (a && b || c) { // +1 if, +1 &&, +1 ||
4148 return 1;
4149 }
4150 return 0;
4151 }",
4152 "foo.js",
4153 |metric| {
4154 // unit(1) + fn(base 1 + if 1 + && 1 + || 1) = sum 5, max 4.
4155 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
4156 assert_eq!(metric.cyclomatic.cyclomatic_max(), 4);
4157 insta::assert_json_snapshot!(metric.cyclomatic);
4158 },
4159 );
4160 }
4161
4162 #[test]
4163 fn javascript_nullish_coalescing_chain_226() {
4164 // `??` is short-circuit and must count as
4165 // a decision point in cyclomatic complexity. `a ?? b ?? c` adds two
4166 // `??` decisions on top of the function entry.
4167 check_metrics::<JavascriptParser>(
4168 "function pick(a, b, c) { // +1 (entry)
4169 return a ?? b ?? c; // +2 (two `??`)
4170 }",
4171 "foo.js",
4172 |metric| {
4173 // unit(1) + fn(entry 1 + 2*?? = 3) = sum 4, max 3.
4174 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
4175 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
4176 insta::assert_json_snapshot!(
4177 metric.cyclomatic,
4178 @r#"
4179 {
4180 "sum": 4,
4181 "value": 1,
4182 "average": 4.0,
4183 "min": 1,
4184 "max": 3,
4185 "modified": {
4186 "sum": 4,
4187 "value": 1,
4188 "average": 4.0,
4189 "min": 1,
4190 "max": 3
4191 }
4192 }
4193 "#
4194 );
4195 },
4196 );
4197 }
4198
4199 #[test]
4200 fn typescript_nullish_coalescing_with_if_226() {
4201 // TypeScript must count `??` as a
4202 // decision. This mirrors the example in the issue body.
4203 check_metrics::<TypescriptParser>(
4204 "function classify(x: string | null, fallback: string | null): string { // +1 (entry)
4205 if (x === \"y\") return \"yes\"; // +1 (if)
4206 return x ?? fallback ?? \"unknown\"; // +2 (two `??`)
4207 }",
4208 "foo.ts",
4209 |metric| {
4210 // unit(1) + fn(entry 1 + if 1 + 2*?? = 4) = sum 5, max 4.
4211 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
4212 assert_eq!(metric.cyclomatic.cyclomatic_max(), 4);
4213 insta::assert_json_snapshot!(
4214 metric.cyclomatic,
4215 @r#"
4216 {
4217 "sum": 5,
4218 "value": 1,
4219 "average": 5.0,
4220 "min": 1,
4221 "max": 4,
4222 "modified": {
4223 "sum": 5,
4224 "value": 1,
4225 "average": 5.0,
4226 "min": 1,
4227 "max": 4
4228 }
4229 }
4230 "#
4231 );
4232 },
4233 );
4234 }
4235
4236 #[test]
4237 fn tsx_nullish_coalescing_chain_226() {
4238 // TSX must count `??` the same as JS/TS.
4239 check_metrics::<TsxParser>(
4240 "function pick(a: number | null, b: number | null, c: number): number { // +1 (entry)
4241 return a ?? b ?? c; // +2 (two `??`)
4242 }",
4243 "foo.tsx",
4244 |metric| {
4245 // unit(1) + fn(entry 1 + 2*?? = 3) = sum 4, max 3.
4246 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
4247 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
4248 insta::assert_json_snapshot!(
4249 metric.cyclomatic,
4250 @r#"
4251 {
4252 "sum": 4,
4253 "value": 1,
4254 "average": 4.0,
4255 "min": 1,
4256 "max": 3,
4257 "modified": {
4258 "sum": 4,
4259 "value": 1,
4260 "average": 4.0,
4261 "min": 1,
4262 "max": 3
4263 }
4264 }
4265 "#
4266 );
4267 },
4268 );
4269 }
4270
4271 #[test]
4272 fn mozjs_nullish_coalescing_chain_226() {
4273 // Mozjs must count `??` the same as JS.
4274 check_metrics::<MozjsParser>(
4275 "function pick(a, b, c) { // +1 (entry)
4276 return a ?? b ?? c; // +2 (two `??`)
4277 }",
4278 "foo.js",
4279 |metric| {
4280 // unit(1) + fn(entry 1 + 2*?? = 3) = sum 4, max 3.
4281 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
4282 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
4283 insta::assert_json_snapshot!(
4284 metric.cyclomatic,
4285 @r#"
4286 {
4287 "sum": 4,
4288 "value": 1,
4289 "average": 4.0,
4290 "min": 1,
4291 "max": 3,
4292 "modified": {
4293 "sum": 4,
4294 "value": 1,
4295 "average": 4.0,
4296 "min": 1,
4297 "max": 3
4298 }
4299 }
4300 "#
4301 );
4302 },
4303 );
4304 }
4305
4306 #[test]
4307 fn javascript_nullish_coalescing_assignment_231() {
4308 // `x ??= y` is `x = x ?? y` — one short-circuit decision edge,
4309 // same as `??`. Two `??=` assignments add +2 on top of the entry.
4310 check_metrics::<JavascriptParser>(
4311 "function pick(o) { // +1 (entry)
4312 o.x ??= 1; // +1 (??=)
4313 o.y ??= 2; // +1 (??=)
4314 return o;
4315 }",
4316 "foo.js",
4317 |metric| {
4318 // unit(1) + fn(entry 1 + 2*??= = 3) = sum 4, max 3.
4319 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
4320 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
4321 insta::assert_json_snapshot!(
4322 metric.cyclomatic,
4323 @r#"
4324 {
4325 "sum": 4,
4326 "value": 1,
4327 "average": 4.0,
4328 "min": 1,
4329 "max": 3,
4330 "modified": {
4331 "sum": 4,
4332 "value": 1,
4333 "average": 4.0,
4334 "min": 1,
4335 "max": 3
4336 }
4337 }
4338 "#
4339 );
4340 },
4341 );
4342 }
4343
4344 #[test]
4345 fn typescript_nullish_coalescing_assignment_231() {
4346 // TypeScript must count `??=` the same as JS.
4347 check_metrics::<TypescriptParser>(
4348 "function pick(o: { x?: number; y?: number }) { // +1 (entry)
4349 o.x ??= 1; // +1 (??=)
4350 o.y ??= 2; // +1 (??=)
4351 return o;
4352 }",
4353 "foo.ts",
4354 |metric| {
4355 // unit(1) + fn(entry 1 + 2*??= = 3) = sum 4, max 3.
4356 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
4357 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
4358 insta::assert_json_snapshot!(
4359 metric.cyclomatic,
4360 @r#"
4361 {
4362 "sum": 4,
4363 "value": 1,
4364 "average": 4.0,
4365 "min": 1,
4366 "max": 3,
4367 "modified": {
4368 "sum": 4,
4369 "value": 1,
4370 "average": 4.0,
4371 "min": 1,
4372 "max": 3
4373 }
4374 }
4375 "#
4376 );
4377 },
4378 );
4379 }
4380
4381 #[test]
4382 fn tsx_nullish_coalescing_assignment_231() {
4383 // TSX must count `??=` the same as JS/TS.
4384 check_metrics::<TsxParser>(
4385 "function pick(o: { x?: number; y?: number }) { // +1 (entry)
4386 o.x ??= 1; // +1 (??=)
4387 o.y ??= 2; // +1 (??=)
4388 return o;
4389 }",
4390 "foo.tsx",
4391 |metric| {
4392 // unit(1) + fn(entry 1 + 2*??= = 3) = sum 4, max 3.
4393 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
4394 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
4395 insta::assert_json_snapshot!(
4396 metric.cyclomatic,
4397 @r#"
4398 {
4399 "sum": 4,
4400 "value": 1,
4401 "average": 4.0,
4402 "min": 1,
4403 "max": 3,
4404 "modified": {
4405 "sum": 4,
4406 "value": 1,
4407 "average": 4.0,
4408 "min": 1,
4409 "max": 3
4410 }
4411 }
4412 "#
4413 );
4414 },
4415 );
4416 }
4417
4418 #[test]
4419 fn mozjs_nullish_coalescing_assignment_231() {
4420 // Mozjs must count `??=` the same as JS.
4421 check_metrics::<MozjsParser>(
4422 "function pick(o) { // +1 (entry)
4423 o.x ??= 1; // +1 (??=)
4424 o.y ??= 2; // +1 (??=)
4425 return o;
4426 }",
4427 "foo.js",
4428 |metric| {
4429 // unit(1) + fn(entry 1 + 2*??= = 3) = sum 4, max 3.
4430 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
4431 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
4432 insta::assert_json_snapshot!(
4433 metric.cyclomatic,
4434 @r#"
4435 {
4436 "sum": 4,
4437 "value": 1,
4438 "average": 4.0,
4439 "min": 1,
4440 "max": 3,
4441 "modified": {
4442 "sum": 4,
4443 "value": 1,
4444 "average": 4.0,
4445 "min": 1,
4446 "max": 3
4447 }
4448 }
4449 "#
4450 );
4451 },
4452 );
4453 }
4454
4455 #[test]
4456 fn javascript_short_circuit_assignments_248() {
4457 // `&&=`, `||=`, `??=` are each one short-circuit decision edge —
4458 // semantically `x = x op y`. #231 added only `??=`; #248 adds the
4459 // sibling `&&=` and `||=`.
4460 check_metrics::<JavascriptParser>(
4461 "function f(x, y, z) { // +1 (entry)
4462 x ??= 1; // +1 (??=)
4463 y &&= 2; // +1 (&&=)
4464 z ||= 3; // +1 (||=)
4465 return x;
4466 }",
4467 "foo.js",
4468 |metric| {
4469 // unit(1) + fn(entry 1 + 3 assignments = 4) = sum 5, max 4.
4470 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
4471 assert_eq!(metric.cyclomatic.cyclomatic_max(), 4);
4472 insta::assert_json_snapshot!(
4473 metric.cyclomatic,
4474 @r#"
4475 {
4476 "sum": 5,
4477 "value": 1,
4478 "average": 5.0,
4479 "min": 1,
4480 "max": 4,
4481 "modified": {
4482 "sum": 5,
4483 "value": 1,
4484 "average": 5.0,
4485 "min": 1,
4486 "max": 4
4487 }
4488 }
4489 "#
4490 );
4491 },
4492 );
4493 }
4494
4495 #[test]
4496 fn typescript_short_circuit_assignments_248() {
4497 // TypeScript parallel of #248: `&&=` / `||=` / `??=` each +1.
4498 check_metrics::<TypescriptParser>(
4499 "function f(x: number | null, y: number | null, z: number | null): number { // +1 (entry)
4500 x ??= 1; // +1 (??=)
4501 y &&= 2; // +1 (&&=)
4502 z ||= 3; // +1 (||=)
4503 return x ?? 0; // +1 (??)
4504 }",
4505 "foo.ts",
4506 |metric| {
4507 // unit(1) + fn(entry 1 + 3 op= + 1 `??` = 5) = sum 6, max 5.
4508 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 6);
4509 assert_eq!(metric.cyclomatic.cyclomatic_max(), 5);
4510 insta::assert_json_snapshot!(
4511 metric.cyclomatic,
4512 @r#"
4513 {
4514 "sum": 6,
4515 "value": 1,
4516 "average": 6.0,
4517 "min": 1,
4518 "max": 5,
4519 "modified": {
4520 "sum": 6,
4521 "value": 1,
4522 "average": 6.0,
4523 "min": 1,
4524 "max": 5
4525 }
4526 }
4527 "#
4528 );
4529 },
4530 );
4531 }
4532
4533 #[test]
4534 fn tsx_short_circuit_assignments_248() {
4535 // TSX parallel of #248: `&&=` / `||=` / `??=` each +1.
4536 check_metrics::<TsxParser>(
4537 "function f(x: number | null, y: number | null, z: number | null): number { // +1 (entry)
4538 x ??= 1; // +1 (??=)
4539 y &&= 2; // +1 (&&=)
4540 z ||= 3; // +1 (||=)
4541 return x ?? 0; // +1 (??)
4542 }",
4543 "foo.tsx",
4544 |metric| {
4545 // unit(1) + fn(entry 1 + 3 op= + 1 `??` = 5) = sum 6, max 5.
4546 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 6);
4547 assert_eq!(metric.cyclomatic.cyclomatic_max(), 5);
4548 insta::assert_json_snapshot!(
4549 metric.cyclomatic,
4550 @r#"
4551 {
4552 "sum": 6,
4553 "value": 1,
4554 "average": 6.0,
4555 "min": 1,
4556 "max": 5,
4557 "modified": {
4558 "sum": 6,
4559 "value": 1,
4560 "average": 6.0,
4561 "min": 1,
4562 "max": 5
4563 }
4564 }
4565 "#
4566 );
4567 },
4568 );
4569 }
4570
4571 #[test]
4572 fn mozjs_short_circuit_assignments_248() {
4573 // Mozjs parallel of #248: `&&=` / `||=` / `??=` each +1.
4574 check_metrics::<MozjsParser>(
4575 "function f(x, y, z) { // +1 (entry)
4576 x ??= 1; // +1 (??=)
4577 y &&= 2; // +1 (&&=)
4578 z ||= 3; // +1 (||=)
4579 return x;
4580 }",
4581 "foo.js",
4582 |metric| {
4583 // unit(1) + fn(entry 1 + 3 assignments = 4) = sum 5, max 4.
4584 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
4585 assert_eq!(metric.cyclomatic.cyclomatic_max(), 4);
4586 insta::assert_json_snapshot!(
4587 metric.cyclomatic,
4588 @r#"
4589 {
4590 "sum": 5,
4591 "value": 1,
4592 "average": 5.0,
4593 "min": 1,
4594 "max": 4,
4595 "modified": {
4596 "sum": 5,
4597 "value": 1,
4598 "average": 5.0,
4599 "min": 1,
4600 "max": 4
4601 }
4602 }
4603 "#
4604 );
4605 },
4606 );
4607 }
4608
4609 // Issue #281: optional chaining (`?.`) is short-circuit (it skips
4610 // the rest of the chain when the LHS is nullish), so each `?.`
4611 // adds one cyclomatic decision point. Before the fix, JS-family
4612 // cyclomatic ignored `?.` entirely. The four tests below mirror
4613 // the existing `nullish_coalescing_chain_226` pattern but for
4614 // `?.`: two `?.` in a chain add +2 on top of the function entry.
4615 #[test]
4616 fn javascript_optional_chain_counted_in_cyclomatic_281() {
4617 check_metrics::<JavascriptParser>(
4618 "function pick(a) { // +1 (entry)
4619 return a?.b?.c; // +2 (two `?.`)
4620 }",
4621 "foo.js",
4622 |metric| {
4623 // unit(1) + fn(entry 1 + 2*?. = 3) = sum 4, max 3.
4624 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
4625 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
4626 },
4627 );
4628 }
4629
4630 #[test]
4631 fn mozjs_optional_chain_counted_in_cyclomatic_281() {
4632 check_metrics::<MozjsParser>(
4633 "function pick(a) { // +1 (entry)
4634 return a?.b?.c; // +2 (two `?.`)
4635 }",
4636 "foo.js",
4637 |metric| {
4638 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
4639 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
4640 },
4641 );
4642 }
4643
4644 #[test]
4645 fn typescript_optional_chain_counted_in_cyclomatic_281() {
4646 // TS exposes `?.` as both an `optional_chain` wrapper (over
4647 // member expressions) and a bare token (over call
4648 // expressions). We dispatch on `QMARKDOT` so every textual
4649 // `?.` adds exactly one decision point regardless of context.
4650 check_metrics::<TypescriptParser>(
4651 "function pick(a: any) { // +1 (entry)
4652 return a?.b?.c; // +2 (two `?.`)
4653 }",
4654 "foo.ts",
4655 |metric| {
4656 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
4657 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
4658 },
4659 );
4660 }
4661
4662 #[test]
4663 fn tsx_optional_chain_counted_in_cyclomatic_281() {
4664 check_metrics::<TsxParser>(
4665 "function pick(a: any) { // +1 (entry)
4666 return a?.b?.c; // +2 (two `?.`)
4667 }",
4668 "foo.tsx",
4669 |metric| {
4670 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
4671 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
4672 },
4673 );
4674 }
4675
4676 // Mix of member-expression `?.` and call-expression `?.()`:
4677 // ensures the TS/TSX dispatch on `QMARKDOT` (not the wrapper)
4678 // counts both forms exactly once. Both forms emit the bare `?.`
4679 // token; the wrapper only appears around member expressions.
4680 #[test]
4681 fn typescript_optional_chain_call_form_counted_281() {
4682 check_metrics::<TypescriptParser>(
4683 "function pick(a: any) { // +1 (entry)
4684 return a?.b?.(); // +2 (member `?.` + call `?.`)
4685 }",
4686 "foo.ts",
4687 |metric| {
4688 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
4689 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
4690 },
4691 );
4692 }
4693
4694 #[test]
4695 fn tsx_optional_chain_call_form_counted_281() {
4696 check_metrics::<TsxParser>(
4697 "function pick(a: any) { // +1 (entry)
4698 return a?.b?.(); // +2 (member `?.` + call `?.`)
4699 }",
4700 "foo.tsx",
4701 |metric| {
4702 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
4703 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
4704 },
4705 );
4706 }
4707
4708 #[test]
4709 fn csharp_nullish_coalescing_assignment_231() {
4710 // C#'s `??=` is short-circuit (RHS evaluates only when LHS is null)
4711 // and must add +1 cyclomatic per occurrence (#231).
4712 check_metrics::<CsharpParser>(
4713 "public class A {
4714 public int? x;
4715 public int? y;
4716 public void Pick() { // +1 (entry)
4717 x ??= 1; // +1 (??=)
4718 y ??= 2; // +1 (??=)
4719 }
4720 }",
4721 "foo.cs",
4722 |metric| {
4723 // unit(1) + class(1) + Pick(entry 1 + 2*??= = 3) = sum 5,
4724 // max 3 (Pick).
4725 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
4726 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
4727 insta::assert_json_snapshot!(
4728 metric.cyclomatic,
4729 @r#"
4730 {
4731 "sum": 5,
4732 "value": 1,
4733 "average": 5.0,
4734 "min": 1,
4735 "max": 3,
4736 "modified": {
4737 "sum": 5,
4738 "value": 1,
4739 "average": 5.0,
4740 "min": 1,
4741 "max": 3
4742 }
4743 }
4744 "#
4745 );
4746 },
4747 );
4748 }
4749
4750 #[test]
4751 fn mozjs_while_loop() {
4752 check_metrics::<MozjsParser>(
4753 "function f(n) { // +2 (+1 unit)
4754 var i = 0;
4755 while (i < n) { // +1
4756 i++;
4757 }
4758 return i;
4759 }",
4760 "foo.js",
4761 |metric| {
4762 // unit(1) + fn(base 1 + while 1) = sum 3, max 2.
4763 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
4764 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
4765 insta::assert_json_snapshot!(metric.cyclomatic);
4766 },
4767 );
4768 }
4769
4770 #[test]
4771 fn bash_while_loop() {
4772 check_metrics::<BashParser>(
4773 "#!/bin/bash
4774f() {
4775 local n=$1
4776 while [ $n -gt 0 ]; do
4777 echo $n
4778 n=$((n - 1))
4779 done
4780}",
4781 "foo.sh",
4782 |metric| {
4783 // unit(1) + fn(base 1 + while 1) = sum 3, max 2.
4784 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
4785 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
4786 insta::assert_json_snapshot!(metric.cyclomatic);
4787 },
4788 );
4789 }
4790
4791 #[test]
4792 fn bash_case_statement() {
4793 check_metrics::<BashParser>(
4794 "#!/bin/bash
4795f() {
4796 case $1 in
4797 start) echo starting ;;
4798 stop) echo stopping ;;
4799 *) echo unknown ;;
4800 esac
4801}",
4802 "foo.sh",
4803 |metric| {
4804 // standard: unit(1) + fn(base 1 + 2 explicit case_items;
4805 // `*)` skipped per #211) = sum 4, max 3.
4806 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
4807 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
4808 insta::assert_json_snapshot!(metric.cyclomatic);
4809 },
4810 );
4811 }
4812
4813 /// Regression #211: a bare `*)` arm is Bash's analogue of the
4814 /// C-family `default:` and must NOT contribute to standard CCN.
4815 /// Without the fix, this 2-arm case reports `cyclomatic_max == 3`
4816 /// (1 base + 2 arms); with the fix it reports `2` (1 base + 1
4817 /// explicit arm), matching every other switch-bearing language
4818 /// in `tests/parity/cyclomatic_cross_language_parity.rs`.
4819 #[test]
4820 fn bash_case_bare_wildcard_excluded() {
4821 check_metrics::<BashParser>(
4822 "#!/bin/bash
4823f() {
4824 case \"$1\" in
4825 one) echo 1 ;;
4826 *) echo 0 ;;
4827 esac
4828}",
4829 "foo.sh",
4830 |metric| {
4831 // standard: unit(1) + fn(base 1 + 1 explicit; `*)` skipped) = 3, max 2.
4832 // modified: unit(1) + fn(base 1 + case_stmt 1) = 3, max 2.
4833 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
4834 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
4835 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 3);
4836 assert_eq!(metric.cyclomatic.cyclomatic_modified_max(), 2);
4837 insta::assert_json_snapshot!(metric.cyclomatic);
4838 },
4839 );
4840 }
4841
4842 /// A multi-value pattern containing `*` (`a|*)`) is NOT a bare
4843 /// wildcard — both alternations make it a non-default case. The
4844 /// arm still contributes one standard decision.
4845 #[test]
4846 fn bash_case_multi_value_with_star_counts() {
4847 check_metrics::<BashParser>(
4848 "#!/bin/bash
4849f() {
4850 case \"$1\" in
4851 a|*) echo any ;;
4852 esac
4853}",
4854 "foo.sh",
4855 |metric| {
4856 // standard: unit(1) + fn(base 1 + 1 arm) = 3, max 2.
4857 // The `a|*` pattern has TWO `value` fields, so the
4858 // bare-wildcard filter (`value_count == 1`) skips it.
4859 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
4860 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
4861 },
4862 );
4863 }
4864
4865 #[test]
4866 fn bash_arithmetic_ternary_is_a_decision() {
4867 // Regression for #1268: the arithmetic ternary is the only ternary
4868 // form Bash has, and it contributed nothing to either cyclomatic
4869 // tier while every sibling with a ternary counts one. Both
4870 // arithmetic contexts are covered — the `$(( … ))` expansion and
4871 // the bare `(( … ))` statement — since the grammar admits the
4872 // construct in both and only a fixture in each shows both reach
4873 // the arm.
4874 check_metrics::<BashParser>(
4875 "#!/bin/bash
4876f() {
4877 local m=$(( a > b ? a : b ))
4878}
4879g() {
4880 (( x = a ? b : c ))
4881}",
4882 "foo.sh",
4883 |metric| {
4884 // unit(1) + f(base 1 + ternary 1) + g(base 1 + ternary 1)
4885 // = sum 5, max 2. Both tiers move: a ternary is a decision
4886 // point in the modified count too.
4887 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
4888 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
4889 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 5);
4890 },
4891 );
4892 }
4893
4894 #[test]
4895 fn bash_ternary_expression_alias_is_unreachable() {
4896 // Drift marker for the defensive `TernaryExpression2` arm (lesson
4897 // 34 / grammar-dispatch §1). The Bash enum carries two kinds
4898 // mapping to `"ternary_expression"`, but at the pinned grammar
4899 // only 223 is ever emitted — verified across every arithmetic
4900 // context the grammar admits: `$(( … ))` expansion, bare
4901 // `(( … ))` statement, a c-style `for ((…))` header, `let`, an
4902 // array subscript, an `if (( … ))` condition, and a `declare -i`
4903 // initializer. Both cyclomatic and cognitive list the alias
4904 // anyway; if a grammar bump starts emitting it, this assertion
4905 // fails loudly rather than the metric silently doubling.
4906 let src = "for ((i = a ? b : c; i < 10; i++)); do :; done\n\
4907 let \"y = a ? b : c\"\n\
4908 arr[$(( a ? b : c ))]=1\n\
4909 if (( a ? b : c )); then :; fi\n\
4910 declare -i z=$(( a ? b : c ))\n\
4911 w=$(( a ? b : c ))\n\
4912 (( v = a ? b : c ))\n";
4913 let parser = BashParser::new(
4914 src.as_bytes().to_vec(),
4915 &std::path::PathBuf::from("foo.sh"),
4916 None,
4917 );
4918 assert!(ast_has_kind_id(&parser, Bash::TernaryExpression as u16));
4919 assert!(!ast_has_kind_id(&parser, Bash::TernaryExpression2 as u16));
4920 }
4921
4922 #[test]
4923 fn bash_nested_arithmetic_ternary_counts_each_occurrence() {
4924 // Each ternary is its own decision point (#1268).
4925 check_metrics::<BashParser>(
4926 "#!/bin/bash
4927h() {
4928 local n=$(( a ? b : c ? d : e ))
4929}",
4930 "foo.sh",
4931 |metric| {
4932 // unit(1) + h(base 1 + two ternaries) = sum 4, max 3.
4933 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
4934 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
4935 },
4936 );
4937 }
4938
4939 #[test]
4940 fn bash_simple_function() {
4941 check_metrics::<BashParser>(
4942 "#!/bin/bash
4943f() {
4944 echo hello
4945}",
4946 "foo.sh",
4947 |metric| {
4948 // unit(1) + fn(base 1) = sum 2, max 1.
4949 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 2);
4950 assert_eq!(metric.cyclomatic.cyclomatic_max(), 1);
4951 insta::assert_json_snapshot!(metric.cyclomatic);
4952 },
4953 );
4954 }
4955
4956 #[test]
4957 fn kotlin_for_loop() {
4958 check_metrics::<KotlinParser>(
4959 "fun sum(n: Int): Int { // +2 (+1 unit)
4960 var s = 0
4961 for (i in 1..n) { // +1
4962 s += i
4963 }
4964 return s
4965 }",
4966 "foo.kt",
4967 |metric| {
4968 // unit(1) + fn(base 1 + for 1) = sum 3, max 2.
4969 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
4970 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
4971 insta::assert_json_snapshot!(metric.cyclomatic);
4972 },
4973 );
4974 }
4975
4976 #[test]
4977 fn kotlin_while_loop() {
4978 check_metrics::<KotlinParser>(
4979 "fun countdown(n: Int): Int { // +2 (+1 unit)
4980 var i = n
4981 while (i > 0) { // +1
4982 i--
4983 }
4984 return i
4985 }",
4986 "foo.kt",
4987 |metric| {
4988 // unit(1) + fn(base 1 + while 1) = sum 3, max 2.
4989 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
4990 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
4991 insta::assert_json_snapshot!(metric.cyclomatic);
4992 },
4993 );
4994 }
4995
4996 #[test]
4997 fn kotlin_logical_operators() {
4998 check_metrics::<KotlinParser>(
4999 "fun check(a: Boolean, b: Boolean, c: Boolean): Boolean { // +2 (+1 unit)
5000 return a && b || c // +1 &&, +1 ||
5001 }",
5002 "foo.kt",
5003 |metric| {
5004 // unit(1) + fn(base 1 + && 1 + || 1) = sum 4, max 3.
5005 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
5006 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
5007 insta::assert_json_snapshot!(metric.cyclomatic);
5008 },
5009 );
5010 }
5011
5012 #[test]
5013 fn kotlin_elvis_operator_239() {
5014 // Regression for issue #239: Kotlin's Elvis operator `?:` is a
5015 // short-circuit nullish operator analogous to JS `??` and each
5016 // occurrence is a distinct decision point, mirroring `&&` /
5017 // `||`. `a ?: b ?: c` contributes +2 to the function's
5018 // cyclomatic complexity (base 1 + two `?:` = 3).
5019 check_metrics::<KotlinParser>(
5020 "fun pick(a: String?, b: String?, c: String): String { // +2 (+1 unit)
5021 return a ?: b ?: c // +2 (two ?: short-circuits)
5022 }",
5023 "foo.kt",
5024 |metric| {
5025 // unit(1) + fn(base 1 + ?: 1 + ?: 1) = sum 4, max 3.
5026 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
5027 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
5028 insta::assert_json_snapshot!(
5029 metric.cyclomatic,
5030 @r#"
5031 {
5032 "sum": 4,
5033 "value": 1,
5034 "average": 4.0,
5035 "min": 1,
5036 "max": 3,
5037 "modified": {
5038 "sum": 4,
5039 "value": 1,
5040 "average": 4.0,
5041 "min": 1,
5042 "max": 3
5043 }
5044 }
5045 "#
5046 );
5047 },
5048 );
5049 }
5050
5051 #[test]
5052 fn kotlin_safe_navigation_436() {
5053 // Issue #436: Kotlin's safe-navigation `?.` is a short-circuit
5054 // decision point, mirroring the JS/TS/C# treatment of `?.`
5055 // (#281). Each `?.` adds +1; the chain `a?.b?.c` adds +2.
5056 check_metrics::<KotlinParser>(
5057 "fun read(a: A?): String? { // +2 (+1 unit)
5058 return a?.b?.c // +2 (two ?. short-circuits)
5059 }",
5060 "foo.kt",
5061 |metric| {
5062 // unit(1) + fn(base 1 + ?. 1 + ?. 1) = sum 4, max 3.
5063 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
5064 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
5065 // modified mirrors standard: each `?.` is both-metric.
5066 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
5067 assert_eq!(metric.cyclomatic.cyclomatic_modified_max(), 3);
5068 },
5069 );
5070 }
5071
5072 #[test]
5073 fn typescript_for_loop() {
5074 check_metrics::<TypescriptParser>(
5075 "function sum(n: number): number { // +2 (+1 unit)
5076 let s = 0;
5077 for (let i = 0; i < n; i++) { // +1
5078 s += i;
5079 }
5080 return s;
5081 }",
5082 "foo.ts",
5083 |metric| {
5084 // unit(1) + fn(base 1 + for 1) = sum 3, max 2.
5085 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
5086 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
5087 insta::assert_json_snapshot!(metric.cyclomatic);
5088 },
5089 );
5090 }
5091
5092 #[test]
5093 fn typescript_while_loop() {
5094 check_metrics::<TypescriptParser>(
5095 "function countdown(n: number): number { // +2 (+1 unit)
5096 let i = n;
5097 while (i > 0) { // +1
5098 i--;
5099 }
5100 return i;
5101 }",
5102 "foo.ts",
5103 |metric| {
5104 // unit(1) + fn(base 1 + while 1) = sum 3, max 2.
5105 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
5106 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
5107 insta::assert_json_snapshot!(metric.cyclomatic);
5108 },
5109 );
5110 }
5111
5112 #[test]
5113 fn typescript_logical_operators() {
5114 check_metrics::<TypescriptParser>(
5115 "function check(a: boolean, b: boolean, c: boolean): boolean { // +2 (+1 unit)
5116 return a && b || c; // +1 &&, +1 ||
5117 }",
5118 "foo.ts",
5119 |metric| {
5120 // unit(1) + fn(base 1 + && 1 + || 1) = sum 4, max 3.
5121 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
5122 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
5123 insta::assert_json_snapshot!(metric.cyclomatic);
5124 },
5125 );
5126 }
5127
5128 #[test]
5129 fn typescript_try_catch() {
5130 check_metrics::<TypescriptParser>(
5131 "function safe(x: number): number { // +2 (+1 unit)
5132 try {
5133 return 1 / x;
5134 } catch (e) { // +1
5135 return 0;
5136 }
5137 }",
5138 "foo.ts",
5139 |metric| {
5140 // unit(1) + fn(base 1 + catch 1) = sum 3, max 2.
5141 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
5142 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
5143 insta::assert_json_snapshot!(metric.cyclomatic);
5144 },
5145 );
5146 }
5147
5148 #[test]
5149 fn tsx_for_loop() {
5150 check_metrics::<TsxParser>(
5151 "function sum(n: number): number { // +2 (+1 unit)
5152 let s = 0;
5153 for (let i = 0; i < n; i++) { // +1
5154 s += i;
5155 }
5156 return s;
5157 }",
5158 "foo.tsx",
5159 |metric| {
5160 // unit(1) + fn(base 1 + for 1) = sum 3, max 2.
5161 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
5162 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
5163 insta::assert_json_snapshot!(metric.cyclomatic);
5164 },
5165 );
5166 }
5167
5168 #[test]
5169 fn tsx_while_loop() {
5170 check_metrics::<TsxParser>(
5171 "function countdown(n: number): number { // +2 (+1 unit)
5172 let i = n;
5173 while (i > 0) { // +1
5174 i--;
5175 }
5176 return i;
5177 }",
5178 "foo.tsx",
5179 |metric| {
5180 // unit(1) + fn(base 1 + while 1) = sum 3, max 2.
5181 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
5182 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
5183 insta::assert_json_snapshot!(metric.cyclomatic);
5184 },
5185 );
5186 }
5187
5188 #[test]
5189 fn tsx_logical_operators() {
5190 check_metrics::<TsxParser>(
5191 "function check(a: boolean, b: boolean, c: boolean): boolean { // +2 (+1 unit)
5192 return a && b || c; // +1 &&, +1 ||
5193 }",
5194 "foo.tsx",
5195 |metric| {
5196 // unit(1) + fn(base 1 + && 1 + || 1) = sum 4, max 3.
5197 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
5198 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
5199 insta::assert_json_snapshot!(metric.cyclomatic);
5200 },
5201 );
5202 }
5203
5204 #[test]
5205 fn tsx_try_catch() {
5206 check_metrics::<TsxParser>(
5207 "function safe(x: number): number { // +2 (+1 unit)
5208 try {
5209 return 1 / x;
5210 } catch (e) { // +1
5211 return 0;
5212 }
5213 }",
5214 "foo.tsx",
5215 |metric| {
5216 // unit(1) + fn(base 1 + catch 1) = sum 3, max 2.
5217 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
5218 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
5219 insta::assert_json_snapshot!(metric.cyclomatic);
5220 },
5221 );
5222 }
5223
5224 #[test]
5225 fn tsx_switch() {
5226 check_metrics::<TsxParser>(
5227 "function describe(x: number): string { // +2 (+1 unit)
5228 switch (x) {
5229 case 1: // +1
5230 return 'one';
5231 case 2: // +1
5232 return 'two';
5233 default:
5234 return 'other';
5235 }
5236 }",
5237 "foo.tsx",
5238 |metric| {
5239 // unit(1) + fn(base 1 + 2 cases) = sum 4, max 3.
5240 // default does NOT add a branch.
5241 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
5242 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
5243 insta::assert_json_snapshot!(metric.cyclomatic);
5244 },
5245 );
5246 }
5247
5248 /// Modified CCN: TSX switch with 2 cases collapses to 1.
5249 #[test]
5250 fn tsx_switch_modified() {
5251 check_metrics::<TsxParser>(
5252 "function f(x: number): string {
5253 switch (x) {
5254 case 1: return 'one';
5255 case 2: return 'two';
5256 default: return 'other';
5257 }
5258 }",
5259 "foo.tsx",
5260 |metric| {
5261 // standard: unit(1) + fn(1) + 2 cases = sum 4, max 3.
5262 // modified: unit(1) + fn(1) + switch(1) = sum 3, max 2.
5263 // default does NOT add a branch.
5264 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
5265 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
5266 insta::assert_json_snapshot!(metric.cyclomatic);
5267 },
5268 );
5269 }
5270
5271 #[test]
5272 fn php_1_level_nesting() {
5273 // Mirrors java_simple_class' if-inside-method shape:
5274 // unit (+1) + function (+1) + if (+1) + && (+1) = sum 4.
5275 check_metrics::<PhpParser>(
5276 "<?php
5277 function f(int $a, int $b): bool {
5278 if ($a > 0 && $b > 0) {
5279 return true;
5280 }
5281 return false;
5282 }",
5283 "foo.php",
5284 |metric| {
5285 insta::assert_json_snapshot!(
5286 metric.cyclomatic,
5287 @r#"
5288 {
5289 "sum": 4,
5290 "value": 1,
5291 "average": 4.0,
5292 "min": 1,
5293 "max": 3,
5294 "modified": {
5295 "sum": 4,
5296 "value": 1,
5297 "average": 4.0,
5298 "min": 1,
5299 "max": 3
5300 }
5301 }
5302 "#
5303 );
5304 },
5305 );
5306 }
5307
5308 // `case`/`cond`/`with` arms surface as `stab_clause` nodes and
5309 // contribute to standard CCN, mirroring the C-family `case:` arm
5310 // treatment — including its `default:` exclusion: the bare `_ ->`
5311 // catch-all is the construct's default arm and adds no decision
5312 // (issue #1272). The container Call (`case`) contributes once to
5313 // modified CCN, collapsing arms back to a single decision point.
5314 // Three func spaces (Unit + defmodule Class + def Function) each
5315 // seed one entry: standard = 3 entries + 2 counted stabs = 5;
5316 // modified = 3 entries + 1 case Call = 4.
5317 #[test]
5318 fn elixir_case_arms() {
5319 check_metrics::<ElixirParser>(
5320 "defmodule Foo do\n def classify(x) do\n case x do\n 1 -> :one\n 2 -> :two\n _ -> :other\n end\n end\nend\n",
5321 "foo.ex",
5322 |metric| {
5323 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
5324 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
5325 },
5326 );
5327 }
5328
5329 // A guarded wildcard (`_ when g ->`) is a real decision — the
5330 // guard can fail, so control can fall through — and must keep
5331 // counting, matching Rust's `_ if guard` rule (issue #1272).
5332 // standard = 3 entries + `1 ->` + `_ when x > 5 ->` = 5 (only the
5333 // final bare `_ ->` is excluded); modified = 3 entries + case = 4.
5334 #[test]
5335 fn elixir_case_guarded_wildcard_counts() {
5336 check_metrics::<ElixirParser>(
5337 "defmodule Foo do\n def classify(x) do\n case x do\n 1 -> :one\n _ when x > 5 -> :big\n _ -> :other\n end\n end\nend\n",
5338 "foo.ex",
5339 |metric| {
5340 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
5341 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
5342 },
5343 );
5344 }
5345
5346 // A named discard (`_x ->`) binds a value the body can read; only
5347 // the bare `_` is the default arm, matching Rust's bare-`_`-only
5348 // MatchArm rule (issue #1272). standard = 3 entries + `1 ->` +
5349 // `_x ->` = 5; modified = 3 entries + case Call = 4.
5350 #[test]
5351 fn elixir_case_named_discard_counts() {
5352 check_metrics::<ElixirParser>(
5353 "defmodule Foo do\n def classify(x) do\n case x do\n 1 -> :one\n _x -> :named\n end\n end\nend\n",
5354 "foo.ex",
5355 |metric| {
5356 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
5357 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
5358 },
5359 );
5360 }
5361
5362 // `true ->` under `case` is an ordinary boolean pattern, not a
5363 // designated default — the cond-only exclusion must not leak here
5364 // (issue #1272, grammar-dispatch §8: anchor the exclusion to the
5365 // owning construct). standard = 3 entries + `true ->` +
5366 // `false ->` = 5; modified = 3 entries + case Call = 4.
5367 #[test]
5368 fn elixir_case_true_pattern_counts() {
5369 check_metrics::<ElixirParser>(
5370 "defmodule Foo do\n def f(x) do\n case x do\n true -> :t\n false -> :f\n end\n end\nend\n",
5371 "foo.ex",
5372 |metric| {
5373 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
5374 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
5375 },
5376 );
5377 }
5378
5379 // Each short-circuit boolean (`&&`, `||`, `and`, `or`) is one
5380 // decision point — Elixir does not expose `if`/`unless` as a
5381 // distinct kind_id, so this is the only operator-driven path the
5382 // metric can see.
5383 #[test]
5384 fn elixir_logical_operators() {
5385 check_metrics::<ElixirParser>(
5386 "defmodule Foo do\n def f(x, y) do\n x and y or (x && y) || x\n end\nend\n",
5387 "foo.ex",
5388 |metric| {
5389 // 4 short-circuit ops + 3 entries (Unit, defmodule, def) = 7.
5390 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 7);
5391 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 7);
5392 },
5393 );
5394 }
5395
5396 // `try`/`rescue`/`catch` is a multi-arm container Call: the `try`
5397 // Call contributes once to modified CCN, while each rescue/catch
5398 // arm's matched pattern (a `stab_clause`) contributes once to
5399 // standard CCN. This mirrors C-family `try`/`catch` semantics.
5400 #[test]
5401 fn elixir_try_rescue() {
5402 check_metrics::<ElixirParser>(
5403 "defmodule Foo do\n def safe do\n try do\n do_it()\n rescue\n ArgumentError -> :bad\n end\n end\nend\n",
5404 "foo.ex",
5405 |metric| {
5406 // standard: 3 entries + 1 rescue stab = 4
5407 // modified: 3 entries + 1 try Call = 4
5408 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
5409 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
5410 },
5411 );
5412 }
5413
5414 // `if x do ... else ... end` surfaces as a `Call(target=if)`; the
5415 // metric inspects the source text of the call's target field to
5416 // identify it. Single-branch keyword Calls (`if`/`unless`/`for`/
5417 // `while`) contribute to both standard and modified CCN.
5418 #[test]
5419 fn elixir_if_else_counts() {
5420 check_metrics::<ElixirParser>(
5421 "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",
5422 "foo.ex",
5423 |metric| {
5424 // 1 if Call + 3 entries (Unit, defmodule Class, def Function) = 4.
5425 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
5426 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
5427 },
5428 );
5429 }
5430
5431 // `if x do ... end` without an `else` clause still surfaces as
5432 // `Call(target=if)` and is counted identically to the if/else
5433 // form — the `else` keyword is a do-block keyword argument, not
5434 // an extra `stab_clause`, so its presence does not change the
5435 // cyclomatic count.
5436 #[test]
5437 fn elixir_if_without_else_counts() {
5438 check_metrics::<ElixirParser>(
5439 "defmodule Foo do\n def f(x) do\n if x > 0 do\n :pos\n end\n end\nend\n",
5440 "foo.ex",
5441 |metric| {
5442 // 1 if Call + 3 entries = 4.
5443 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
5444 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
5445 },
5446 );
5447 }
5448
5449 // `unless x do ... end` is the negated `if`; it surfaces as
5450 // `Call(target=unless)` and is treated identically to `if`.
5451 #[test]
5452 fn elixir_unless_counts() {
5453 check_metrics::<ElixirParser>(
5454 "defmodule Foo do\n def f(x) do\n unless x > 0 do\n :nonpos\n end\n end\nend\n",
5455 "foo.ex",
5456 |metric| {
5457 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
5458 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
5459 },
5460 );
5461 }
5462
5463 // `for x <- list, do: ...` is Elixir's comprehension generator —
5464 // a `Call(target=for)`. Counts once for both standard and
5465 // modified, mirroring `if`/`unless`.
5466 #[test]
5467 fn elixir_for_comprehension_counts() {
5468 check_metrics::<ElixirParser>(
5469 "defmodule Foo do\n def f(xs) do\n for x <- xs do\n x * 2\n end\n end\nend\n",
5470 "foo.ex",
5471 |metric| {
5472 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
5473 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
5474 },
5475 );
5476 }
5477
5478 // `fn ... end` is its own function space (`get_space_kind` →
5479 // `Function`), so its cyclomatic gets its own `+1` entry path
5480 // alongside the Unit / defmodule Class / def Function entries.
5481 // The FIRST `stab_clause` is the closure's head/definition and does
5482 // NOT count (issue #776); only the 2nd+ clauses are pattern-dispatch
5483 // branches. The bare-`_` default-arm exclusion (issue #1272) does
5484 // NOT apply inside a `fn`: the head skip already grants the free
5485 // base path, so the trailing `_ ->` here is a real dispatch
5486 // decision — a two-clause fn must report the same one decision as
5487 // the identical two-arm `case`. The anon-fn itself is not a
5488 // `Call`, so it adds no modified-CCN container decision.
5489 // Standard = 4 entries (Unit, defmodule, def, anon-fn) + 1 counted
5490 // branch (`_ ->`) = 5; modified = 4 entries = 4.
5491 #[test]
5492 fn elixir_anonymous_fn_arms_count() {
5493 check_metrics::<ElixirParser>(
5494 "defmodule Foo do\n def f do\n multi = fn 0 -> :zero; _ -> :other end\n multi.(0)\n end\nend\n",
5495 "foo.ex",
5496 |metric| {
5497 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
5498 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
5499 },
5500 );
5501 }
5502
5503 // Three-clause anonymous fn ending in a catch-all: a multi-clause
5504 // `fn` is a dispatch like `case` — n clauses are n−1 decisions —
5505 // and its free base path is the head-clause skip (#776), so the
5506 // bare-`_` exclusion (issue #1272) must not stack on top of it:
5507 // when both applied, this fn contributed ZERO decisions while the
5508 // identical `case 0/1/_` contributed one per counted arm. The head
5509 // is skipped, and BOTH the `1 ->` clause and the final `_ ->`
5510 // count, matching the 2 decisions of a 3-arm `case` ending in
5511 // `_ ->` (2 counted arms there: the bare `_ ->` is free but the
5512 // container's arms 1 and 2 count). Standard = 4 entries (Unit,
5513 // defmodule, def, anon-fn) + 2 branches = 6; modified = 4.
5514 #[test]
5515 fn elixir_multi_clause_fn_catchall_composition() {
5516 check_metrics::<ElixirParser>(
5517 "defmodule Foo do\n def f do\n multi = fn 0 -> :zero; 1 -> :one; _ -> :other end\n multi.(0)\n end\nend\n",
5518 "foo.ex",
5519 |metric| {
5520 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 6);
5521 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
5522 },
5523 );
5524 }
5525
5526 // A non-head `true ->` clause of an anonymous fn is an ordinary
5527 // dispatch branch — its parent is the `anonymous_function`, not a
5528 // `cond`'s `do_block`, so the cond-default exclusion must not fire
5529 // (issue #1272). Standard = 4 entries (Unit, defmodule, def,
5530 // anon-fn) + 1 branch (the `true ->` clause) = 5; modified = 4.
5531 #[test]
5532 fn elixir_fn_true_clause_counts() {
5533 check_metrics::<ElixirParser>(
5534 "defmodule Foo do\n def f do\n flag = fn false -> :f; true -> :t end\n flag.(true)\n end\nend\n",
5535 "foo.ex",
5536 |metric| {
5537 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
5538 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
5539 },
5540 );
5541 }
5542
5543 // Regression for issue #776: a single-clause anonymous function
5544 // (`fn x -> x end`) has zero decision points — its lone
5545 // `stab_clause` is the closure head, not a branch. The closure's
5546 // own function space must therefore report cyclomatic 1 (base
5547 // entry only), matching cognitive's treatment (`cognitive.rs`
5548 // `elixir_enum_reduce_is_zero`). Before the fix the head clause
5549 // added a spurious +1, reporting 2. Standard = 4 entries (Unit,
5550 // defmodule, def, anon-fn) + 0 branches = 4; modified = 4.
5551 #[test]
5552 fn elixir_single_clause_anonymous_fn_is_not_a_branch() {
5553 check_metrics::<ElixirParser>(
5554 "defmodule Foo do\n def f do\n id = fn x -> x end\n id.(1)\n end\nend\n",
5555 "foo.ex",
5556 |metric| {
5557 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
5558 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
5559 },
5560 );
5561 }
5562
5563 // A zero-arity multi-clause `fn` is the one shape whose clauses
5564 // carry an *empty* pattern list (`()`), so neither clause has a sole
5565 // pattern for the default-arm exclusion to inspect. The head clause
5566 // is still free (#776) and the second clause is still a real
5567 // dispatch decision — "no pattern" must not be mistaken for the bare
5568 // `_ ->` catch-all, which is only free under a non-`fn` container
5569 // anyway (#1272). Standard = 4 entries (Unit, defmodule, def,
5570 // anon-fn) + 1 branch = 5; modified = 4 entries, the `fn` itself
5571 // being no container Call.
5572 #[test]
5573 fn elixir_zero_arity_multi_clause_fn_counts_second_clause() {
5574 check_metrics::<ElixirParser>(
5575 "defmodule Foo do\n def f do\n both = fn () -> :a; () -> :b end\n both.()\n end\nend\n",
5576 "foo.ex",
5577 |metric| {
5578 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
5579 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
5580 // The anonymous fn's own space carries base 1 + the one
5581 // counted clause; every enclosing space stays at base 1.
5582 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
5583 assert_eq!(metric.cyclomatic.cyclomatic_modified_max(), 1);
5584 },
5585 );
5586 }
5587
5588 // `cond do ... end` is the standard Elixir multi-way conditional.
5589 // Each clause is a `stab_clause` (standard CCN), except the
5590 // idiomatic `true ->` final arm — the construct's designated
5591 // default, the analogue of `if`/`elif`/`else`'s free `else`
5592 // (issue #1272). The `cond` Call is a multi-arm container
5593 // (modified CCN, once).
5594 #[test]
5595 fn elixir_cond_arms() {
5596 check_metrics::<ElixirParser>(
5597 "defmodule Foo do\n def f(x) do\n cond do\n x < 0 -> :neg\n x == 0 -> :zero\n true -> :pos\n end\n end\nend\n",
5598 "foo.ex",
5599 |metric| {
5600 // standard: 3 entries + 2 counted stabs (`true ->` free) = 5
5601 // modified: 3 entries + 1 cond Call = 4
5602 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
5603 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
5604 },
5605 );
5606 }
5607
5608 // A `cond` with no `true ->` default counts every arm — the
5609 // exclusion targets only the designated-default clause, not the
5610 // container's last arm (issue #1272). standard = 3 entries +
5611 // 2 stabs = 5; modified = 3 entries + 1 cond Call = 4.
5612 #[test]
5613 fn elixir_cond_without_default_counts_all_arms() {
5614 check_metrics::<ElixirParser>(
5615 "defmodule Foo do\n def f(x) do\n cond do\n x > 10 -> :big\n x > 5 -> :mid\n end\n end\nend\n",
5616 "foo.ex",
5617 |metric| {
5618 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
5619 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
5620 },
5621 );
5622 }
5623
5624 // The `cond` `true ->` exclusion is shape-based, not positional: a
5625 // NON-final unguarded `true ->` (which shadows every later arm) is
5626 // excluded exactly like the idiomatic final one. This is the
5627 // deliberate, Rust-convention-matching choice — Rust's bare-`_`
5628 // MatchArm exclusion is equally position-blind, so the sibling
5629 // family sets the precedent (issue #1272). standard = 3 entries +
5630 // 1 counted stab (`x > 5 ->`; the shadowing `true ->` is free)
5631 // = 4; modified = 3 entries + 1 cond Call = 4.
5632 #[test]
5633 fn elixir_cond_shadowing_true_arm_also_excluded() {
5634 check_metrics::<ElixirParser>(
5635 "defmodule Foo do\n def f(x) do\n cond do\n true -> :forced\n x > 5 -> :b\n end\n end\nend\n",
5636 "foo.ex",
5637 |metric| {
5638 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
5639 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
5640 },
5641 );
5642 }
5643
5644 // Nesting keeps each exclusion anchored to its own construct
5645 // (issue #1272, grammar-dispatch §8): the outer `cond`'s `true ->`
5646 // is free (cond default), the inner `case`'s `true ->` counts
5647 // (ordinary pattern — its container is the case, not the cond),
5648 // and the inner bare `_ ->` is free (case default).
5649 // standard: 3 entries + outer `x > 1 ->` + inner `true ->` = 5;
5650 // modified: 3 entries + cond Call + case Call = 5.
5651 #[test]
5652 fn elixir_nested_case_inside_cond_keeps_exclusions_scoped() {
5653 check_metrics::<ElixirParser>(
5654 "defmodule Foo do\n def f(x, y) do\n cond do\n x > 1 ->\n case y do\n true -> :t\n _ -> :o\n end\n true -> :d\n end\n end\nend\n",
5655 "foo.ex",
5656 |metric| {
5657 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
5658 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 5);
5659 },
5660 );
5661 }
5662
5663 // `with` chains use `<-` arrows, which parse as `binary_operator`
5664 // nodes — NOT `stab_clause`s — so the `with`-head clauses do not
5665 // contribute to standard CCN per-arm. The fallthrough `else`
5666 // branch, when present, contains `stab_clause`s that count for
5667 // standard. The `with` Call itself is a multi-arm container Call
5668 // that contributes once to modified CCN.
5669 #[test]
5670 fn elixir_with_else_only_counts_else_arms() {
5671 check_metrics::<ElixirParser>(
5672 "defmodule Foo do\n def f(x) do\n with {:ok, v} <- fetch(x),\n {:ok, w} <- fetch(v) do\n {:ok, w}\n else\n :error -> :nope\n other -> {:bad, other}\n end\n end\nend\n",
5673 "foo.ex",
5674 |metric| {
5675 // standard: 3 entries + 2 else-block stabs = 5
5676 // modified: 3 entries + 1 with Call = 4
5677 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
5678 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
5679 },
5680 );
5681 }
5682
5683 #[test]
5684 fn php_match_expression() {
5685 // Each `match_conditional_expression` arm (+1) but the default arm
5686 // does NOT add a branch (mirrors switch/case Java semantics).
5687 check_metrics::<PhpParser>(
5688 "<?php
5689 function color(string $c): int {
5690 return match ($c) {
5691 'red' => 1,
5692 'green' => 2,
5693 'blue' => 3,
5694 default => 0,
5695 };
5696 }",
5697 "foo.php",
5698 |metric| {
5699 // unit (+1) + function (+1) + 3 match arms (+3) = sum 5.
5700 // Default arm contributes 0.
5701 insta::assert_json_snapshot!(
5702 metric.cyclomatic,
5703 @r#"
5704 {
5705 "sum": 5,
5706 "value": 1,
5707 "average": 5.0,
5708 "min": 1,
5709 "max": 4,
5710 "modified": {
5711 "sum": 3,
5712 "value": 1,
5713 "average": 3.0,
5714 "min": 1,
5715 "max": 2
5716 }
5717 }
5718 "#
5719 );
5720 },
5721 );
5722 }
5723
5724 /// Modified CCN: PHP switch with 3 cases collapses to 1.
5725 #[test]
5726 fn php_switch_modified() {
5727 check_metrics::<PhpParser>(
5728 "<?php
5729 function describe(int $n): string {
5730 switch ($n) {
5731 case 1:
5732 return 'one';
5733 case 2:
5734 return 'two';
5735 case 3:
5736 return 'three';
5737 default:
5738 return 'other';
5739 }
5740 }",
5741 "foo.php",
5742 |metric| {
5743 // standard: unit(1) + fn(1) + 3 cases = sum 5, max 4.
5744 // modified: unit(1) + fn(1) + switch(1) = sum 3, max 2.
5745 // default does NOT add a branch.
5746 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
5747 assert_eq!(metric.cyclomatic.cyclomatic_max(), 4);
5748 insta::assert_json_snapshot!(metric.cyclomatic);
5749 },
5750 );
5751 }
5752
5753 #[test]
5754 fn php_null_coalescing() {
5755 // `??` and `??=` are each one short-circuit decision (#231).
5756 // Tree-sitter emits `??=` as the single token `QMARKQMARKEQ`, so it
5757 // is matched independently from the binary `??`.
5758 check_metrics::<PhpParser>(
5759 "<?php
5760 function pick($x, $y) {
5761 $a = $x ?? $y;
5762 $a ??= 0;
5763 return $a;
5764 }",
5765 "foo.php",
5766 |metric| {
5767 // unit (+1) + function (+1) + ?? (+1) + ??= (+1) = sum 4.
5768 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
5769 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
5770 insta::assert_json_snapshot!(
5771 metric.cyclomatic,
5772 @r#"
5773 {
5774 "sum": 4,
5775 "value": 1,
5776 "average": 4.0,
5777 "min": 1,
5778 "max": 3,
5779 "modified": {
5780 "sum": 4,
5781 "value": 1,
5782 "average": 4.0,
5783 "min": 1,
5784 "max": 3
5785 }
5786 }
5787 "#
5788 );
5789 },
5790 );
5791 }
5792
5793 #[test]
5794 fn php_nullsafe_operator_436() {
5795 // Issue #436: PHP's nullsafe operator `?->` is a short-circuit
5796 // decision point, mirroring the JS/TS/C# treatment of `?.`
5797 // (#281). The `QMARKDASHGT` token fires once per operator across
5798 // both property access (`$a?->b`) and method call (`$a?->c()`),
5799 // and once per link in a chain. Here: one access + one chained
5800 // call (`$a?->b?->c()`) = +2 for that statement.
5801 check_metrics::<PhpParser>(
5802 "<?php
5803 function read($a) {
5804 return $a?->b?->c();
5805 }",
5806 "foo.php",
5807 |metric| {
5808 // unit(1) + fn(base 1 + ?-> 1 + ?-> 1) = sum 4, max 3.
5809 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
5810 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
5811 // modified mirrors standard: each `?->` is both-metric.
5812 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
5813 assert_eq!(metric.cyclomatic.cyclomatic_modified_max(), 3);
5814 },
5815 );
5816 }
5817
5818 /// Modified CCN: nested switches contribute one decision each, not one
5819 /// total — the outer container does not absorb the inner one.
5820 #[test]
5821 fn cpp_nested_switch_modified() {
5822 check_metrics::<CppParser>(
5823 "void f() {
5824 switch (x) {
5825 case 1:
5826 switch (y) {
5827 case 10: break;
5828 case 20: break;
5829 }
5830 break;
5831 case 2: break;
5832 }
5833 }",
5834 "foo.c",
5835 |metric| {
5836 // standard: unit(1) + fn(1) + 4 cases = 6
5837 // modified: unit(1) + fn(1) + 2 switches = 4
5838 insta::assert_json_snapshot!(
5839 metric.cyclomatic,
5840 @r#"
5841 {
5842 "sum": 6,
5843 "value": 1,
5844 "average": 6.0,
5845 "min": 1,
5846 "max": 5,
5847 "modified": {
5848 "sum": 4,
5849 "value": 1,
5850 "average": 4.0,
5851 "min": 1,
5852 "max": 3
5853 }
5854 }
5855 "#
5856 );
5857 },
5858 );
5859 }
5860
5861 /// Modified CCN: nested Rust matches each contribute one container.
5862 /// Bare `_ =>` arms are skipped.
5863 #[test]
5864 fn rust_nested_match_modified() {
5865 check_metrics::<RustParser>(
5866 "fn f(x: u8) -> u8 {
5867 match x {
5868 1 => match x {
5869 10 => 1,
5870 20 => 2,
5871 _ => 0,
5872 },
5873 _ => 0,
5874 }
5875 }",
5876 "foo.rs",
5877 |metric| {
5878 // standard: unit(1) + fn(1) + 3 arms (1,10,20; both _ skipped) = 5
5879 // modified: unit(1) + fn(1) + 2 matches = 4
5880 insta::assert_json_snapshot!(
5881 metric.cyclomatic,
5882 @r#"
5883 {
5884 "sum": 5,
5885 "value": 1,
5886 "average": 5.0,
5887 "min": 1,
5888 "max": 4,
5889 "modified": {
5890 "sum": 4,
5891 "value": 1,
5892 "average": 4.0,
5893 "min": 1,
5894 "max": 3
5895 }
5896 }
5897 "#
5898 );
5899 },
5900 );
5901 }
5902
5903 /// Pin the empty-switch edge case: standard counts no arms (0) while
5904 /// modified still counts the container (+1) per Lizard's `-m`.
5905 #[test]
5906 fn cpp_empty_switch_modified() {
5907 check_metrics::<CppParser>("void f() { switch (x) {} }", "foo.c", |metric| {
5908 // standard: unit(1) + fn(1) + 0 cases = 2
5909 // modified: unit(1) + fn(1) + 1 switch = 3
5910 insta::assert_json_snapshot!(
5911 metric.cyclomatic,
5912 @r#"
5913 {
5914 "sum": 2,
5915 "value": 1,
5916 "average": 2.0,
5917 "min": 1,
5918 "max": 1,
5919 "modified": {
5920 "sum": 3,
5921 "value": 1,
5922 "average": 3.0,
5923 "min": 1,
5924 "max": 2
5925 }
5926 }
5927 "#
5928 );
5929 });
5930 }
5931
5932 /// Two nested `for` loops contribute +1 each on top of the function and
5933 /// unit decisions. No condition expressions, so `&&` / `||` do not fire.
5934 #[test]
5935 fn c_nested_loops() {
5936 check_metrics::<CParser>(
5937 "void f() {
5938 for (int i = 0; i < 10; ++i) { // +1
5939 for (int j = 0; j < 10; ++j) { // +1
5940 g(i, j);
5941 }
5942 }
5943 }",
5944 "foo.c",
5945 |metric| {
5946 // standard: unit(1) + fn(1) + 2 for = 4
5947 // modified: identical (no switch container, no extra arms)
5948 let s = &metric.cyclomatic;
5949 assert_eq!(s.cyclomatic_sum(), 4);
5950 assert_eq!(s.cyclomatic_max(), 3);
5951 assert_eq!(s.cyclomatic_modified_sum(), 4);
5952 insta::assert_json_snapshot!(
5953 metric.cyclomatic,
5954 @r#"
5955 {
5956 "sum": 4,
5957 "value": 1,
5958 "average": 4.0,
5959 "min": 1,
5960 "max": 3,
5961 "modified": {
5962 "sum": 4,
5963 "value": 1,
5964 "average": 4.0,
5965 "min": 1,
5966 "max": 3
5967 }
5968 }
5969 "#
5970 );
5971 },
5972 );
5973 }
5974
5975 /// C++ `do { … } while (…)` contributes exactly +1 to both
5976 /// standard and modified CCN. The +1 comes from the `while`
5977 /// keyword token inside the do-statement (`Cpp::While`), which the
5978 /// C-family macro already counts. Adding the `DoStatement`
5979 /// statement node would double-count — see the macro doc comment
5980 /// and issue #284. This test pins the correct keyword-driven
5981 /// count.
5982 #[test]
5983 fn cpp_do_statement_counts_in_cyclomatic() {
5984 check_metrics::<CppParser>(
5985 "void f() {
5986 int i = 0;
5987 do { // +1 (via inner `while` keyword)
5988 ++i;
5989 } while (i < 10);
5990 }",
5991 "foo.cpp",
5992 |metric| {
5993 // standard: unit(1) + fn(1) + do(1) = 3
5994 // modified: identical (no switch, no extra arms)
5995 let s = &metric.cyclomatic;
5996 assert_eq!(s.cyclomatic_sum(), 3);
5997 assert_eq!(s.cyclomatic_max(), 2);
5998 assert_eq!(s.cyclomatic_modified_sum(), 3);
5999 insta::assert_json_snapshot!(
6000 metric.cyclomatic,
6001 @r#"
6002 {
6003 "sum": 3,
6004 "value": 1,
6005 "average": 3.0,
6006 "min": 1,
6007 "max": 2,
6008 "modified": {
6009 "sum": 3,
6010 "value": 1,
6011 "average": 3.0,
6012 "min": 1,
6013 "max": 2
6014 }
6015 }
6016 "#
6017 );
6018 },
6019 );
6020 }
6021
6022 /// C++ range-based `for (auto x : xs)` contributes exactly +1 to
6023 /// both standard and modified CCN — the `for` keyword token
6024 /// (`Cpp::For`) fires inside the `ForRangeLoop` node just like
6025 /// inside a classic `ForStatement`. Pinning this prevents
6026 /// reintroducing the double-count from issue #284's incorrect fix
6027 /// proposal.
6028 #[test]
6029 fn cpp_for_range_loop_counts_in_cyclomatic() {
6030 check_metrics::<CppParser>(
6031 "void f(std::vector<int> xs) {
6032 for (auto x : xs) { // +1 (via `for` keyword)
6033 g(x);
6034 }
6035 }",
6036 "foo.cpp",
6037 |metric| {
6038 // standard: unit(1) + fn(1) + for-range(1) = 3
6039 let s = &metric.cyclomatic;
6040 assert_eq!(s.cyclomatic_sum(), 3);
6041 assert_eq!(s.cyclomatic_max(), 2);
6042 assert_eq!(s.cyclomatic_modified_sum(), 3);
6043 insta::assert_json_snapshot!(
6044 metric.cyclomatic,
6045 @r#"
6046 {
6047 "sum": 3,
6048 "value": 1,
6049 "average": 3.0,
6050 "min": 1,
6051 "max": 2,
6052 "modified": {
6053 "sum": 3,
6054 "value": 1,
6055 "average": 3.0,
6056 "min": 1,
6057 "max": 2
6058 }
6059 }
6060 "#
6061 );
6062 },
6063 );
6064 }
6065
6066 /// Decision kinds through the dedicated `LANG::C` grammar (#721):
6067 /// `if`, `for`, `while`, `case`, and the `&&` short-circuit each
6068 /// add +1; `switch` adds only to the modified count. C has no
6069 /// `catch`, so the hand-written `Cyclomatic for CCode` impl omits
6070 /// the exception arm the C++ macro carries.
6071 #[test]
6072 fn c_grammar_decision_kinds_count_in_cyclomatic() {
6073 check_metrics::<CParser>(
6074 "int f(int a, int b) {
6075 if (a && b) { // +1 if, +1 &&
6076 return 1;
6077 }
6078 for (int i = 0; i < a; ++i) { // +1 for
6079 b += i;
6080 }
6081 switch (b) { // +1 modified only
6082 case 0: return 0; // +1 case
6083 default: return b;
6084 }
6085 }",
6086 "foo.c",
6087 |metric| {
6088 let s = &metric.cyclomatic;
6089 // standard: unit(1) + fn(1) + if(1) + &&(1) + for(1) + case(1) = 6
6090 assert_eq!(s.cyclomatic_sum(), 6);
6091 // modified: `case` adds to standard only and `switch` to
6092 // modified only, so they balance — base(2) + if + && + for
6093 // + switch(1) = 6.
6094 assert_eq!(s.cyclomatic_modified_sum(), 6);
6095 },
6096 );
6097 }
6098
6099 /// `?:` ternary is matched by `Cpp::ConditionalExpression` in the
6100 /// C-family macro and contributes +1 standard *and* +1 modified.
6101 /// Two nested ternaries in one expression therefore add 2 to each.
6102 #[test]
6103 fn c_ternary_chain() {
6104 check_metrics::<CParser>(
6105 "int f(int a, int b, int c) {
6106 return a > 0 ? a : (b > 0 ? b : c); // +2 ternaries (?: each)
6107 }",
6108 "foo.c",
6109 |metric| {
6110 // standard: unit(1) + fn(1) + 2 ?: = 4
6111 let s = &metric.cyclomatic;
6112 assert_eq!(s.cyclomatic_sum(), 4);
6113 assert_eq!(s.cyclomatic_max(), 3);
6114 assert_eq!(s.cyclomatic_modified_sum(), 4);
6115 insta::assert_json_snapshot!(
6116 metric.cyclomatic,
6117 @r#"
6118 {
6119 "sum": 4,
6120 "value": 1,
6121 "average": 4.0,
6122 "min": 1,
6123 "max": 3,
6124 "modified": {
6125 "sum": 4,
6126 "value": 1,
6127 "average": 4.0,
6128 "min": 1,
6129 "max": 3
6130 }
6131 }
6132 "#
6133 );
6134 },
6135 );
6136 }
6137
6138 /// Short-circuit `&&` / `||` chains each contribute +1 — every binary
6139 /// operator token in the chain is a separate decision (Lizard parity).
6140 #[test]
6141 fn c_short_circuit_chain() {
6142 check_metrics::<CParser>(
6143 "int f(int a, int b, int c, int d) {
6144 if (a && b || c && d) { // 3 logical ops + 1 if = 4
6145 return 1;
6146 }
6147 return 0;
6148 }",
6149 "foo.c",
6150 |metric| {
6151 // standard: unit(1) + fn(1) + if(1) + && (2) + || (1) = 6
6152 let s = &metric.cyclomatic;
6153 assert_eq!(s.cyclomatic_sum(), 6);
6154 assert_eq!(s.cyclomatic_max(), 5);
6155 assert_eq!(s.cyclomatic_modified_sum(), 6);
6156 insta::assert_json_snapshot!(
6157 metric.cyclomatic,
6158 @r#"
6159 {
6160 "sum": 6,
6161 "value": 1,
6162 "average": 6.0,
6163 "min": 1,
6164 "max": 5,
6165 "modified": {
6166 "sum": 6,
6167 "value": 1,
6168 "average": 6.0,
6169 "min": 1,
6170 "max": 5
6171 }
6172 }
6173 "#
6174 );
6175 },
6176 );
6177 }
6178
6179 /// Switch with intentional fall-through: every `case` adds +1 standard
6180 /// regardless of whether the arm `break`s. Modified collapses all three
6181 /// arms into one switch container.
6182 #[test]
6183 fn c_switch_fallthrough() {
6184 check_metrics::<CParser>(
6185 "int f(int x) {
6186 int r = 0;
6187 switch (x) {
6188 case 1: // +1
6189 case 2: // +1
6190 r = 10;
6191 break;
6192 case 3: // +1
6193 r = 20;
6194 break;
6195 }
6196 return r;
6197 }",
6198 "foo.c",
6199 |metric| {
6200 // standard: unit(1) + fn(1) + 3 cases = 5
6201 // modified: unit(1) + fn(1) + 1 switch container = 3
6202 let s = &metric.cyclomatic;
6203 assert_eq!(s.cyclomatic_sum(), 5);
6204 assert_eq!(s.cyclomatic_modified_sum(), 3);
6205 assert!(s.cyclomatic_modified_sum() < s.cyclomatic_sum());
6206 insta::assert_json_snapshot!(
6207 metric.cyclomatic,
6208 @r#"
6209 {
6210 "sum": 5,
6211 "value": 1,
6212 "average": 5.0,
6213 "min": 1,
6214 "max": 4,
6215 "modified": {
6216 "sum": 3,
6217 "value": 1,
6218 "average": 3.0,
6219 "min": 1,
6220 "max": 2
6221 }
6222 }
6223 "#
6224 );
6225 },
6226 );
6227 }
6228
6229 /// `goto` is not a recognised decision keyword in the C-family macro
6230 /// (only `If | For | While | Catch | ConditionalExpression | && | ||`
6231 /// add complexity, plus `Case` / `SwitchStatement`). The label and the
6232 /// `goto` jump are control-flow, but the metric deliberately mirrors
6233 /// Lizard, which also does not count `goto`. This test pins that
6234 /// decision so a future change that adds `Cpp::GotoStatement` to the
6235 /// macro fires here first.
6236 #[test]
6237 fn c_goto_not_counted() {
6238 check_metrics::<CParser>(
6239 "int f(int n) {
6240 int i = 0;
6241 retry:
6242 if (i < n) { // +1
6243 ++i;
6244 goto retry; // ignored
6245 }
6246 return i;
6247 }",
6248 "foo.c",
6249 |metric| {
6250 // standard: unit(1) + fn(1) + if(1) = 3
6251 // goto/label add nothing.
6252 let s = &metric.cyclomatic;
6253 assert_eq!(s.cyclomatic_sum(), 3);
6254 assert_eq!(s.cyclomatic_modified_sum(), 3);
6255 insta::assert_json_snapshot!(
6256 metric.cyclomatic,
6257 @r#"
6258 {
6259 "sum": 3,
6260 "value": 1,
6261 "average": 3.0,
6262 "min": 1,
6263 "max": 2,
6264 "modified": {
6265 "sum": 3,
6266 "value": 1,
6267 "average": 3.0,
6268 "min": 1,
6269 "max": 2
6270 }
6271 }
6272 "#
6273 );
6274 },
6275 );
6276 }
6277
6278 /// Direct accessor coverage: assert the modified-CCN getters return
6279 /// the values we expect from a known fixture, bypassing the JSON
6280 /// serializer. Modified must never exceed standard for non-degenerate
6281 /// inputs (a switch with at least one arm).
6282 #[test]
6283 fn cyclomatic_modified_accessors() {
6284 check_metrics::<RustParser>(
6285 "fn f(x: u8) -> u8 {
6286 match x {
6287 1 => 1,
6288 2 => 2,
6289 _ => 0,
6290 }
6291 }",
6292 "foo.rs",
6293 |metric| {
6294 // standard sum: unit(1) + fn(1 + 2 arms, _ skipped) = 4
6295 // modified sum: unit(1) + fn(1 + 1 MatchExpr) = 3
6296 let s = &metric.cyclomatic;
6297 assert_eq!(s.cyclomatic_modified_sum(), 3);
6298 assert_eq!(s.cyclomatic_modified_min(), 1);
6299 assert_eq!(s.cyclomatic_modified_max(), 2);
6300 // #512: divisor is the single function space, not the two
6301 // total spaces (unit + fn), so 3 / 1 = 3.0 (was 3 / 2 = 1.5).
6302 assert_eq!(s.cyclomatic_modified_average(), 3.0);
6303 assert!(s.cyclomatic_modified_sum() <= s.cyclomatic_sum());
6304 },
6305 );
6306 }
6307
6308 /// Bare `_ =>` wildcard is not counted (matches C-family `default:`).
6309 #[test]
6310 fn rust_wildcard_only_match() {
6311 check_metrics::<RustParser>(
6312 "fn f(x: u8) -> &'static str {
6313 match x {
6314 _ => \"fallback\",
6315 }
6316 }",
6317 "foo.rs",
6318 |metric| {
6319 // standard: unit(1) + fn(1) + 0 arms (bare wildcard skipped) = 2
6320 // modified: unit(1) + fn(1) + MatchExpr(1) = 3
6321 insta::assert_json_snapshot!(
6322 metric.cyclomatic,
6323 @r#"
6324 {
6325 "sum": 2,
6326 "value": 1,
6327 "average": 2.0,
6328 "min": 1,
6329 "max": 1,
6330 "modified": {
6331 "sum": 3,
6332 "value": 1,
6333 "average": 3.0,
6334 "min": 1,
6335 "max": 2
6336 }
6337 }
6338 "#
6339 );
6340 },
6341 );
6342 }
6343
6344 /// Wildcard arm plus explicit arms: only explicit arms count.
6345 #[test]
6346 fn rust_wildcard_plus_explicit_arms() {
6347 check_metrics::<RustParser>(
6348 "fn f(x: u8) -> &'static str {
6349 match x {
6350 1 => \"one\",
6351 2 => \"two\",
6352 3 => \"three\",
6353 _ => \"other\",
6354 }
6355 }",
6356 "foo.rs",
6357 |metric| {
6358 // standard: unit(1) + fn(1) + 3 arms (1,2,3) = 5
6359 // modified: unit(1) + fn(1) + MatchExpr(1) = 3
6360 insta::assert_json_snapshot!(
6361 metric.cyclomatic,
6362 @r#"
6363 {
6364 "sum": 5,
6365 "value": 1,
6366 "average": 5.0,
6367 "min": 1,
6368 "max": 4,
6369 "modified": {
6370 "sum": 3,
6371 "value": 1,
6372 "average": 3.0,
6373 "min": 1,
6374 "max": 2
6375 }
6376 }
6377 "#
6378 );
6379 },
6380 );
6381 }
6382
6383 /// `Some(_)` is NOT a bare wildcard — still counts.
6384 #[test]
6385 fn rust_some_wildcard_still_counts() {
6386 check_metrics::<RustParser>(
6387 "fn f(x: Option<u8>) -> u8 {
6388 match x {
6389 Some(_) => 1,
6390 None => 0,
6391 }
6392 }",
6393 "foo.rs",
6394 |metric| {
6395 // standard: unit(1) + fn(1) + 2 arms (Some(_), None) = 4
6396 // modified: unit(1) + fn(1) + MatchExpr(1) = 3
6397 insta::assert_json_snapshot!(
6398 metric.cyclomatic,
6399 @r#"
6400 {
6401 "sum": 4,
6402 "value": 1,
6403 "average": 4.0,
6404 "min": 1,
6405 "max": 3,
6406 "modified": {
6407 "sum": 3,
6408 "value": 1,
6409 "average": 3.0,
6410 "min": 1,
6411 "max": 2
6412 }
6413 }
6414 "#
6415 );
6416 },
6417 );
6418 }
6419
6420 /// Tuple pattern `(_, x)` is NOT a bare wildcard — still counts.
6421 #[test]
6422 fn rust_tuple_wildcard_still_counts() {
6423 check_metrics::<RustParser>(
6424 "fn f(x: (u8, u8)) -> u8 {
6425 match x {
6426 (0, y) => y,
6427 (_, y) => y + 1,
6428 }
6429 }",
6430 "foo.rs",
6431 |metric| {
6432 // standard: unit(1) + fn(1) + 2 arms = 4
6433 // modified: unit(1) + fn(1) + MatchExpr(1) = 3
6434 insta::assert_json_snapshot!(
6435 metric.cyclomatic,
6436 @r#"
6437 {
6438 "sum": 4,
6439 "value": 1,
6440 "average": 4.0,
6441 "min": 1,
6442 "max": 3,
6443 "modified": {
6444 "sum": 3,
6445 "value": 1,
6446 "average": 3.0,
6447 "min": 1,
6448 "max": 2
6449 }
6450 }
6451 "#
6452 );
6453 },
6454 );
6455 }
6456
6457 /// `_ if guard` is NOT a bare wildcard — still counts.
6458 /// The `if` keyword inside the guard also contributes +1 standard/modified.
6459 #[test]
6460 fn rust_guarded_wildcard_still_counts() {
6461 check_metrics::<RustParser>(
6462 "fn f(x: u8) -> &'static str {
6463 match x {
6464 1 => \"one\",
6465 _ if x > 100 => \"big\",
6466 _ => \"other\",
6467 }
6468 }",
6469 "foo.rs",
6470 |metric| {
6471 // standard: unit(1) + fn(1 + arm(1) + guarded_arm(1) + if_kw(1)) = 5
6472 // modified: unit(1) + fn(1 + MatchExpr(1) + if_kw(1)) = 4
6473 insta::assert_json_snapshot!(
6474 metric.cyclomatic,
6475 @r#"
6476 {
6477 "sum": 5,
6478 "value": 1,
6479 "average": 5.0,
6480 "min": 1,
6481 "max": 4,
6482 "modified": {
6483 "sum": 4,
6484 "value": 1,
6485 "average": 4.0,
6486 "min": 1,
6487 "max": 3
6488 }
6489 }
6490 "#
6491 );
6492 },
6493 );
6494 }
6495
6496 /// Regression #107: empty case…esac has no arms, so standard adds 0 and
6497 /// modified adds 1 (the container).
6498 #[test]
6499 fn bash_case_empty() {
6500 check_metrics::<BashParser>(
6501 "#!/bin/bash
6502f() {
6503 case $1 in
6504 esac
6505}",
6506 "foo.sh",
6507 |metric| {
6508 // standard: unit(1) + fn(1) + 0 arms = 2
6509 // modified: unit(1) + fn(1) + case_stmt(1) = 3
6510 insta::assert_json_snapshot!(
6511 metric.cyclomatic,
6512 @r#"
6513 {
6514 "sum": 2,
6515 "value": 1,
6516 "average": 2.0,
6517 "min": 1,
6518 "max": 1,
6519 "modified": {
6520 "sum": 3,
6521 "value": 1,
6522 "average": 3.0,
6523 "min": 1,
6524 "max": 2
6525 }
6526 }
6527 "#
6528 );
6529 },
6530 );
6531 }
6532
6533 /// Regression #107: nested case…esac — each container contributes to
6534 /// modified independently, and each arm contributes to standard.
6535 #[test]
6536 fn bash_nested_case() {
6537 check_metrics::<BashParser>(
6538 "#!/bin/bash
6539f() {
6540 case $1 in
6541 a)
6542 case $2 in
6543 x) echo ax ;;
6544 y) echo ay ;;
6545 esac
6546 ;;
6547 b) echo b ;;
6548 esac
6549}",
6550 "foo.sh",
6551 |metric| {
6552 // standard: unit(1) + fn(1) + outer arms(a,b = 2) + inner arms(x,y = 2) = 6
6553 // modified: unit(1) + fn(1) + 2 case_stmts = 4
6554 insta::assert_json_snapshot!(
6555 metric.cyclomatic,
6556 @r#"
6557 {
6558 "sum": 6,
6559 "value": 1,
6560 "average": 6.0,
6561 "min": 1,
6562 "max": 5,
6563 "modified": {
6564 "sum": 4,
6565 "value": 1,
6566 "average": 4.0,
6567 "min": 1,
6568 "max": 3
6569 }
6570 }
6571 "#
6572 );
6573 },
6574 );
6575 }
6576
6577 /// Nested matches with wildcards: only bare `_` skipped at each level.
6578 #[test]
6579 fn rust_nested_match_with_wildcards() {
6580 check_metrics::<RustParser>(
6581 "fn f(x: u8, y: u8) -> &'static str {
6582 match x {
6583 1 => match y {
6584 1 => \"one-one\",
6585 _ => \"one-other\",
6586 },
6587 _ => \"other\",
6588 }
6589 }",
6590 "foo.rs",
6591 |metric| {
6592 // standard: unit(1) + fn(1) + outer arm 1(+1) + inner arm 1(+1)
6593 // + outer bare _(0) + inner bare _(0) = 4
6594 // modified: unit(1) + fn(1) + 2 MatchExpr(+2) = 4
6595 insta::assert_json_snapshot!(
6596 metric.cyclomatic,
6597 @r#"
6598 {
6599 "sum": 4,
6600 "value": 1,
6601 "average": 4.0,
6602 "min": 1,
6603 "max": 3,
6604 "modified": {
6605 "sum": 4,
6606 "value": 1,
6607 "average": 4.0,
6608 "min": 1,
6609 "max": 3
6610 }
6611 }
6612 "#
6613 );
6614 },
6615 );
6616 }
6617
6618 #[test]
6619 fn ruby_nested_branches() {
6620 // expected: unit(1) + method(1 + `if` + `while`) = 1 + 3 = 4
6621 // standard CCN.
6622 check_metrics::<RubyParser>(
6623 "def foo(a)\n if a > 0\n while a > 0\n a -= 1\n end\n end\nend\n",
6624 "foo.rb",
6625 |metric| {
6626 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
6627 insta::assert_json_snapshot!(metric.cyclomatic);
6628 },
6629 );
6630 }
6631
6632 #[test]
6633 fn ruby_case_when_arms() {
6634 // Each `when` arm adds standard CCN; the `case` container is
6635 // counted ONCE in modified CCN.
6636 // expected: standard = unit(1) + method(1 + 3 when) = 5;
6637 // modified = unit(1) + method(1 + 1 case) = 3.
6638 check_metrics::<RubyParser>(
6639 "def foo(x)\n case x\n when 1 then 'one'\n when 2 then 'two'\n when 3 then 'three'\n end\nend\n",
6640 "foo.rb",
6641 |metric| {
6642 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
6643 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 3);
6644 insta::assert_json_snapshot!(metric.cyclomatic);
6645 },
6646 );
6647 }
6648
6649 #[test]
6650 fn ruby_case_match_default_only_arm_not_counted() {
6651 // Regression for #977: a `case … in` whose only arm is the bare
6652 // wildcard `in _` (no guard) is a default-only match and must add
6653 // NO standard decision — mirroring Rust's bare-`_` `MatchArm` and
6654 // Python's `case _:` filters. The `case_match` container still
6655 // contributes one modified decision.
6656 // expected per function: standard = 1 (base) + 0 = 1;
6657 // modified = 1 (base) + 1 (case_match) = 2.
6658 check_metrics::<RubyParser>(
6659 "def f(x)\n case x\n in _ then :default\n end\nend\n",
6660 "foo.rb",
6661 |metric| {
6662 assert_eq!(metric.cyclomatic.cyclomatic_max(), 1);
6663 assert_eq!(metric.cyclomatic.cyclomatic_modified_max(), 2);
6664 },
6665 );
6666 }
6667
6668 #[test]
6669 fn ruby_case_match_in_arms_and_guard_counted() {
6670 // Regression for #977: a non-wildcard `in 1` arm and a guarded
6671 // wildcard `in _ if x > 0` arm each add one standard decision,
6672 // while the trailing bare `in _` default arm adds none. The
6673 // `case_match` container stays a modified-only decision.
6674 // expected per function: standard = 1 (base) + `in 1` + `in _ if`
6675 // = 3; modified = 1 (base) + 1 (case_match) = 2.
6676 check_metrics::<RubyParser>(
6677 "def f(x)\n case x\n in 1 then :one\n in _ if x > 0 then :positive\n in _ then :default\n end\nend\n",
6678 "foo.rb",
6679 |metric| {
6680 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
6681 assert_eq!(metric.cyclomatic.cyclomatic_modified_max(), 2);
6682 },
6683 );
6684 }
6685
6686 /// Cross-language parity for default-arm filtering (#977): a
6687 /// match/switch whose single arm is the bare wildcard must score the
6688 /// same per-function cyclomatic across Ruby `case … in`, Rust `match`,
6689 /// and Python `match`. Each language's catch-all arm is its
6690 /// `default:`-equivalent and adds no standard decision, so every
6691 /// function is just its base 1. Per-language snapshot suites pin each
6692 /// history but cannot catch the cross-language disagreement this
6693 /// guards (lesson 11; #106 was exactly a wildcard-counting drift).
6694 #[test]
6695 fn cyclomatic_bare_wildcard_default_arm_cross_language() {
6696 check_metrics::<RubyParser>(
6697 "def f(x)\n case x\n in _ then :default\n end\nend\n",
6698 "foo.rb",
6699 |m| assert_eq!(m.cyclomatic.cyclomatic_max(), 1, "ruby"),
6700 );
6701 check_metrics::<RustParser>(
6702 "fn f(x: i32) -> i32 {\n match x {\n _ => 0,\n }\n}\n",
6703 "foo.rs",
6704 |m| assert_eq!(m.cyclomatic.cyclomatic_max(), 1, "rust"),
6705 );
6706 check_metrics::<PythonParser>(
6707 "def f(x):\n match x:\n case _:\n return 0\n",
6708 "foo.py",
6709 |m| assert_eq!(m.cyclomatic.cyclomatic_max(), 1, "python"),
6710 );
6711 }
6712
6713 #[test]
6714 fn ruby_ternary_conditional() {
6715 // Ruby's `cond ? a : b` parses as `Conditional` and counts as a
6716 // branch in both standard and modified CCN.
6717 // expected: standard = unit(1) + method(1 + 1) = 3.
6718 check_metrics::<RubyParser>(
6719 "def foo(x)\n x.positive? ? :pos : :nonpos\nend\n",
6720 "foo.rb",
6721 |metric| {
6722 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
6723 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 3);
6724 },
6725 );
6726 }
6727
6728 #[test]
6729 fn ruby_and_or_keywords() {
6730 // Word-form `and` / `or` are distinct grammar kinds from
6731 // `&&` / `||` and must each contribute one decision point.
6732 // expected: standard = unit(1) + method(1 + and + or) = 4.
6733 check_metrics::<RubyParser>(
6734 "def foo(a, b, c)\n a and b or c\nend\n",
6735 "foo.rb",
6736 |metric| {
6737 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
6738 },
6739 );
6740 }
6741
6742 /// Cross-language parity for cyclomatic: an `if/else if/else` chain
6743 /// of three arms must produce the same per-function (max-space)
6744 /// cyclomatic score across Ruby, Rust, and Java. Per-language
6745 /// snapshot tests pin each language's history but cannot detect
6746 /// drift on the same logical construct — lesson 11
6747 /// (`docs/development/lessons_learned.md`) catalogues real
6748 /// incidents (#106 Rust-vs-C-family wildcard counting; #107 Bash
6749 /// double-counting case containers) that survived per-language
6750 /// suites for years. `cyclomatic_max()` is the function-level
6751 /// cyclomatic and is independent of unit/class space stacking, so
6752 /// the comparison is meaningful across languages with different
6753 /// space hierarchies.
6754 ///
6755 /// Expected per function: 1 (base) + 1 (`if`) + 1 (`else if`) = 3.
6756 /// The `else` arm is unconditional and does not contribute. Each
6757 /// language asserts the literal 3.0 in its own closure so a future
6758 /// drift in any single language fails THIS test (and only this
6759 /// test), making cross-language disagreement visible at a glance.
6760 #[test]
6761 fn cyclomatic_if_elseif_else_chain_cross_language() {
6762 check_metrics::<RubyParser>(
6763 "def classify(x)\n if x > 0\n :pos\n elsif x < 0\n :neg\n else\n :zero\n end\nend\n",
6764 "foo.rb",
6765 |m| {
6766 assert_eq!(m.cyclomatic.cyclomatic_max(), 3, "ruby");
6767 },
6768 );
6769 check_metrics::<RustParser>(
6770 "fn classify(x: i32) -> &'static str {\n if x > 0 { \"pos\" } else if x < 0 { \"neg\" } else { \"zero\" }\n}\n",
6771 "foo.rs",
6772 |m| {
6773 assert_eq!(m.cyclomatic.cyclomatic_max(), 3, "rust");
6774 },
6775 );
6776 check_metrics::<JavaParser>(
6777 "class C {\n String classify(int x) {\n if (x > 0) return \"pos\";\n else if (x < 0) return \"neg\";\n else return \"zero\";\n }\n}\n",
6778 "Foo.java",
6779 |m| {
6780 assert_eq!(m.cyclomatic.cyclomatic_max(), 3, "java");
6781 },
6782 );
6783 }
6784
6785 /// Parity gate for the `impl_cyclomatic_java_like!` macro (#300):
6786 /// every decision kind shared by Java and Groovy must produce the
6787 /// same per-function cyclomatic score for a common decision-rich
6788 /// method body. Dropping a kind from the macro body (e.g.,
6789 /// removing `For` or `TernaryExpression`) would fail BOTH language
6790 /// assertions; dropping a kind from only one invocation would fail
6791 /// only that language's assertion.
6792 ///
6793 /// The body intentionally exercises every shared kind:
6794 /// `If`, `For`, `While`, `Catch`, `TernaryExpression`, `AMPAMP`,
6795 /// `PIPEPIPE`, plus a `switch` with two `Case` arms (one is the
6796 /// default and contributes nothing under standard CCN). Expected
6797 /// per-function: 1 (base) + if + for + while + catch + ternary +
6798 /// && + || + 2 cases = 10 (standard).
6799 ///
6800 /// Modified CCN is asserted in parallel: the multi-kind arm
6801 /// bumps both counters, and `Switch` (one keyword token per
6802 /// switch construct) replaces the standard CCN's two `Case`
6803 /// arms. Expected modified per-function: 1 (base) + if + for +
6804 /// while + catch + ternary + && + || + switch = 9. Without the
6805 /// modified assertion a mutation that drops
6806 /// `stats.cyclomatic_modified += 1.` from any shared arm (or
6807 /// drops the `Switch` arm entirely) would pass.
6808 #[test]
6809 fn cyclomatic_java_groovy_parity_300() {
6810 const JAVA_SRC: &str = "class C {\n\
6811 int decide(int x, int y, int[] xs) {\n\
6812 int r = 0;\n\
6813 if (x > 0 && y > 0) r = 1;\n\
6814 for (int i = 0; i < 3; i++) r++;\n\
6815 while (x > 0) { x--; r++; }\n\
6816 try { r += xs[0]; } catch (Exception e) { r = -1; }\n\
6817 r = (x > 0 || y < 0) ? r : -r;\n\
6818 switch (x) { case 1: r++; break; case 2: r--; break; default: break; }\n\
6819 return r;\n\
6820 }\n\
6821 }\n";
6822 const GROOVY_SRC: &str = "class C {\n\
6823 int decide(int x, int y, int[] xs) {\n\
6824 int r = 0\n\
6825 if (x > 0 && y > 0) r = 1\n\
6826 for (int i = 0; i < 3; i++) r++\n\
6827 while (x > 0) { x--; r++ }\n\
6828 try { r += xs[0] } catch (Exception e) { r = -1 }\n\
6829 r = (x > 0 || y < 0) ? r : -r\n\
6830 switch (x) { case 1: r++; break; case 2: r--; break; default: break }\n\
6831 return r\n\
6832 }\n\
6833 }\n";
6834 check_metrics::<JavaParser>(JAVA_SRC, "Foo.java", |m| {
6835 assert_eq!(m.cyclomatic.cyclomatic_max(), 10, "java parity");
6836 assert_eq!(
6837 m.cyclomatic.cyclomatic_modified_max(),
6838 9,
6839 "java modified parity"
6840 );
6841 });
6842 check_metrics::<GroovyParser>(GROOVY_SRC, "foo.groovy", |m| {
6843 assert_eq!(m.cyclomatic.cyclomatic_max(), 10, "groovy parity");
6844 assert_eq!(
6845 m.cyclomatic.cyclomatic_modified_max(),
6846 9,
6847 "groovy modified parity"
6848 );
6849 });
6850 }
6851
6852 /// Groovy-only delta in `impl_cyclomatic_java_like!`: the `Assert`
6853 /// extra-kind invocation must keep Groovy's `assert` branching at
6854 /// +1 while Java does not count anything for an identical-looking
6855 /// construct (Java has no `assert`-as-branch token; its `assert`
6856 /// statement is grammar-distinct and not in this macro's arm).
6857 /// Dropping `[Assert]` from the Groovy invocation would fail this
6858 /// test.
6859 #[test]
6860 fn cyclomatic_groovy_assert_arm_300() {
6861 check_metrics::<GroovyParser>("void check(int x) { assert x > 0 }", "foo.groovy", |m| {
6862 // unit(1) + fn(1) + assert(1) = 3
6863 assert_eq!(m.cyclomatic.cyclomatic_sum(), 3, "groovy assert sum");
6864 assert_eq!(m.cyclomatic.cyclomatic_max(), 2, "groovy assert max");
6865 // Assert contributes to BOTH standard and modified CCN, so the
6866 // fn-level modified score is also base(1) + assert(1) = 2.
6867 // Without this assertion, a mutation that dropped
6868 // `stats.cyclomatic_modified += 1.` from the multi-kind arm
6869 // would pass.
6870 assert_eq!(
6871 m.cyclomatic.cyclomatic_modified_max(),
6872 2,
6873 "groovy assert modified max"
6874 );
6875 });
6876 }
6877
6878 /// Regression for issue #246: Groovy's Elvis operator `?:` is a
6879 /// short-circuit nullish operator that introduces a branch — each
6880 /// occurrence in a chain adds +1 to cyclomatic complexity. The
6881 /// dekobon Groovy grammar models Elvis as a distinct
6882 /// `elvis_expression` node with a real `QMARKCOLON` token, so the
6883 /// `impl_cyclomatic_java_like!(GroovyCode, Groovy, [Assert,
6884 /// QMARKCOLON])` invocation picks it up directly.
6885 #[test]
6886 fn cyclomatic_groovy_elvis_chain_246() {
6887 check_metrics::<GroovyParser>(
6888 "def pick(a, b, c) { return a ?: b ?: c }",
6889 "foo.groovy",
6890 |m| {
6891 // unit(1) + fn(1) + two `?:` short-circuits(2) = 4
6892 assert_eq!(m.cyclomatic.cyclomatic_sum(), 4, "groovy elvis sum");
6893 assert_eq!(m.cyclomatic.cyclomatic_max(), 3, "groovy elvis max");
6894 assert_eq!(
6895 m.cyclomatic.cyclomatic_modified_max(),
6896 3,
6897 "groovy elvis modified max"
6898 );
6899 },
6900 );
6901 }
6902
6903 #[test]
6904 fn ruby_rescue_modifier() {
6905 // Postfix `x rescue y` parses as a `RescueModifier` node that
6906 // wraps the recovery clause. Both wrapper and clause fire the
6907 // cyclomatic branch arm; the method body therefore contributes
6908 // +2 to its space.
6909 // expected: standard = unit(1) + method(1 + 1) = 3.
6910 check_metrics::<RubyParser>(
6911 "def foo\n parse(x) rescue nil\nend\n",
6912 "foo.rb",
6913 |metric| {
6914 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
6915 insta::assert_json_snapshot!(metric.cyclomatic);
6916 },
6917 );
6918 }
6919
6920 #[test]
6921 fn ruby_safe_navigation_cyclomatic() {
6922 // Issue #452: Ruby's safe-navigation `&.` (AMPDOT) is a
6923 // short-circuit decision point per link, mirroring the
6924 // Kotlin/PHP/JS/C# treatment of `?.` (#281). The chain
6925 // `a&.b&.c` adds +2 to both standard and modified CCN.
6926 check_metrics::<RubyParser>("def read(a); a&.b&.c; end\n", "foo.rb", |metric| {
6927 // unit(1) + method(base 1 + &. 1 + &. 1) = sum 4, max 3.
6928 let s = &metric.cyclomatic;
6929 assert_eq!(s.cyclomatic_sum(), 4);
6930 assert_eq!(s.cyclomatic_max(), 3);
6931 assert_eq!(s.cyclomatic_modified_sum(), 4);
6932 assert_eq!(s.cyclomatic_modified_max(), 3);
6933 });
6934 }
6935
6936 /// Nested control flow inside a `when` handler (the iRules floor case,
6937 /// mirroring `rust_1_level_nesting`). unit(1) + handler(base 1 + while 1
6938 /// + if 1 = 3) = sum 4, max 3.
6939 #[test]
6940 fn irules_1_level_nesting() {
6941 check_metrics::<IrulesParser>(
6942 "when HTTP_REQUEST {
6943 while { $x > 0 } {
6944 if { $x > 10 } {
6945 set x [expr { $x - 1 }]
6946 }
6947 }
6948}
6949",
6950 "foo.irule",
6951 |metric| {
6952 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
6953 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
6954 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
6955 },
6956 );
6957 }
6958
6959 /// iRules `switch` is a dedicated node: each non-`default` arm is one
6960 /// standard decision; the whole `switch` is one modified decision.
6961 /// standard: unit(1) + handler(base 1 + 2 arms) = 4; modified:
6962 /// unit(1) + handler(base 1 + switch 1) = 3. The `default` arm is free.
6963 #[test]
6964 fn irules_switch() {
6965 check_metrics::<IrulesParser>(
6966 "when HTTP_REQUEST {
6967 switch [HTTP::host] {
6968 a { pool pool_a }
6969 b { pool pool_b }
6970 default { pool pool_d }
6971 }
6972}
6973",
6974 "foo.irule",
6975 |metric| {
6976 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
6977 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 3);
6978 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
6979 assert_eq!(metric.cyclomatic.cyclomatic_modified_max(), 2);
6980 },
6981 );
6982 }
6983
6984 /// The keyword logical operators `and` / `or` are decision points just
6985 /// like `&&` / `||` (iRules-specific — Tcl's grammar has no keyword
6986 /// forms). unit(1) + handler(base 1 + if 1 + and 1 + or 1 = 4) = 5.
6987 /// Guards edge case #3 / the keyword-operator arms in the impl.
6988 #[test]
6989 fn irules_and_or_keywords() {
6990 check_metrics::<IrulesParser>(
6991 "when HTTP_REQUEST {
6992 if { $a and $b or $c } {
6993 log local0. \"hit\"
6994 }
6995}
6996",
6997 "foo.irule",
6998 |metric| {
6999 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
7000 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 5);
7001 assert_eq!(metric.cyclomatic.cyclomatic_max(), 4);
7002 },
7003 );
7004 }
7005
7006 /// String comparison operators (`contains`, `eq`, `matches`, …) are
7007 /// operators, NOT branches. Two of them appear here, joined by one `||`;
7008 /// only the `if` and the `||` are decisions: unit(1) + handler(base 1 +
7009 /// if 1 + `||` 1 = 3) = 4. The two string operators add 0. Guards edge
7010 /// case #4: if each string operator were wrongly counted as a branch the
7011 /// sum would be 6, so the divergence (4 vs 6) is unambiguous — it cannot
7012 /// be confused with the `if`/`||` simply being miscounted.
7013 #[test]
7014 fn irules_string_ops_not_branches() {
7015 check_metrics::<IrulesParser>(
7016 "when HTTP_REQUEST {
7017 if { [HTTP::uri] contains \"admin\" || [HTTP::host] eq \"x\" } {
7018 log local0. \"hit\"
7019 }
7020}
7021",
7022 "foo.irule",
7023 |metric| {
7024 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 4);
7025 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 4);
7026 assert_eq!(metric.cyclomatic.cyclomatic_max(), 3);
7027 },
7028 );
7029 }
7030
7031 /// A ternary `? :` in an `expr` is one decision; the bare `>` comparison
7032 /// is not. unit(1) + handler(base 1 + ternary 1 = 2) = 3.
7033 #[test]
7034 fn irules_ternary() {
7035 check_metrics::<IrulesParser>(
7036 "when HTTP_REQUEST {
7037 set y [expr { $x > 0 ? 1 : 0 }]
7038}
7039",
7040 "foo.irule",
7041 |metric| {
7042 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
7043 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 3);
7044 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
7045 },
7046 );
7047 }
7048
7049 /// `dict for` iterates and is a loop decision; the non-looping
7050 /// `dict update` / `dict with` are excluded by the impl.
7051 /// unit(1) + handler(base 1 + dict_for 1 = 2) = 3.
7052 #[test]
7053 fn irules_dict_for_loop() {
7054 check_metrics::<IrulesParser>(
7055 "when HTTP_REQUEST {
7056 dict for { k v } $d {
7057 log local0. $k
7058 }
7059}
7060",
7061 "foo.irule",
7062 |metric| {
7063 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 3);
7064 assert_eq!(metric.cyclomatic.cyclomatic_modified_sum(), 3);
7065 assert_eq!(metric.cyclomatic.cyclomatic_max(), 2);
7066 },
7067 );
7068 }
7069
7070 /// Objective-C floor: an `if` nested inside a `for` inside a
7071 /// `method_definition` held by an `@implementation`. The
7072 /// `@implementation` opens a Class space (+1). Standard CCN =
7073 /// unit(1) + class(1) + method(1) + for(1) + if(1) = 5.
7074 #[test]
7075 fn objc_nested_control() {
7076 check_metrics::<ObjcParser>(
7077 "@implementation Foo
7078- (void)bar:(NSArray *)arr {
7079 for (int i = 0; i < 10; ++i) {
7080 if (i > 5) {
7081 [self use:i];
7082 }
7083 }
7084}
7085@end
7086",
7087 "foo.m",
7088 |metric| {
7089 assert_eq!(metric.cyclomatic.cyclomatic_sum() as u32, 5);
7090 insta::assert_json_snapshot!(metric.cyclomatic, @r#"
7091 {
7092 "sum": 5,
7093 "value": 1,
7094 "average": 5.0,
7095 "min": 1,
7096 "max": 3,
7097 "modified": {
7098 "sum": 5,
7099 "value": 1,
7100 "average": 5.0,
7101 "min": 1,
7102 "max": 3
7103 }
7104 }
7105 "#);
7106 },
7107 );
7108 }
7109
7110 /// Objective-C `@try { } @catch { }`: the `catch_clause` node adds
7111 /// one decision point. Standard CCN = unit(1) + class(1) + method(1)
7112 /// + catch(1) = 4.
7113 #[test]
7114 fn objc_try_catch() {
7115 check_metrics::<ObjcParser>(
7116 "@implementation Foo
7117- (void)bar {
7118 @try {
7119 [self doWork];
7120 } @catch (NSException *e) {
7121 [self log:e];
7122 }
7123}
7124@end
7125",
7126 "foo.m",
7127 |metric| {
7128 assert_eq!(metric.cyclomatic.cyclomatic_sum() as u32, 4);
7129 insta::assert_json_snapshot!(metric.cyclomatic, @r#"
7130 {
7131 "sum": 4,
7132 "value": 1,
7133 "average": 4.0,
7134 "min": 1,
7135 "max": 2,
7136 "modified": {
7137 "sum": 4,
7138 "value": 1,
7139 "average": 4.0,
7140 "min": 1,
7141 "max": 2
7142 }
7143 }
7144 "#);
7145 },
7146 );
7147 }
7148
7149 /// Objective-C fast enumeration `for (id x in arr)` folds into a
7150 /// `for_statement` whose `for` keyword fires once, exactly like a
7151 /// classic `for`. Standard CCN = unit(1) + class(1) + method(1) +
7152 /// for(1) = 4.
7153 #[test]
7154 fn objc_fast_enumeration() {
7155 check_metrics::<ObjcParser>(
7156 "@implementation Foo
7157- (void)bar:(NSArray *)arr {
7158 for (id x in arr) {
7159 [self use:x];
7160 }
7161}
7162@end
7163",
7164 "foo.m",
7165 |metric| {
7166 assert_eq!(metric.cyclomatic.cyclomatic_sum() as u32, 4);
7167 insta::assert_json_snapshot!(metric.cyclomatic, @r#"
7168 {
7169 "sum": 4,
7170 "value": 1,
7171 "average": 4.0,
7172 "min": 1,
7173 "max": 2,
7174 "modified": {
7175 "sum": 4,
7176 "value": 1,
7177 "average": 4.0,
7178 "min": 1,
7179 "max": 2
7180 }
7181 }
7182 "#);
7183 },
7184 );
7185 }
7186}