big_code_analysis/metrics/abc.rs
1// Per-language metric and AST modules deliberately consume the macro-
2// generated tree-sitter token enums via `use crate::*` and `use Foo::*`
3// inside match expressions — explicit imports would list dozens of
4// variants per arm and obscure the per-language token sets that are the
5// point of these files. Allowed at the module level rather than per
6// function so the per-language impl blocks stay readable.
7#![allow(
8 clippy::enum_glob_use,
9 clippy::too_many_lines,
10 clippy::wildcard_imports
11)]
12// Metric counts (token, function, branch, argument, etc.) are stored as
13// `usize` and crossed with `f64` averages, ratios, and Halstead scores
14// across the cyclomatic / MI / Halstead computations. The `usize as f64`
15// and `f64 as usize` casts are intentional and snapshot-anchored — every
16// site is bounded by the count it came from. Allowing the lints at the
17// module level keeps the metric arithmetic legible.
18#![allow(
19 clippy::cast_precision_loss,
20 clippy::cast_possible_truncation,
21 clippy::cast_sign_loss
22)]
23
24use std::fmt;
25
26use crate::checker::Checker;
27
28use crate::macros::implement_metric_trait;
29
30use crate::*;
31
32mod bash;
33mod c;
34mod cpp;
35mod csharp;
36mod elixir;
37mod go;
38mod groovy;
39mod irules;
40mod java;
41mod js_family;
42mod kotlin;
43mod lua;
44mod mozcpp;
45mod objc;
46mod perl;
47mod php;
48mod python;
49mod ruby;
50mod rust;
51mod tcl;
52
53/// The `ABC` metric.
54///
55/// The `ABC` metric measures the size of a source code by counting
56/// the number of Assignments (`A`), Branches (`B`) and Conditions (`C`).
57/// The metric defines an ABC score as a vector of three elements (`<A,B,C>`).
58/// The ABC score can be represented by its individual components (`A`, `B` and `C`)
59/// or by the magnitude of the vector (`|<A,B,C>| = sqrt(A^2 + B^2 + C^2)`).
60///
61/// Official paper and definition:
62///
63/// Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report.
64///
65/// <https://www.softwarerenovation.com/Articles.aspx>
66///
67/// # Cross-language `&&` / `||` policy
68///
69/// Per Fitzpatrick's conditional-operator rule (Rule 5 in Figure 2
70/// for C and Figure 4 for Java; Rule 7 in Figure 3 for C++), only
71/// comparison operators (`==`, `!=`, `<=`, `>=`, `<`, `>`) and a
72/// paper-defined keyword set (`else`, `case`, `default`, `?`, plus
73/// `try` / `catch` for C++ and Java) contribute to the condition
74/// count. Per-language `impl Abc` blocks narrow this set where
75/// appropriate — e.g., C++/Rust/Go/Python exclude `default` since
76/// it falls through unconditionally (matching the Rust `_ =>` and
77/// Java `default:` precedent). The short-
78/// circuit logical operators `&&` and `||` (and per-language
79/// equivalents — Python's `and` / `or`, Lua's `and` / `or`, Tcl's
80/// `&&` / `||`, Perl's `&&` / `||` / `//` / `and` / `or` / `xor`)
81/// are deliberately **not** counted on their own. The paper's
82/// worked Listing 2 annotates `(am >= 0 && am <= 0xF) ? '/' : 'C'`
83/// as `accc` — three conditions for `>=`, `<=`, `?`, zero for
84/// `&&`.
85///
86/// Fitzpatrick's Rule 7 (Figure 3, C++) / Rule 9 (Figure 4, Java) —
87/// "Add one to the condition count for each unary conditional
88/// expression" — instead counts each non-comparison operand of a
89/// `&&` / `||` chain once. The paper's worked example for this
90/// rule is `if (x || y) printf("test failure\n");`, annotated:
91/// "there are two unary conditions since both `x` and `y` are
92/// tested as conditional expressions" (so `||` contributes zero,
93/// `x` contributes one, `y` contributes one, and `printf(...)`
94/// contributes one branch). The walker machinery for this —
95/// modelled on `java_count_unary_conditions` /
96/// `java_inspect_container` — is present today for Java, Groovy,
97/// C#, Rust, Go, JavaScript, TypeScript, TSX, Mozjs, PHP, C++,
98/// Python, Perl, Lua, Tcl, iRules, Kotlin, Ruby, and Elixir. So
99/// `if (a && b)` reports 2 conditions across this set, matching
100/// the paper. Bash is the lone exception: its `&&` / `||` are
101/// command-list separators rather than boolean-expression operands
102/// with named leaf operands, so Fitzpatrick's Rule 9 does not map
103/// onto its grammar and the walker is deliberately not wired.
104///
105/// This policy is paper-faithful and deviates from RuboCop's
106/// `Metrics/AbcSize` (which counts `and` / `or` as conditions
107/// directly) while matching `StepicOrg/abcmeter` and
108/// `eoinnoble/python-abc`. The book's *ABC counting rules*
109/// section reproduces the rule tables, a per-language deviation
110/// table, and worked examples — see the chapter at
111/// <https://dekobon.github.io/big-code-analysis/metrics.html#abc>.
112///
113/// See issue #395 for the Phase-1 cross-language policy
114/// alignment, #403 for the Phase-2 unary-conditional walker
115/// fan-out, #404 for the Phase-3 book documentation, and #557
116/// for the Kotlin / Ruby / Elixir walker wiring.
117#[derive(Debug, Clone, PartialEq)]
118#[non_exhaustive]
119pub struct Stats {
120 pub(super) assignments: f64,
121 assignments_sum: f64,
122 assignments_min: f64,
123 assignments_max: f64,
124 pub(super) branches: f64,
125 branches_sum: f64,
126 branches_min: f64,
127 branches_max: f64,
128 pub(super) conditions: f64,
129 conditions_sum: f64,
130 conditions_min: f64,
131 conditions_max: f64,
132 space_count: usize,
133 pub(super) declaration: Vec<DeclKind>,
134}
135
136#[derive(Debug, Clone, PartialEq)]
137pub(super) enum DeclKind {
138 Var,
139 Const,
140}
141
142impl Default for Stats {
143 fn default() -> Self {
144 Self {
145 assignments: 0.,
146 assignments_sum: 0.,
147 assignments_min: f64::MAX,
148 assignments_max: 0.,
149 branches: 0.,
150 branches_sum: 0.,
151 branches_min: f64::MAX,
152 branches_max: 0.,
153 conditions: 0.,
154 conditions_sum: 0.,
155 conditions_min: f64::MAX,
156 conditions_max: 0.,
157 space_count: 1,
158 declaration: Vec::new(),
159 }
160 }
161}
162
163impl fmt::Display for Stats {
164 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
165 write!(
166 f,
167 "assignments: {}, branches: {}, conditions: {}, magnitude: {}, \
168 assignments_average: {}, branches_average: {}, conditions_average: {}, \
169 assignments_min: {}, assignments_max: {}, \
170 branches_min: {}, branches_max: {}, \
171 conditions_min: {}, conditions_max: {}",
172 self.assignments_sum(),
173 self.branches_sum(),
174 self.conditions_sum(),
175 self.magnitude_sum(),
176 self.assignments_average(),
177 self.branches_average(),
178 self.conditions_average(),
179 self.assignments_min(),
180 self.assignments_max(),
181 self.branches_min(),
182 self.branches_max(),
183 self.conditions_min(),
184 self.conditions_max()
185 )
186 }
187}
188
189impl Stats {
190 /// Merges a second `Abc` metric into the first one.
191 pub fn merge(&mut self, other: &Stats) {
192 // Calculates minimum and maximum values
193 self.assignments_min = self.assignments_min.min(other.assignments_min);
194 self.assignments_max = self.assignments_max.max(other.assignments_max);
195 self.branches_min = self.branches_min.min(other.branches_min);
196 self.branches_max = self.branches_max.max(other.branches_max);
197 self.conditions_min = self.conditions_min.min(other.conditions_min);
198 self.conditions_max = self.conditions_max.max(other.conditions_max);
199
200 self.assignments_sum += other.assignments_sum;
201 self.branches_sum += other.branches_sum;
202 self.conditions_sum += other.conditions_sum;
203
204 self.space_count += other.space_count;
205 }
206
207 /// Returns the `Abc` assignments metric value.
208 #[must_use]
209 pub fn assignments(&self) -> u64 {
210 self.assignments as u64
211 }
212
213 /// Returns the `Abc` assignments sum metric value.
214 #[must_use]
215 pub fn assignments_sum(&self) -> u64 {
216 self.assignments_sum as u64
217 }
218
219 /// Returns the `Abc` assignments average value.
220 ///
221 /// This value is computed dividing the `Abc`
222 /// assignments value for the number of spaces.
223 #[must_use]
224 pub fn assignments_average(&self) -> f64 {
225 crate::metrics::average(self.assignments_sum() as f64, self.space_count)
226 }
227
228 /// Returns the `Abc` assignments minimum value.
229 ///
230 /// Collapses the `f64::MAX` sentinel that `Stats::default()` plants
231 /// into `assignments_min` to `0`, so a never-observed space
232 /// serializes to a meaningful number rather than `1.7976931e308`.
233 #[allow(clippy::float_cmp)]
234 #[must_use]
235 pub fn assignments_min(&self) -> u64 {
236 if self.assignments_min == f64::MAX {
237 0
238 } else {
239 self.assignments_min as u64
240 }
241 }
242
243 /// Returns the `Abc` assignments maximum value.
244 #[must_use]
245 pub fn assignments_max(&self) -> u64 {
246 self.assignments_max as u64
247 }
248
249 /// Returns the `Abc` branches metric value.
250 #[must_use]
251 pub fn branches(&self) -> u64 {
252 self.branches as u64
253 }
254
255 /// Returns the `Abc` branches sum metric value.
256 #[must_use]
257 pub fn branches_sum(&self) -> u64 {
258 self.branches_sum as u64
259 }
260
261 /// Returns the `Abc` branches average value.
262 ///
263 /// This value is computed dividing the `Abc`
264 /// branches value for the number of spaces.
265 #[must_use]
266 pub fn branches_average(&self) -> f64 {
267 crate::metrics::average(self.branches_sum() as f64, self.space_count)
268 }
269
270 /// Returns the `Abc` branches minimum value.
271 ///
272 /// Same `f64::MAX` sentinel collapse as `assignments_min`.
273 #[allow(clippy::float_cmp)]
274 #[must_use]
275 pub fn branches_min(&self) -> u64 {
276 if self.branches_min == f64::MAX {
277 0
278 } else {
279 self.branches_min as u64
280 }
281 }
282
283 /// Returns the `Abc` branches maximum value.
284 #[must_use]
285 pub fn branches_max(&self) -> u64 {
286 self.branches_max as u64
287 }
288
289 /// Returns the `Abc` conditions metric value.
290 #[must_use]
291 pub fn conditions(&self) -> u64 {
292 self.conditions as u64
293 }
294
295 /// Returns the `Abc` conditions sum metric value.
296 #[must_use]
297 pub fn conditions_sum(&self) -> u64 {
298 self.conditions_sum as u64
299 }
300
301 /// Returns the `Abc` conditions average value.
302 ///
303 /// This value is computed dividing the `Abc`
304 /// conditions value for the number of spaces.
305 #[must_use]
306 pub fn conditions_average(&self) -> f64 {
307 crate::metrics::average(self.conditions_sum() as f64, self.space_count)
308 }
309
310 /// Returns the `Abc` conditions minimum value.
311 ///
312 /// Same `f64::MAX` sentinel collapse as `assignments_min`.
313 #[allow(clippy::float_cmp)]
314 #[must_use]
315 pub fn conditions_min(&self) -> u64 {
316 if self.conditions_min == f64::MAX {
317 0
318 } else {
319 self.conditions_min as u64
320 }
321 }
322
323 /// Returns the `Abc` conditions maximum value.
324 #[must_use]
325 pub fn conditions_max(&self) -> u64 {
326 self.conditions_max as u64
327 }
328
329 /// Returns the `Abc` magnitude metric value.
330 #[must_use]
331 pub fn magnitude(&self) -> f64 {
332 (self.assignments.powi(2) + self.branches.powi(2) + self.conditions.powi(2)).sqrt()
333 }
334
335 /// Returns the `Abc` magnitude sum metric value.
336 #[must_use]
337 pub fn magnitude_sum(&self) -> f64 {
338 (self.assignments_sum.powi(2) + self.branches_sum.powi(2) + self.conditions_sum.powi(2))
339 .sqrt()
340 }
341
342 #[inline]
343 pub(crate) fn compute_sum(&mut self) {
344 self.assignments_sum += self.assignments;
345 self.branches_sum += self.branches;
346 self.conditions_sum += self.conditions;
347 }
348
349 #[inline]
350 pub(crate) fn compute_minmax(&mut self) {
351 self.assignments_min = self.assignments_min.min(self.assignments);
352 self.assignments_max = self.assignments_max.max(self.assignments);
353 self.branches_min = self.branches_min.min(self.branches);
354 self.branches_max = self.branches_max.max(self.branches);
355 self.conditions_min = self.conditions_min.min(self.conditions);
356 self.conditions_max = self.conditions_max.max(self.conditions);
357 self.compute_sum();
358 }
359}
360
361#[doc(hidden)]
362/// Per-language computation of the ABC metric.
363pub(crate) trait Abc
364where
365 Self: Checker,
366{
367 /// Walk `node` and update `stats` with this metric for the language
368 /// implementing the trait.
369 ///
370 /// `code` is the source bytes underlying the parsed tree. Most
371 /// languages ignore it: assignments, branches, and conditions all
372 /// surface as distinct grammar productions and a `kind_id()` match
373 /// is enough. Elixir is the exception — `case` / `cond` / `if` /
374 /// `with` / guard `when` arms surface as `Call` nodes whose keyword
375 /// target lives only in the source text. Matching the `Cyclomatic`
376 /// / `Halstead` / `Exit` / `Cognitive` pattern keeps the signature
377 /// uniform.
378 fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats);
379}
380
381// Shared Phase-2B helper (issue #403): walk every named child of an
382// expression-list-style wrapper (Go's `expression_list`, Lua's
383// `expression_list`) and route each through a language-specific
384// classifier. Used for `return value1, value2, ...` arms where the
385// values live one level below the return statement under a list
386// wrapper. The classifier receives only named children so that
387// `,` / `;` / `(` / `)` tokens never reach it.
388pub(super) fn for_each_named_child(list: &Node, conditions: &mut f64, f: fn(&Node, &mut f64)) {
389 let mut cursor = list.cursor();
390 if cursor.goto_first_child() {
391 loop {
392 let child = cursor.node();
393 if child.is_named() {
394 f(&child, conditions);
395 }
396 if !cursor.goto_next_sibling() {
397 break;
398 }
399 }
400 }
401}
402
403// Default no-op `Abc` impls. Audited in #188; the matrix below
404// records the rationale for every entry so the no-op default is a
405// deliberate choice, not scaffolding leftover.
406//
407// Real defaults (the language has no construct ABC measures, so the
408// metric is genuinely 0):
409// - PreprocCode, CcommentCode: no executable code (comments /
410// preprocessor lines only).
411implement_metric_trait!(Abc, PreprocCode, CcommentCode);
412
413#[cfg(test)]
414#[allow(
415 clippy::float_cmp,
416 clippy::cast_precision_loss,
417 clippy::cast_possible_truncation,
418 clippy::cast_sign_loss,
419 clippy::similar_names,
420 clippy::doc_markdown,
421 clippy::needless_raw_string_hashes,
422 clippy::too_many_lines
423)]
424mod tests {
425 use crate::tools::{check_func_space, check_metrics};
426 use crate::traits::ParserTrait;
427
428 use super::*;
429
430 // Walk the AST and return true iff any node has `kind_id == target`.
431 // Used as a drift marker for hidden-rule kind ids: a passing
432 // `!ast_has_kind_id(...)` assertion proves the kind is unreachable
433 // at the pinned grammar version, so a future grammar bump that
434 // promotes the hidden rule to a concrete emitted node will fail
435 // loudly instead of silently changing the metric (lesson 34).
436 fn ast_has_kind_id<P: ParserTrait>(parser: &P, target: u16) -> bool {
437 let mut stack = vec![parser.root()];
438 while let Some(node) = stack.pop() {
439 if node.kind_id() == target {
440 return true;
441 }
442 for i in (0..node.child_count()).rev() {
443 if let Some(c) = node.child(i) {
444 stack.push(c);
445 }
446 }
447 }
448 false
449 }
450
451 /// Regression for #227: a `Stats::default()` that never sees an
452 /// observation must not leak the `f64::MAX` sentinel for
453 /// `assignments_min`, `branches_min`, or `conditions_min`. All
454 /// three getters collapse the sentinel to `0.0` so JSON never
455 /// emits `1.7976931e308`.
456 #[test]
457 fn abc_empty_file_min_is_zero() {
458 let stats = Stats::default();
459 assert_eq!(stats.assignments_min(), 0);
460 assert_eq!(stats.branches_min(), 0);
461 assert_eq!(stats.conditions_min(), 0);
462 }
463
464 // Regression test for the `EQ` arm guard in `JavaCode::compute`:
465 // the rewrite from `.map().unwrap_or_else()` to
466 // `is_none_or(|decl| matches!(decl, DeclKind::Var))` must preserve
467 // the three-way truth table — None → ++, Some(Var) → ++,
468 // Some(Const) → no-op.
469 #[test]
470 fn java_eq_arm_increments_when_declaration_stack_is_empty() {
471 // No surrounding `int x = ...` / `Final` token → declaration
472 // stack is empty when the `EQ` token is visited, so the None
473 // branch must increment `assignments`.
474 check_metrics::<JavaParser>(
475 "class A { void m() { int x = 0; x = 1; x = 2; x = 3; } }",
476 "foo.java",
477 |metric| {
478 // `int x = 0;` adds 1 (Some(Var) branch),
479 // each subsequent `x = N;` adds 1 (None branch).
480 assert_eq!(metric.abc.assignments_sum(), 4);
481 },
482 );
483 }
484
485 #[test]
486 fn java_eq_arm_skips_when_declaration_stack_top_is_const() {
487 // `final` pushes `DeclKind::Const` on top of the active `Var`
488 // entry, so the Some(non-Var) branch must skip the increment.
489 check_metrics::<JavaParser>(
490 "class A {
491 final int X = 1;
492 final int Y = 2;
493 void m() { final int Z = 3; }
494 }",
495 "foo.java",
496 |metric| {
497 // All three `=` tokens land under a `Const` top, so
498 // assignments should be 0 across all spaces.
499 assert_eq!(metric.abc.assignments_sum(), 0);
500 },
501 );
502 }
503
504 // Constant declarations are not counted as assignments
505 #[test]
506 fn java_constant_declarations() {
507 check_metrics::<JavaParser>(
508 "class A {
509 private final int X1 = 0, Y1 = 0;
510 public final float PI = 3.14f;
511 final static String HELLO = \"Hello,\";
512 protected String world = \" world!\"; // +1a
513 public float e = 2.718f; // +1a
514 private int x2 = 1, y2 = 2; // +2a
515
516 void m() {
517 final int Z1 = 0, Z2 = 0, Z3 = 0;
518 final float T = 0.0f;
519 int z1 = 1, z2 = 2, z3 = 3; // +3a
520 float t = 60.0f; // +1a
521 }
522 }",
523 "foo.java",
524 |metric| {
525 // magnitude: sqrt(64 + 0 + 0) = sqrt(64)
526 // space count: 3 (1 unit, 1 class and 1 method)
527 insta::assert_json_snapshot!(
528 metric.abc,
529 @r#"
530 {
531 "assignments": 8,
532 "branches": 0,
533 "conditions": 0,
534 "magnitude": 8.0,
535 "value": 0.0,
536 "assignments_average": 2.6666666666666665,
537 "branches_average": 0.0,
538 "conditions_average": 0.0,
539 "assignments_min": 0,
540 "assignments_max": 4,
541 "branches_min": 0,
542 "branches_max": 0,
543 "conditions_min": 0,
544 "conditions_max": 0
545 }
546 "#
547 );
548 },
549 );
550 }
551
552 // "In computer science, conditionals (that is, conditional statements, conditional expressions
553 // and conditional constructs,) are programming language commands for handling decisions."
554 // Source: https://en.wikipedia.org/wiki/Conditional_(computer_programming)
555 // According to this definition, boolean expressions that are evaluated to make a decision are considered as conditions
556 // Variables, method invocations and true or false values used inside
557 // variable declarations and assignment expressions are not counted as conditions
558 #[test]
559 fn java_declarations_with_conditions() {
560 check_metrics::<JavaParser>(
561 "
562 boolean a = (1 > 2); // +1a +1c
563 boolean b = 3 > 4; // +1a +1c
564 boolean c = (1 > 2) && 3 > 4; // +1a +2c
565 boolean d = b && (x > 5) || c; // +1a +3c
566 boolean e = !d; // +1a +1c
567 boolean f = ((!false)); // +1a +1c
568 boolean g = !(!(true)); // +1a +1c
569 boolean h = true; // +1a
570 boolean i = (false); // +1a
571 boolean j = (((((true))))); // +1a
572 boolean k = (((((m()))))); // +1a +1b
573 boolean l = (((((!m()))))); // +1a +1b +1c
574 boolean m = (!(!((m())))); // +1a +1b +1c
575 List<String> n = null; // +1a (< and > used for generic types are not counted as conditions)
576 ",
577 "foo.java",
578 |metric| {
579 // magnitude: sqrt(196 + 9 + 144) = sqrt(349)
580 // space count: 1 (1 unit)
581 insta::assert_json_snapshot!(
582 metric.abc,
583 @r#"
584 {
585 "assignments": 14,
586 "branches": 3,
587 "conditions": 12,
588 "magnitude": 18.681541692269406,
589 "value": 18.681541692269406,
590 "assignments_average": 14.0,
591 "branches_average": 3.0,
592 "conditions_average": 12.0,
593 "assignments_min": 14,
594 "assignments_max": 14,
595 "branches_min": 3,
596 "branches_max": 3,
597 "conditions_min": 12,
598 "conditions_max": 12
599 }
600 "#
601 );
602 },
603 );
604 }
605
606 // Conditions can be found in assignment expressions
607 #[test]
608 fn java_assignments_with_conditions() {
609 check_metrics::<JavaParser>(
610 "
611 a = 2 < 1; // +1a +1c
612 b = (4 >= 3) && 2 <= 1; // +1a +2c
613 c = a || (x != 10) && b; // +1a +3c
614 d = !false; // +1a +1c
615 e = (!false); // +1a +1c
616 f = !(false); // +1a +1c
617 g = (!(((true)))); // +1a +1c
618 h = ((true)); // +1a
619 i = !m(); // +1a +1b +1c
620 j = !((m())); // +1a +1b +1c
621 k = (!(m())); // +1a +1b +1c
622 l = ((!(m()))); // +1a +1b +1c
623 m = !B.<Integer>m(2); // +1a +1b +1c
624 n = !((B.<Integer>m(4))); // +1a +1b +1c
625 ",
626 "foo.java",
627 |metric| {
628 // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
629 // space count: 1 (1 unit)
630 insta::assert_json_snapshot!(
631 metric.abc,
632 @r#"
633 {
634 "assignments": 14,
635 "branches": 6,
636 "conditions": 16,
637 "magnitude": 22.090722034374522,
638 "value": 22.090722034374522,
639 "assignments_average": 14.0,
640 "branches_average": 6.0,
641 "conditions_average": 16.0,
642 "assignments_min": 14,
643 "assignments_max": 14,
644 "branches_min": 6,
645 "branches_max": 6,
646 "conditions_min": 16,
647 "conditions_max": 16
648 }
649 "#
650 );
651 },
652 );
653 }
654
655 // Conditions can be found in method arguments
656 #[test]
657 fn java_methods_arguments_with_conditions() {
658 check_metrics::<JavaParser>(
659 "
660 m1(a); // +1b
661 m2(a, b); // +1b
662 m3(true, (false), (((true)))); // +1b
663 m3(m1(false), m1(true), m1(false)); // +4b
664 m1(!a); // +1b +1c
665 m2((((a))), (!b)); // +1b +1c
666 m3(!(a), b, !!!c); // +1b +2c
667 m3(a, !b, m2(!a, !m2(!b, !m1(!c)))); // +4b +6c
668 ",
669 "foo.java",
670 |metric| {
671 // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
672 // space count: 1 (1 unit)
673 insta::assert_json_snapshot!(
674 metric.abc,
675 @r#"
676 {
677 "assignments": 0,
678 "branches": 14,
679 "conditions": 10,
680 "magnitude": 17.204650534085253,
681 "value": 17.204650534085253,
682 "assignments_average": 0.0,
683 "branches_average": 14.0,
684 "conditions_average": 10.0,
685 "assignments_min": 0,
686 "assignments_max": 0,
687 "branches_min": 14,
688 "branches_max": 14,
689 "conditions_min": 10,
690 "conditions_max": 10
691 }
692 "#
693 );
694 },
695 );
696 }
697
698 // "A unary conditional expression is an implicit condition that uses no relational operators."
699 // Source: Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report.
700 // https://www.softwarerenovation.com/Articles.aspx (page 5)
701 #[test]
702 fn java_if_single_conditions() {
703 check_metrics::<JavaParser>(
704 "
705 if ( a < 0 ) {} // +1c
706 if ( ((a != 0)) ) {} // +1c
707 if ( !(a > 0) ) {} // +1c
708 if ( !(((a == 0))) ) {} // +1c
709 if ( b.m1() ) {} // +1b +1c
710 if ( !b.m1() ) {} // +1b +1c
711 if ( !!b.m2() ) {} // +1b +1c
712 if ( (!(b.m1())) ) {} // +1b +1c
713 if ( (!(!b.m1())) ) {} // +1b +1c
714 if ( ((b.m2())) ) {} // +1b +1c
715 if ( ((b.m().m1())) ) {} // +2b +1c
716 if ( c ) {} // +1c
717 if ( !c ) {} // +1c
718 if ( !!!!!!!!!!c ) {} // +1c
719 if ( (((c))) ) {} // +1c
720 if ( (((!c))) ) {} // +1c
721 if ( ((!(c))) ) {} // +1c
722 if ( true ) {} // +1c
723 if ( !true ) {} // +1c
724 if ( ((false)) ) {} // +1c
725 if ( !(!(false)) ) {} // +1c
726 if ( !!!false ) {} // +1c
727 ",
728 "foo.java",
729 |metric| {
730 // magnitude: sqrt(0 + 64 + 484) = sqrt(548)
731 // space count: 1 (1 unit)
732 insta::assert_json_snapshot!(
733 metric.abc,
734 @r#"
735 {
736 "assignments": 0,
737 "branches": 8,
738 "conditions": 22,
739 "magnitude": 23.40939982143925,
740 "value": 23.40939982143925,
741 "assignments_average": 0.0,
742 "branches_average": 8.0,
743 "conditions_average": 22.0,
744 "assignments_min": 0,
745 "assignments_max": 0,
746 "branches_min": 8,
747 "branches_max": 8,
748 "conditions_min": 22,
749 "conditions_max": 22
750 }
751 "#
752 );
753 },
754 );
755 }
756
757 #[test]
758 fn java_if_multiple_conditions() {
759 check_metrics::<JavaParser>(
760 "
761 if ( a || b || c || d ) {} // +4c
762 if ( a || b && c && d ) {} // +4c
763 if ( x < y && a == b ) {} // +2c
764 if ( ((z < (x + y))) ) {} // +1c
765 if ( a || ((((b))) && c) ) {} // +3c
766 if ( a && ((((a == b))) && c) ) {} // +3c
767 if ( a || ((((a == b))) || ((c))) ) {} // +3c
768 if ( x < y && B.m() ) {} // +1b +2c
769 if ( x < y && !(((B.m()))) ) {} // +1b +2c
770 if ( !(x < y) && !B.m() ) {} // +1b +2c
771 if ( !!!(!!!(a)) && B.m() || // +1b +2c
772 !B.m() && (((x > 4))) ) {} // +1b +2c
773 ",
774 "foo.java",
775 |metric| {
776 // magnitude: sqrt(0 + 25 + 900) = sqrt(925)
777 // space count: 1 (1 unit)
778 insta::assert_json_snapshot!(
779 metric.abc,
780 @r#"
781 {
782 "assignments": 0,
783 "branches": 5,
784 "conditions": 30,
785 "magnitude": 30.4138126514911,
786 "value": 30.4138126514911,
787 "assignments_average": 0.0,
788 "branches_average": 5.0,
789 "conditions_average": 30.0,
790 "assignments_min": 0,
791 "assignments_max": 0,
792 "branches_min": 5,
793 "branches_max": 5,
794 "conditions_min": 30,
795 "conditions_max": 30
796 }
797 "#
798 );
799 },
800 );
801 }
802
803 #[test]
804 fn java_while_and_do_while_conditions() {
805 check_metrics::<JavaParser>(
806 "
807 while ( (!(!(!(a)))) ) {} // +1c
808 while ( b || 1 > 2 ) {} // +2c
809 while ( x.m() && (((c))) ) {} // +1b +2c
810 do {} while ( !!!(((!!!a))) ); // +1c
811 do {} while ( a || (b && c) ); // +3c
812 do {} while ( !x.m() && 1 > 2 || !true ); // +1b +3c
813 ",
814 "foo.java",
815 |metric| {
816 // magnitude: sqrt(0 + 4 + 144) = sqrt(148)
817 // space count: 1 (1 unit)
818 insta::assert_json_snapshot!(
819 metric.abc,
820 @r#"
821 {
822 "assignments": 0,
823 "branches": 2,
824 "conditions": 12,
825 "magnitude": 12.165525060596439,
826 "value": 12.165525060596439,
827 "assignments_average": 0.0,
828 "branches_average": 2.0,
829 "conditions_average": 12.0,
830 "assignments_min": 0,
831 "assignments_max": 0,
832 "branches_min": 2,
833 "branches_max": 2,
834 "conditions_min": 12,
835 "conditions_max": 12
836 }
837 "#
838 );
839 },
840 );
841 }
842
843 // GMetrics, a Groovy source code analyzer, provides the following definition of unary conditional expression:
844 // "These are cases where a single variable/field/value is treated as a boolean value.
845 // Examples include `if (x)` and `return !ready`."
846 // According to this definition, unary conditional expressions are counted also in function return values.
847 // Source: https://dx42.github.io/gmetrics/metrics/AbcMetric.html
848 // Examples: https://github.com/dx42/gmetrics/blob/master/src/test/groovy/org/gmetrics/metric/abc/AbcMetric_MethodTest.groovy
849 #[test]
850 fn java_return_with_conditions() {
851 check_metrics::<JavaParser>(
852 "class A {
853 boolean m1() {
854 return !(z >= 0); // +1c
855 }
856 boolean m2() {
857 return (((!x))); // +1c
858 }
859 boolean m3() {
860 return x && y; // +2c
861 }
862 boolean m4() {
863 return y || (z < 0); // +2c
864 }
865 boolean m5() {
866 return x || y ? // +3c (two unary conditions and one ?)
867 true : false;
868 }
869 }",
870 "foo.java",
871 |metric| {
872 // magnitude: sqrt(0 + 0 + 81) = sqrt(81)
873 // space count: 7 (1 unit, 1 class and 5 methods)
874 insta::assert_json_snapshot!(
875 metric.abc,
876 @r#"
877 {
878 "assignments": 0,
879 "branches": 0,
880 "conditions": 9,
881 "magnitude": 9.0,
882 "value": 0.0,
883 "assignments_average": 0.0,
884 "branches_average": 0.0,
885 "conditions_average": 1.2857142857142858,
886 "assignments_min": 0,
887 "assignments_max": 0,
888 "branches_min": 0,
889 "branches_max": 0,
890 "conditions_min": 0,
891 "conditions_max": 3
892 }
893 "#
894 );
895 },
896 );
897 }
898
899 // Variables, method invocations, and true or false values
900 // inside return statements are not counted as conditions
901 #[test]
902 fn java_return_without_conditions() {
903 check_metrics::<JavaParser>(
904 "class A {
905 boolean m1() {
906 return x;
907 }
908 boolean m2() {
909 return (x);
910 }
911 boolean m3() {
912 return y.m(); // +1b
913 }
914 boolean m4() {
915 return false;
916 }
917 void m5() {
918 return;
919 }
920 }",
921 "foo.java",
922 |metric| {
923 // magnitude: sqrt(0 + 1 + 0) = sqrt(1)
924 // space count: 7 (1 unit, 1 class and 5 methods)
925 insta::assert_json_snapshot!(
926 metric.abc,
927 @r#"
928 {
929 "assignments": 0,
930 "branches": 1,
931 "conditions": 0,
932 "magnitude": 1.0,
933 "value": 0.0,
934 "assignments_average": 0.0,
935 "branches_average": 0.14285714285714285,
936 "conditions_average": 0.0,
937 "assignments_min": 0,
938 "assignments_max": 0,
939 "branches_min": 0,
940 "branches_max": 1,
941 "conditions_min": 0,
942 "conditions_max": 0
943 }
944 "#
945 );
946 },
947 );
948 }
949
950 // Variables, method invocations, and true or false values
951 // in lambda expression return values are not counted as conditions
952 #[test]
953 fn java_lambda_expressions_return_with_conditions() {
954 check_metrics::<JavaParser>(
955 "
956 Predicate<Boolean> p1 = a -> a; // +1a
957 Predicate<Boolean> p2 = b -> true; // +1a
958 Predicate<Boolean> p3 = c -> m(); // +1a
959 Predicate<Integer> p4 = d -> d > 10; // +1a +1c
960 Predicate<Boolean> p5 = (e) -> !e; // +1a +1c
961 Predicate<Boolean> p6 = (f) -> !((!f)); // +1a +1c
962 Predicate<Boolean> p7 = (g) -> !g && true; // +1a +2c
963 BiPredicate<Boolean, Boolean> bp1 = (h, i) -> !h && !i; // +1a +2c
964 BiPredicate<Boolean, Boolean> bp2 = (j, k) -> {
965 return j || k; // +1a +2c
966 };
967 ",
968 "foo.java",
969 |metric| {
970 // magnitude: sqrt(81 + 1 + 81) = sqrt(163)
971 // space count: 1 (1 unit)
972 insta::assert_json_snapshot!(
973 metric.abc,
974 @r#"
975 {
976 "assignments": 9,
977 "branches": 1,
978 "conditions": 9,
979 "magnitude": 12.767145334803704,
980 "value": 12.767145334803704,
981 "assignments_average": 9.0,
982 "branches_average": 1.0,
983 "conditions_average": 9.0,
984 "assignments_min": 9,
985 "assignments_max": 9,
986 "branches_min": 1,
987 "branches_max": 1,
988 "conditions_min": 9,
989 "conditions_max": 9
990 }
991 "#
992 );
993 },
994 );
995 }
996
997 #[test]
998 fn java_for_with_variable_declaration() {
999 check_metrics::<JavaParser>(
1000 "
1001 for ( int i1 = 0; !(!(!(!a))); i1++ ) {} // +2a +1c
1002 for ( int i2 = 0; !B.m(); i2++ ) {} // +2a +1b +1c
1003 for ( int i3 = 0; a || false; i3++ ) {} // +2a +2c
1004 for ( int i4 = 0; a && B.m() ? true : false; i4++ ) {} // +2a +1b +3c
1005 for ( int i5 = 0; true; i5++ ) {} // +2a +1c
1006 ",
1007 "foo.java",
1008 |metric| {
1009 // magnitude: sqrt(100 + 4 + 64) = sqrt(168)
1010 // space count: 1 (1 unit)
1011 insta::assert_json_snapshot!(
1012 metric.abc,
1013 @r#"
1014 {
1015 "assignments": 10,
1016 "branches": 2,
1017 "conditions": 8,
1018 "magnitude": 12.96148139681572,
1019 "value": 12.96148139681572,
1020 "assignments_average": 10.0,
1021 "branches_average": 2.0,
1022 "conditions_average": 8.0,
1023 "assignments_min": 10,
1024 "assignments_max": 10,
1025 "branches_min": 2,
1026 "branches_max": 2,
1027 "conditions_min": 8,
1028 "conditions_max": 8
1029 }
1030 "#
1031 );
1032 },
1033 );
1034 }
1035
1036 #[test]
1037 fn java_for_without_variable_declaration() {
1038 check_metrics::<JavaParser>(
1039 "class A{
1040 void m1() {
1041 for (i = 0; x < y; i++) {} // +2a +1c
1042 for (i = 0; ((x < y)); i++) {} // +2a +1c
1043 for (i = 0; !(!(x < y)); i++) {} // +2a +1c
1044 for (i = 0; true; i++) {} // +2a +1c
1045 }
1046 void m2() {
1047 for ( ; true; ) {} // +1c
1048 }
1049 void m3() {
1050 for ( ; ; ) {} // +1c (one implicit unary condition set to true)
1051 }
1052 }",
1053 "foo.java",
1054 |metric| {
1055 // magnitude: sqrt(64 + 0 + 36) = sqrt(100)
1056 // space count: 5 (1 unit, 1 class and 3 methods)
1057 insta::assert_json_snapshot!(
1058 metric.abc,
1059 @r#"
1060 {
1061 "assignments": 8,
1062 "branches": 0,
1063 "conditions": 6,
1064 "magnitude": 10.0,
1065 "value": 0.0,
1066 "assignments_average": 1.6,
1067 "branches_average": 0.0,
1068 "conditions_average": 1.2,
1069 "assignments_min": 0,
1070 "assignments_max": 8,
1071 "branches_min": 0,
1072 "branches_max": 0,
1073 "conditions_min": 0,
1074 "conditions_max": 4
1075 }
1076 "#
1077 );
1078 },
1079 );
1080 }
1081
1082 // Variables, method invocations, and true or false values
1083 // in ternary expression return values are not counted as conditions
1084 #[test]
1085 fn java_ternary_conditions() {
1086 check_metrics::<JavaParser>(
1087 "
1088 a = true; // +1a
1089 b = a ? true : false; // +1a +2c
1090 c = ((((a)))) ? !false : !b; // +1a +4c
1091 d = !this.m() ? !!a : (false); // +1a +1b +3c
1092 e = !(a) && b ? ((c)) : !d; // +1a +4c
1093 if ( this.m() ? a : !this.m() ) {} // +2b +3c
1094 if ( x > 0 ? !(false) : this.m() ) {} // +1b +3c
1095 if ( x > 0 && x != 3 ? !(a) : (!(b)) ) {} // +5c
1096 ",
1097 "foo.java",
1098 |metric| {
1099 // magnitude: sqrt(25 + 16 + 576) = sqrt(617)
1100 // space count: 1 (1 unit)
1101 insta::assert_json_snapshot!(
1102 metric.abc,
1103 @r#"
1104 {
1105 "assignments": 5,
1106 "branches": 4,
1107 "conditions": 24,
1108 "magnitude": 24.839484696748443,
1109 "value": 24.839484696748443,
1110 "assignments_average": 5.0,
1111 "branches_average": 4.0,
1112 "conditions_average": 24.0,
1113 "assignments_min": 5,
1114 "assignments_max": 5,
1115 "branches_min": 4,
1116 "branches_max": 4,
1117 "conditions_min": 24,
1118 "conditions_max": 24
1119 }
1120 "#
1121 );
1122 },
1123 );
1124 }
1125
1126 #[test]
1127 fn bash_assignments_only() {
1128 check_metrics::<BashParser>(
1129 "f() {
1130 a=1
1131 b=2
1132 c+=3
1133 }",
1134 "foo.sh",
1135 |metric| {
1136 insta::assert_json_snapshot!(
1137 metric.abc,
1138 @r#"
1139 {
1140 "assignments": 3,
1141 "branches": 0,
1142 "conditions": 0,
1143 "magnitude": 3.0,
1144 "value": 0.0,
1145 "assignments_average": 1.5,
1146 "branches_average": 0.0,
1147 "conditions_average": 0.0,
1148 "assignments_min": 0,
1149 "assignments_max": 3,
1150 "branches_min": 0,
1151 "branches_max": 0,
1152 "conditions_min": 0,
1153 "conditions_max": 0
1154 }
1155 "#
1156 );
1157 },
1158 );
1159 }
1160
1161 #[test]
1162 fn bash_commands_only() {
1163 check_metrics::<BashParser>(
1164 "f() {
1165 echo a
1166 ls
1167 }",
1168 "foo.sh",
1169 |metric| {
1170 insta::assert_json_snapshot!(
1171 metric.abc,
1172 @r#"
1173 {
1174 "assignments": 0,
1175 "branches": 2,
1176 "conditions": 0,
1177 "magnitude": 2.0,
1178 "value": 0.0,
1179 "assignments_average": 0.0,
1180 "branches_average": 1.0,
1181 "conditions_average": 0.0,
1182 "assignments_min": 0,
1183 "assignments_max": 0,
1184 "branches_min": 0,
1185 "branches_max": 2,
1186 "conditions_min": 0,
1187 "conditions_max": 0
1188 }
1189 "#
1190 );
1191 },
1192 );
1193 }
1194
1195 #[test]
1196 fn bash_control_flow_counts_conditions() {
1197 // Regression for #696: Bash control-flow branches are ABC
1198 // conditions (a Bash predicate is a command, so the branch keyword
1199 // is the only condition signal). Each mirrors a cyclomatic decision.
1200 //
1201 // expected: 4 conditions — `if` (1) + `elif` (1) + `while` (1) +
1202 // the non-wildcard case arm `a)` (1). The bare-`*)` wildcard arm is
1203 // the Bash analogue of `default:` and is excluded, exactly as the
1204 // cyclomatic standard count excludes it. No comparison / test
1205 // operators appear, so every condition here is control-flow.
1206 check_metrics::<BashParser>(
1207 "f() {
1208 if cmd; then
1209 echo a
1210 elif other; then
1211 echo b
1212 fi
1213 while running; do
1214 echo c
1215 done
1216 case \"$x\" in
1217 a) echo d ;;
1218 *) echo e ;;
1219 esac
1220 }",
1221 "foo.sh",
1222 |metric| {
1223 assert_eq!(metric.abc.conditions_sum(), 4);
1224 },
1225 );
1226 }
1227
1228 #[test]
1229 fn bash_conditions_mix() {
1230 // Exercises every condition path: `==` and `!=` inside `[[ ]]`,
1231 // arithmetic `<` inside `(( ))`, and the prefix `-z` test operator
1232 // inside `[ ]`. Each `if` body's `echo` contributes a branch.
1233 //
1234 // expected: 8 conditions — each of the four `if`s contributes one
1235 // for the control-flow branch (#696) plus one for its comparison /
1236 // test operator (`==`, `!=`, `<`, `-z`). 4 branches (one `echo`
1237 // each). magnitude = sqrt(4² + 8²) = sqrt(80).
1238 check_metrics::<BashParser>(
1239 "f() {
1240 if [[ \"$a\" == \"$b\" ]]; then
1241 echo eq
1242 fi
1243 if [[ \"$x\" != \"$y\" ]]; then
1244 echo ne
1245 fi
1246 if (( $a < $b )); then
1247 echo lt
1248 fi
1249 if [ -z \"$x\" ]; then
1250 echo empty
1251 fi
1252 }",
1253 "foo.sh",
1254 |metric| {
1255 assert_eq!(metric.abc.conditions_sum(), 8);
1256 assert_eq!(metric.abc.branches_sum(), 4);
1257 insta::assert_json_snapshot!(
1258 metric.abc,
1259 @r#"
1260 {
1261 "assignments": 0,
1262 "branches": 4,
1263 "conditions": 8,
1264 "magnitude": 8.94427190999916,
1265 "value": 0.0,
1266 "assignments_average": 0.0,
1267 "branches_average": 2.0,
1268 "conditions_average": 4.0,
1269 "assignments_min": 0,
1270 "assignments_max": 0,
1271 "branches_min": 0,
1272 "branches_max": 4,
1273 "conditions_min": 0,
1274 "conditions_max": 8
1275 }
1276 "#
1277 );
1278 },
1279 );
1280 }
1281
1282 #[test]
1283 fn bash_magnitude() {
1284 // Combined assignments + branches + conditions. The single `if`
1285 // contributes two conditions (the control-flow branch, #696, plus
1286 // the `==` operator), so magnitude = sqrt(2² + 1² + 2²) = sqrt(9).
1287 check_metrics::<BashParser>(
1288 "f() {
1289 a=1
1290 b=2
1291 if [[ \"$a\" == \"$b\" ]]; then
1292 echo eq
1293 fi
1294 }",
1295 "foo.sh",
1296 |metric| {
1297 assert_eq!(metric.abc.conditions_sum(), 2);
1298 insta::assert_json_snapshot!(
1299 metric.abc,
1300 @r#"
1301 {
1302 "assignments": 2,
1303 "branches": 1,
1304 "conditions": 2,
1305 "magnitude": 3.0,
1306 "value": 0.0,
1307 "assignments_average": 1.0,
1308 "branches_average": 0.5,
1309 "conditions_average": 1.0,
1310 "assignments_min": 0,
1311 "assignments_max": 2,
1312 "branches_min": 0,
1313 "branches_max": 1,
1314 "conditions_min": 0,
1315 "conditions_max": 2
1316 }
1317 "#
1318 );
1319 },
1320 );
1321 }
1322
1323 #[test]
1324 fn java_malformed_parenthesized_no_panic() {
1325 check_metrics::<JavaParser>("class A { void m() { if (( }) }", "foo.java", |metric| {
1326 // tree-sitter emits ERROR nodes for this malformed source, so no
1327 // IfStatement, branch, or condition is recognised — all counts are 0.
1328 // Primary goal: the unwrap-free path does not panic.
1329 assert_eq!(metric.abc.assignments(), 0);
1330 assert_eq!(metric.abc.branches(), 0);
1331 assert_eq!(metric.abc.conditions(), 0);
1332 assert_eq!(metric.abc.magnitude(), 0.0);
1333 });
1334 }
1335
1336 #[test]
1337 fn java_bool_returning_terminal_kinds_count() {
1338 // Companion to `csharp_bool_returning_terminal_kinds_count`
1339 // (issue #372 / lesson #19). Java's grammar wraps every
1340 // if/while/do condition in `parenthesized_expression`, so
1341 // the gap lived in `java_inspect_container`'s terminal-arm
1342 // recognizer: `FieldAccess` (`cfg.flag`), `CastExpression`
1343 // (`(boolean)v`), `ArrayAccess` (`flags[0]`), and
1344 // `InstanceofExpression` (`x instanceof Foo`) were never
1345 // counted. Java has no `await` or `is_pattern` analogues,
1346 // so the C# fix's five-kind set collapses to four here.
1347 //
1348 // expected: 4 conditions (one per `if`), 0 assignments,
1349 // 0 branches (no invocations).
1350 check_metrics::<JavaParser>(
1351 "class Cfg { boolean flag; }
1352 class A {
1353 void m(Object v, boolean[] flags, Cfg cfg) {
1354 if (cfg.flag) { }
1355 if ((boolean) v) { }
1356 if (v instanceof Cfg) { }
1357 if (flags[0]) { }
1358 }
1359 }",
1360 "foo.java",
1361 |metric| {
1362 assert_eq!(metric.abc.conditions_sum(), 4);
1363 assert_eq!(metric.abc.assignments_sum(), 0);
1364 assert_eq!(metric.abc.branches_sum(), 0);
1365 },
1366 );
1367 }
1368
1369 #[test]
1370 fn groovy_no_abc() {
1371 // Comment-only file has no executable code → all-zero ABC.
1372 check_metrics::<GroovyParser>(
1373 "// just a comment, no executable code",
1374 "foo.groovy",
1375 |metric| {
1376 assert_eq!(metric.abc.assignments_sum(), 0);
1377 assert_eq!(metric.abc.branches_sum(), 0);
1378 assert_eq!(metric.abc.conditions_sum(), 0);
1379 },
1380 );
1381 }
1382
1383 #[test]
1384 fn groovy_single_assignment() {
1385 // `int x = 1` is a local-variable declaration whose `=` counts
1386 // as one assignment (matches Java's semantics).
1387 check_metrics::<GroovyParser>("int x = 1", "foo.groovy", |metric| {
1388 assert_eq!(metric.abc.assignments_sum(), 1);
1389 assert_eq!(metric.abc.branches_sum(), 0);
1390 assert_eq!(metric.abc.conditions_sum(), 0);
1391 });
1392 }
1393
1394 #[test]
1395 fn groovy_assignments() {
1396 check_metrics::<GroovyParser>(
1397 "void f() {
1398 int a = 1
1399 int b = 2
1400 a = 3
1401 b = 4
1402 a += 1
1403 b -= 1
1404 }",
1405 "foo.groovy",
1406 |metric| {
1407 // Six `=` tokens total. The two `Final`-less local
1408 // var-decls (`int a = 1`, `int b = 2`) and the two
1409 // bare assignments (`a = 3`, `b = 4`) each contribute
1410 // one assignment via the `EQ` arm; the `+=` / `-=`
1411 // each contribute one via the compound-assign arm.
1412 assert_eq!(metric.abc.assignments_sum(), 6);
1413 },
1414 );
1415 }
1416
1417 #[test]
1418 fn groovy_branches() {
1419 check_metrics::<GroovyParser>(
1420 "void f() {
1421 doStuff()
1422 helper.invoke()
1423 new Worker()
1424 }",
1425 "foo.groovy",
1426 |metric| {
1427 // 2 method invocations + 1 object creation = 3 branches
1428 assert_eq!(metric.abc.branches_sum(), 3);
1429 },
1430 );
1431 }
1432
1433 #[test]
1434 fn groovy_conditions_in_if() {
1435 check_metrics::<GroovyParser>(
1436 "void f(int a) {
1437 if (a == 0) { println(a) }
1438 if (a >= 1) { println(a) }
1439 if (a != 2) { println(a) }
1440 }",
1441 "foo.groovy",
1442 |metric| {
1443 // Three relational ops = 3 conditions
1444 assert_eq!(metric.abc.conditions_sum(), 3);
1445 },
1446 );
1447 }
1448
1449 #[test]
1450 fn groovy_branches_with_juxt_call() {
1451 // Groovy's parens-less call form `println foo` must be counted
1452 // as a branch (`JuxtFunctionCall`).
1453 check_metrics::<GroovyParser>(
1454 "void f() {
1455 println 'hi'
1456 println 'bye'
1457 }",
1458 "foo.groovy",
1459 |metric| {
1460 // 2 juxt calls = 2 branches.
1461 assert_eq!(metric.abc.branches_sum(), 2);
1462 },
1463 );
1464 }
1465
1466 #[test]
1467 fn groovy_try_catch_conditions() {
1468 // Each `try` and `catch` keyword token contributes +1 to
1469 // conditions (mirrors Java).
1470 check_metrics::<GroovyParser>(
1471 "void f() {
1472 try {
1473 risky()
1474 } catch (Exception e) {
1475 handle(e)
1476 }
1477 }",
1478 "foo.groovy",
1479 |metric| {
1480 // try + catch = 2 conditions
1481 assert_eq!(metric.abc.conditions_sum(), 2);
1482 },
1483 );
1484 }
1485
1486 #[test]
1487 fn groovy_ternary_conditions() {
1488 check_metrics::<GroovyParser>(
1489 "void f(int x) {
1490 def y = x > 0 ? 1 : 2
1491 }",
1492 "foo.groovy",
1493 |metric| {
1494 // QMARK alone is +1 condition, plus the `>` condition = 2.
1495 assert_eq!(metric.abc.conditions_sum(), 2);
1496 },
1497 );
1498 }
1499
1500 #[test]
1501 fn groovy_constant_excluded_from_assignments() {
1502 // `final` declarations are not counted as assignments
1503 // (mirrors Java's `Final` handling).
1504 check_metrics::<GroovyParser>(
1505 "class A {
1506 final int CONST = 42
1507 int field = 0
1508 }",
1509 "foo.groovy",
1510 |metric| {
1511 // The `=` on `final int CONST = 42` is a constant
1512 // initialiser (skipped). Only `field = 0` counts.
1513 assert_eq!(metric.abc.assignments_sum(), 1);
1514 },
1515 );
1516 }
1517
1518 #[test]
1519 fn groovy_malformed_parenthesized_no_panic() {
1520 // Regression: malformed Groovy input must not panic the ABC
1521 // walker; the `spaces.rs` Unit fallback (lesson 9) covers
1522 // structural recovery. amaanq's grammar treats `def x = (((`
1523 // as a `local_variable_declaration` whose initialiser is the
1524 // first opening paren — the `=` still fires the assignment
1525 // arm.
1526 check_metrics::<GroovyParser>("def x = (((", "foo.groovy", |metric| {
1527 assert_eq!(metric.abc.assignments_sum(), 1);
1528 });
1529 }
1530
1531 #[test]
1532 fn groovy_bool_returning_terminal_kinds_count() {
1533 // Companion to `csharp_bool_returning_terminal_kinds_count`
1534 // (issue #372 / lesson #19). The dekobon Groovy grammar
1535 // shares Java's wrapping conventions for `FieldAccess` and
1536 // `InstanceofExpression`, but it splits casts into two
1537 // distinct kinds — `cast_expression` for the Groovy-idiomatic
1538 // `v as Boolean` and `parenthesized_type_cast` for the
1539 // Java-style `(boolean) v`. The grammar has no `await` or
1540 // `array_access` analogues, so the C# fix's five-kind set
1541 // collapses to four here (with the cast slot doubled).
1542 //
1543 // expected: 4 conditions (one per `if`), 0 assignments,
1544 // 0 branches (no invocations).
1545 check_metrics::<GroovyParser>(
1546 "class Cfg { boolean flag }
1547 class A {
1548 void m(Object v, Cfg cfg) {
1549 if (cfg.flag) { }
1550 if ((boolean) v) { }
1551 if (v as Boolean) { }
1552 if (v instanceof Cfg) { }
1553 }
1554 }",
1555 "foo.groovy",
1556 |metric| {
1557 assert_eq!(metric.abc.conditions_sum(), 4);
1558 assert_eq!(metric.abc.assignments_sum(), 0);
1559 assert_eq!(metric.abc.branches_sum(), 0);
1560 },
1561 );
1562 }
1563
1564 #[test]
1565 fn groovy_if_multiple_conditions() {
1566 // Mirrors `java_if_multiple_conditions`: `&&` / `||` chains
1567 // and parenthesised unary forms each contribute one
1568 // condition per primitive comparison; the inspect-container
1569 // pass picks up the unary `!a` / `!b` arguments inside the
1570 // `BinaryExpression` and counts them too.
1571 check_metrics::<GroovyParser>(
1572 "void f(boolean a, boolean b, boolean c) {
1573 if (a || b || c) { println(a) }
1574 if (a && b && c) { println(a) }
1575 if (!a && !b) { println(a) }
1576 }",
1577 "foo.groovy",
1578 |metric| {
1579 // Conditions counted via the AMPAMP/PIPEPIPE arms
1580 // (one count per identifier in the chain — three
1581 // for `||`, three for `&&`, two for the unary chain)
1582 // = 8.
1583 assert_eq!(metric.abc.conditions_sum(), 8);
1584 // Three `println a` juxt calls — each is a branch.
1585 assert_eq!(metric.abc.branches_sum(), 3);
1586 },
1587 );
1588 }
1589
1590 #[test]
1591 fn groovy_while_and_do_while_conditions() {
1592 // Covers the WhileStatement and DoStatement arms in
1593 // `impl Abc for GroovyCode`. Each `while` / `do-while` has
1594 // its condition inspected through `groovy_inspect_container`.
1595 check_metrics::<GroovyParser>(
1596 "void f(boolean a, boolean b) {
1597 while (a) {
1598 a = false
1599 }
1600 do {
1601 b = !b
1602 } while (b)
1603 }",
1604 "foo.groovy",
1605 |metric| {
1606 // `while(a)` + `while(b)` each contribute one condition;
1607 // the unary `!b` on the do body's right-hand side adds
1608 // one more via the assignment-arm inspection = 3.
1609 assert_eq!(metric.abc.conditions_sum(), 3);
1610 // Two assignments to existing variables (`a = false`,
1611 // `b = !b`).
1612 assert_eq!(metric.abc.assignments_sum(), 2);
1613 },
1614 );
1615 }
1616
1617 #[test]
1618 fn groovy_if_while_boolean_literal_condition() {
1619 // Regression for the Groovy half of #371-class bugs: the
1620 // dekobon tree-sitter-groovy grammar wraps a bare
1621 // `true` / `false` literal used as the condition of
1622 // `if` / `while` / `do` / `?:` in a `boolean_literal` node
1623 // (`Groovy::BooleanLiteral`, kind_id 270), not the leaf
1624 // `True` / `False` keyword tokens. `groovy_count_condition`
1625 // must therefore match `BooleanLiteral` (the wrapper).
1626 // Without that, every literal-condition statement silently
1627 // scored 0 conditions. Mirror of
1628 // `csharp_if_while_boolean_literal_condition`.
1629 check_metrics::<GroovyParser>(
1630 "void m() {
1631 if (true) { println 'a' }
1632 if (false) { println 'b' }
1633 while (true) { break }
1634 int t = true ? 1 : 0
1635 }",
1636 "foo.groovy",
1637 |metric| {
1638 // Four literal-condition statements contribute 4
1639 // `BooleanLiteral` conditions (if / if / while /
1640 // ternary), plus the ternary's `?` token adds one
1641 // more via `groovy_count_token_condition` → 5
1642 // total. The `println` calls contribute 2 branches
1643 // (the `while` body's `break` is not a branch).
1644 // The `int t = …` initializer contributes 1
1645 // assignment.
1646 assert_eq!(metric.abc.conditions_sum(), 5);
1647 assert_eq!(metric.abc.branches_sum(), 2);
1648 assert_eq!(metric.abc.assignments_sum(), 1);
1649 },
1650 );
1651 }
1652
1653 #[test]
1654 fn groovy_return_unary_boolean_literal() {
1655 // Companion to `groovy_if_while_boolean_literal_condition`:
1656 // a `!true` / `!false` operand inside a `return` statement
1657 // routes through `groovy_inspect_container` (via
1658 // `groovy_inspect_child(node, 1)` on the ReturnStatement).
1659 // The `!` operator establishes boolean context, then the
1660 // innermost-operand check matches `BooleanLiteral` — that
1661 // helper's `BooleanLiteral` arm must be present or the
1662 // count silently drops. Mutation-verified: removing
1663 // `BooleanLiteral` from `groovy_inspect_container` leaves
1664 // every other Groovy test passing.
1665 check_metrics::<GroovyParser>(
1666 "boolean f() {
1667 return !true
1668 }
1669 boolean g() {
1670 return !false
1671 }",
1672 "foo.groovy",
1673 |metric| {
1674 // Each `return !X` walks into
1675 // `groovy_inspect_container` with a UnaryExpression
1676 // wrapping a `BANG` + BooleanLiteral. The `!` arm
1677 // seeds `has_boolean_content = true` (ReturnStatement
1678 // is not a known-boolean parent), then the
1679 // BooleanLiteral operand contributes one condition.
1680 // Two `return !X` → 2 conditions, no branches, no
1681 // assignments.
1682 assert_eq!(metric.abc.conditions_sum(), 2);
1683 assert_eq!(metric.abc.branches_sum(), 0);
1684 assert_eq!(metric.abc.assignments_sum(), 0);
1685 },
1686 );
1687 }
1688
1689 #[test]
1690 fn groovy_short_circuit_with_boolean_literal_operand() {
1691 // Companion to `groovy_if_while_boolean_literal_condition`:
1692 // a bare `true` / `false` operand of `&&` / `||` lands in
1693 // `groovy_count_unary_conditions`, which iterates the
1694 // parent BinaryExpression's children. That helper must
1695 // match the `BooleanLiteral` wrapper just like
1696 // `groovy_count_condition` does — otherwise the operand
1697 // silently scores zero. Mutation-verified: removing
1698 // `BooleanLiteral` from the `groovy_count_unary_conditions`
1699 // arm leaves every other Groovy test passing.
1700 check_metrics::<GroovyParser>(
1701 "void m(boolean x) {
1702 if (x && true) { println 'a' }
1703 if (false || x) { println 'b' }
1704 }",
1705 "foo.groovy",
1706 |metric| {
1707 // `&&` and `||` themselves are NOT in
1708 // `groovy_count_token_condition`'s match list —
1709 // they route through
1710 // `groovy_walk_for_conditions::AMPAMP|PIPEPIPE`,
1711 // which calls `groovy_count_unary_conditions` on
1712 // the parent BinaryExpression. Each invocation
1713 // counts every child that matches the terminal-
1714 // operand kinds and whose parent is a
1715 // BinaryExpression. For `x && true`: Identifier x
1716 // (+1) + BooleanLiteral true (+1) = 2. For
1717 // `false || x`: BooleanLiteral false (+1) +
1718 // Identifier x (+1) = 2. Total 4.
1719 assert_eq!(metric.abc.conditions_sum(), 4);
1720 assert_eq!(metric.abc.branches_sum(), 2);
1721 assert_eq!(metric.abc.assignments_sum(), 0);
1722 },
1723 );
1724 }
1725
1726 #[test]
1727 fn groovy_methods_arguments_with_conditions() {
1728 // Mirror of `java_methods_arguments_with_conditions`: a
1729 // unary `!x` inside an argument list must count both the
1730 // method invocation as a branch AND the unary as a
1731 // condition. The `ArgumentList | ArgumentList2` arm in
1732 // `impl Abc for GroovyCode` is what exercises this.
1733 check_metrics::<GroovyParser>(
1734 "void f(boolean a, boolean b, boolean c) {
1735 m1(a)
1736 m1(!a)
1737 m2(!a, !b)
1738 }",
1739 "foo.groovy",
1740 |metric| {
1741 // 3 method invocations (m1, m1, m2) — each fires the
1742 // branches arm.
1743 assert_eq!(metric.abc.branches_sum(), 3);
1744 // Three `!` unaries — `m1(!a)` and the two args of
1745 // `m2(!a, !b)` — each contribute one condition via
1746 // the ArgumentList inspection.
1747 assert_eq!(metric.abc.conditions_sum(), 3);
1748 },
1749 );
1750 }
1751
1752 #[test]
1753 fn groovy_return_with_conditions() {
1754 // Mirror of `java_return_with_conditions`: a parenthesised
1755 // or unary expression inside `return` flows through the
1756 // `ReturnStatement` arm to `groovy_inspect_container`.
1757 check_metrics::<GroovyParser>(
1758 "boolean f(boolean a) {
1759 return (a)
1760 }
1761 boolean g(boolean a) {
1762 return !a
1763 }",
1764 "foo.groovy",
1765 |metric| {
1766 // Only one of the two return forms surfaces a
1767 // condition: `return !a` hits the UnaryExpression
1768 // path and adds one; `return (a)` reaches
1769 // `groovy_inspect_container` but the inner
1770 // identifier `a` is not in a boolean-context-firing
1771 // parent, so no condition is added.
1772 assert_eq!(metric.abc.conditions_sum(), 1);
1773 },
1774 );
1775 }
1776
1777 #[test]
1778 fn groovy_for_with_variable_declaration() {
1779 // Classical `for (int i = 0; cond; i++)` form. The init
1780 // slot's `int i = 0` is suppressed from assignments by the
1781 // `LocalVariableDeclaration` push/pop dance; the `i++` in
1782 // the update slot contributes one assignment via the
1783 // `PLUSPLUS` arm. The condition `i < 10` flows through the
1784 // `ForStatement` arm.
1785 check_metrics::<GroovyParser>(
1786 "void f() {
1787 for (int i = 0; i < 10; i++) {
1788 println(i)
1789 }
1790 }",
1791 "foo.groovy",
1792 |metric| {
1793 // `int i = 0` fires the EQ arm + `i++` fires the
1794 // PLUSPLUS arm = 2 assignments.
1795 assert_eq!(metric.abc.assignments_sum(), 2);
1796 // `i < 10` is one condition (the LT arm).
1797 assert_eq!(metric.abc.conditions_sum(), 1);
1798 },
1799 );
1800 }
1801
1802 #[test]
1803 fn groovy_eq_arm_increments_when_no_declaration() {
1804 // Bare reassignment of an already-declared variable: the
1805 // `EQ` arm fires when the declaration stack is empty
1806 // (`stats.declaration.last().is_none()`), so the `=` counts
1807 // as one assignment. Mirrors `java_eq_arm_increments_when_
1808 // declaration_stack_is_empty`.
1809 check_metrics::<GroovyParser>(
1810 "void f(int x) {
1811 x = 42
1812 }",
1813 "foo.groovy",
1814 |metric| {
1815 assert_eq!(metric.abc.assignments_sum(), 1);
1816 assert_eq!(metric.abc.branches_sum(), 0);
1817 assert_eq!(metric.abc.conditions_sum(), 0);
1818 },
1819 );
1820 }
1821
1822 #[test]
1823 fn csharp_constant_declarations() {
1824 check_metrics::<CsharpParser>(
1825 "class A {
1826 private const int X1 = 0, Y1 = 0;
1827 public const float PI = 3.14f;
1828 const string HELLO = \"Hello,\";
1829 protected string world = \" world!\";
1830 public float e = 2.718f;
1831 private int x2 = 1, y2 = 2;
1832 void M() {
1833 const int Z1 = 0, Z2 = 0, Z3 = 0;
1834 const float T = 0.0f;
1835 int z1 = 1, z2 = 2, z3 = 3;
1836 }
1837 }",
1838 "foo.cs",
1839 |metric| insta::assert_json_snapshot!(metric.abc),
1840 );
1841 }
1842
1843 #[test]
1844 fn csharp_declarations_with_conditions() {
1845 check_metrics::<CsharpParser>(
1846 "class A {
1847 bool a = (1 == 2);
1848 bool b = (1 < 2);
1849 bool c = !true;
1850 bool d = !false;
1851 }",
1852 "foo.cs",
1853 |metric| insta::assert_json_snapshot!(metric.abc),
1854 );
1855 }
1856
1857 #[test]
1858 fn csharp_assignments_with_conditions() {
1859 check_metrics::<CsharpParser>(
1860 "class A {
1861 void M() {
1862 int a = 0;
1863 a += 1;
1864 a -= 2;
1865 a *= 3;
1866 a /= 4;
1867 a %= 5;
1868 a++;
1869 a--;
1870 }
1871 }",
1872 "foo.cs",
1873 |metric| insta::assert_json_snapshot!(metric.abc),
1874 );
1875 }
1876
1877 #[test]
1878 fn csharp_methods_arguments_with_conditions() {
1879 check_metrics::<CsharpParser>(
1880 "class A {
1881 void M(int x, int y) {
1882 F(x == y, x < y, !x.Equals(y));
1883 }
1884 void F(bool a, bool b, bool c) {}
1885 }",
1886 "foo.cs",
1887 |metric| insta::assert_json_snapshot!(metric.abc),
1888 );
1889 }
1890
1891 #[test]
1892 fn csharp_if_single_conditions() {
1893 check_metrics::<CsharpParser>(
1894 "class A {
1895 void M(int x) {
1896 if (x > 0) { System.Console.WriteLine(\"a\"); }
1897 if (x < 0) { System.Console.WriteLine(\"b\"); }
1898 if (x == 0) { System.Console.WriteLine(\"c\"); }
1899 }
1900 }",
1901 "foo.cs",
1902 |metric| insta::assert_json_snapshot!(metric.abc),
1903 );
1904 }
1905
1906 #[test]
1907 fn csharp_if_multiple_conditions() {
1908 check_metrics::<CsharpParser>(
1909 "class A {
1910 void M(int x, int y) {
1911 if (x > 0 && y > 0) { System.Console.WriteLine(\"a\"); }
1912 if (x < 0 || y < 0) { System.Console.WriteLine(\"b\"); }
1913 }
1914 }",
1915 "foo.cs",
1916 |metric| insta::assert_json_snapshot!(metric.abc),
1917 );
1918 }
1919
1920 #[test]
1921 fn csharp_while_and_do_while_conditions() {
1922 check_metrics::<CsharpParser>(
1923 "class A {
1924 void M(int x) {
1925 while (x > 0) { x--; }
1926 do { x++; } while (x < 10);
1927 }
1928 }",
1929 "foo.cs",
1930 |metric| insta::assert_json_snapshot!(metric.abc),
1931 );
1932 }
1933
1934 #[test]
1935 fn csharp_return_with_conditions() {
1936 check_metrics::<CsharpParser>(
1937 "class A {
1938 bool M(int x) {
1939 return (x > 0);
1940 }
1941 bool N(int x) {
1942 return !(x < 0);
1943 }
1944 }",
1945 "foo.cs",
1946 |metric| insta::assert_json_snapshot!(metric.abc),
1947 );
1948 }
1949
1950 // C# `switch` *expression* arms scored zero ABC conditions before
1951 // #456 — they carry no `case` / `default` token, so the token-driven
1952 // `csharp_count_token_condition` never saw them, even though C#
1953 // cyclomatic counts each non-discard arm. Revert-verified: adding the
1954 // gated `SwitchExpressionArm` arm is what lifts this from 0 to 2. The
1955 // bare `_ =>` discard arm is excluded (the `default:` analogue),
1956 // mirroring the cyclomatic gate (lesson 11).
1957 #[test]
1958 fn csharp_switch_expression_arm_counts_condition() {
1959 check_metrics::<CsharpParser>(
1960 "class A {
1961 int M(int x) {
1962 return x switch { 1 => 10, 2 => 20, _ => 0 };
1963 }
1964 }",
1965 "foo.cs",
1966 |metric| {
1967 // arm `1 =>` (+1) + arm `2 =>` (+1) + `_ =>` discard (+0).
1968 assert_eq!(metric.abc.conditions_sum(), 2);
1969 },
1970 );
1971 }
1972
1973 // Cross-language parity (lesson 11): a C# `switch` expression and the
1974 // equivalent Java arrow-`switch` must report the same ABC condition
1975 // count on equivalent code. Both have two concrete case arms and no
1976 // fallback arm, so both must count exactly 2. `check_metrics` takes a
1977 // non-capturing `fn` pointer, so the shared expected value (2) is
1978 // asserted in each callback rather than compared across closures; the
1979 // matching constant is what enforces parity. This guards against the
1980 // C# fix drifting away from the Java arrow-case treatment.
1981 #[test]
1982 fn csharp_java_switch_arm_abc_parity() {
1983 // C# switch expression: two arms, no fallback → 2 conditions.
1984 check_metrics::<CsharpParser>(
1985 "class A {
1986 int M(int x) {
1987 return x switch { 1 => 10, 2 => 20 };
1988 }
1989 }",
1990 "foo.cs",
1991 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
1992 );
1993
1994 // Equivalent Java arrow-`switch`: two case arms, no default → 2.
1995 check_metrics::<JavaParser>(
1996 "class A {
1997 int m(int x) {
1998 return switch (x) { case 1 -> 10; case 2 -> 20; };
1999 }
2000 }",
2001 "foo.java",
2002 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2003 );
2004 }
2005
2006 // Issue #469: the `default` arm of a C-family `switch` is the
2007 // unconditional fallthrough and must NOT count as an ABC condition,
2008 // mirroring cyclomatic — which counts only the `Case` arms, never
2009 // the `Default` token.
2010 //
2011 // expected: each fixture is a single function whose switch has two
2012 // concrete `case` arms plus one `default`. ABC must count exactly
2013 // the two case arms (conditions = 2), matching cyclomatic's two
2014 // case-arm decisions. Pre-fix, every language below scored 3 (the
2015 // `Default` token leaked into the condition tally) — revert-verified
2016 // against the pre-#469 condition arms. We anchor on the integer
2017 // `conditions_sum()` headline (the value the public JSON serializes;
2018 // float magnitude is bit-brittle and excluded by the snapshot
2019 // policy). The cyclomatic side is pinned separately in
2020 // `java_csharp_cpp_switch_default_cyclomatic_parity` below, where
2021 // the per-space `cyclomatic()` decision count is isolated.
2022 #[test]
2023 fn java_switch_default_not_a_condition() {
2024 // Classic statement `default:`.
2025 check_metrics::<JavaParser>(
2026 "class A {
2027 int m(int x) {
2028 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2029 }
2030 }",
2031 "foo.java",
2032 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2033 );
2034 // Arrow `default ->` — shares the same `Default` token.
2035 check_metrics::<JavaParser>(
2036 "class A {
2037 int m(int x) {
2038 return switch (x) { case 1 -> 1; case 2 -> 2; default -> 0; };
2039 }
2040 }",
2041 "foo.java",
2042 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2043 );
2044 }
2045
2046 // Over-exclusion guard (issue #469): a statement `switch` with two
2047 // `case` arms and NO `default` must still count both cases. This
2048 // pins that the fix excludes only the `Default` token, never a
2049 // `Case` arm — the count is identical before and after #469 (two
2050 // cases → 2), so it would catch a fix that over-eagerly dropped a
2051 // real case (e.g. treating the trailing case as a fallthrough).
2052 // expected: case 1 (+1) + case 2 (+1) = 2.
2053 #[test]
2054 fn java_switch_without_default_counts_all_cases() {
2055 check_metrics::<JavaParser>(
2056 "class A {
2057 int m(int x) {
2058 switch (x) { case 1: return 1; case 2: return 2; }
2059 return -1;
2060 }
2061 }",
2062 "foo.java",
2063 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2064 );
2065 }
2066
2067 #[test]
2068 fn csharp_switch_default_not_a_condition() {
2069 check_metrics::<CsharpParser>(
2070 "class A {
2071 int M(int x) {
2072 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2073 }
2074 }",
2075 "foo.cs",
2076 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2077 );
2078 }
2079
2080 #[test]
2081 fn cpp_switch_default_not_a_condition() {
2082 // C++ (and plain C, which shares this grammar) already excluded
2083 // `default`; this pins the cross-language parity invariant.
2084 check_metrics::<CppParser>(
2085 "void f(int x) {
2086 switch (x) { case 1: return; case 2: return; default: return; }
2087 }",
2088 "foo.cpp",
2089 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2090 );
2091 }
2092
2093 #[test]
2094 fn objc_abc() {
2095 // ObjC ABC reuses the C/C++ walker with two additions: a message
2096 // send `[obj msg]` is a call (B), and `@try` / `@catch` count as
2097 // conditions (C) like C++ try/catch.
2098 // A: `int total = 0`, `int i = 0`, `i++`, `total = total + …`,
2099 // `total = -1` = 5.
2100 // B: `[self valueAt:i]`, `[self risky]` = 2 message sends.
2101 // C: `i < n`, `@try`, `@catch`, `total >= 0` = 4.
2102 check_metrics::<ObjcParser>(
2103 "@implementation Foo\n\
2104 - (int)bar:(int)n {\n\
2105 int total = 0;\n\
2106 for (int i = 0; i < n; i++) {\n\
2107 total = total + [self valueAt:i];\n\
2108 }\n\
2109 @try {\n\
2110 [self risky];\n\
2111 } @catch (NSException *e) {\n\
2112 total = -1;\n\
2113 }\n\
2114 if (total >= 0) {\n\
2115 return total;\n\
2116 }\n\
2117 return 0;\n\
2118 }\n\
2119 @end\n",
2120 "foo.m",
2121 |metric| {
2122 assert_eq!(metric.abc.assignments_sum(), 5);
2123 assert_eq!(metric.abc.branches_sum(), 2);
2124 assert_eq!(metric.abc.conditions_sum(), 4);
2125 },
2126 );
2127 }
2128
2129 #[test]
2130 fn objc_abc_conditions() {
2131 // Exercises the condition-slot arms shared with C/C++: a `while`
2132 // head, a `&&` chain, a `do … while` trailing condition, and a
2133 // `return <comparison>`. ObjC routes these through the same
2134 // grammar-agnostic `cpp_inspect_*` helpers.
2135 // A: `p++`, `p--` = 2. B: no calls = 0.
2136 // C: `p != 0`, `*p > 0`, `*p < 9`, `*p == 0` = 4.
2137 check_metrics::<ObjcParser>(
2138 "@implementation Foo\n\
2139 - (int)g:(int *)p {\n\
2140 while (p != 0 && *p > 0) {\n\
2141 p++;\n\
2142 }\n\
2143 do {\n\
2144 p--;\n\
2145 } while (*p < 9);\n\
2146 return *p == 0;\n\
2147 }\n\
2148 @end\n",
2149 "foo.m",
2150 |metric| {
2151 assert_eq!(metric.abc.assignments_sum(), 2);
2152 assert_eq!(metric.abc.branches_sum(), 0);
2153 assert_eq!(metric.abc.conditions_sum(), 4);
2154 },
2155 );
2156 }
2157
2158 #[test]
2159 fn objc_abc_message_send_unary_condition() {
2160 // A negated boolean passed as a message-send argument is a unary
2161 // condition (Fitzpatrick Rule 9), the same as in a C-call argument.
2162 // Message args are direct children of `message_expression` (no
2163 // `argument_list`), so they are inspected in the `MessageExpression`
2164 // arm. Here: `[self use:!a]` (1 call + 1 unary condition) +
2165 // `cFunc(!a)` (1 call + 1 unary condition) → B=2, C=2.
2166 check_metrics::<ObjcParser>(
2167 "@implementation Foo\n\
2168 - (void)bar:(int)a {\n\
2169 [self use:!a];\n\
2170 cFunc(!a);\n\
2171 }\n\
2172 @end\n",
2173 "foo.m",
2174 |metric| {
2175 assert_eq!(metric.abc.branches_sum(), 2);
2176 assert_eq!(metric.abc.conditions_sum(), 2);
2177 },
2178 );
2179 }
2180
2181 #[test]
2182 fn groovy_switch_default_not_a_condition() {
2183 check_metrics::<GroovyParser>(
2184 "class A {
2185 int m(int x) {
2186 switch (x) { case 1: return 1; case 2: return 2; default: return 0 }
2187 }
2188 }",
2189 "foo.groovy",
2190 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2191 );
2192 }
2193
2194 #[test]
2195 fn js_switch_default_not_a_condition() {
2196 check_metrics::<JavascriptParser>(
2197 "function f(x) {
2198 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2199 }",
2200 "foo.js",
2201 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2202 );
2203 }
2204
2205 #[test]
2206 fn ts_switch_default_not_a_condition() {
2207 check_metrics::<TypescriptParser>(
2208 "function f(x: number): number {
2209 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2210 }",
2211 "foo.ts",
2212 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2213 );
2214 }
2215
2216 // Cross-language parity (lesson 11): the equivalent statement-`switch`
2217 // with a `default` arm reports the same ABC condition count across
2218 // Java / C# / C++. All three have two concrete case arms plus a
2219 // fallthrough `default`, so all three must count exactly 2 conditions
2220 // (the `default` excluded). `check_metrics` takes a non-capturing
2221 // `fn` pointer, so the shared expected value is asserted in each
2222 // callback; the matching constant is what enforces parity.
2223 #[test]
2224 fn java_csharp_cpp_switch_default_abc_parity() {
2225 check_metrics::<JavaParser>(
2226 "class A {
2227 int m(int x) {
2228 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2229 }
2230 }",
2231 "foo.java",
2232 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2233 );
2234 check_metrics::<CsharpParser>(
2235 "class A {
2236 int M(int x) {
2237 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2238 }
2239 }",
2240 "foo.cs",
2241 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2242 );
2243 check_metrics::<CppParser>(
2244 "void f(int x) {
2245 switch (x) { case 1: return; case 2: return; default: return; }
2246 }",
2247 "foo.cpp",
2248 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2249 );
2250 }
2251
2252 // Pins the ABC-vs-cyclomatic agreement the fix is about (lesson 11):
2253 // on the method's own function space, the cyclomatic decision count
2254 // (`cyclomatic()` minus the per-space base of 1) must equal the ABC
2255 // `conditions()` for the same switch. Both must be 2 — the two case
2256 // arms — with the `default` excluded from each. Revert-verified: pre-
2257 // #469 ABC `conditions()` was 3 here while cyclomatic stayed at 2.
2258 #[test]
2259 fn java_csharp_cpp_switch_default_cyclomatic_parity() {
2260 // Recurse to the deepest function space (the method holding the
2261 // switch) and assert per-space cyclomatic decisions == ABC
2262 // conditions.
2263 fn assert_deepest(space: &crate::FuncSpace) {
2264 if let Some(child) = space.spaces.last() {
2265 assert_deepest(child);
2266 return;
2267 }
2268 // Per-space cyclomatic base is 1; the two case arms add 2.
2269 let decisions = space.metrics.cyclomatic.cyclomatic() - 1;
2270 assert_eq!(decisions, 2);
2271 assert_eq!(space.metrics.abc.conditions(), decisions);
2272 }
2273 check_func_space::<JavaParser, _>(
2274 "class A {
2275 int m(int x) {
2276 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2277 }
2278 }",
2279 "foo.java",
2280 |space| assert_deepest(&space),
2281 );
2282 check_func_space::<CsharpParser, _>(
2283 "class A {
2284 int M(int x) {
2285 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2286 }
2287 }",
2288 "foo.cs",
2289 |space| assert_deepest(&space),
2290 );
2291 check_func_space::<CppParser, _>(
2292 "void f(int x) {
2293 switch (x) { case 1: return; case 2: return; default: return; }
2294 }",
2295 "foo.cpp",
2296 |space| assert_deepest(&space),
2297 );
2298 }
2299
2300 // Issue #473: PHP `switch` `default:` (`DefaultStatement`) is the
2301 // unconditional fallthrough, not a condition. ABC `conditions()` must
2302 // equal the cyclomatic decision count (`cyclomatic() - 1`) on the
2303 // function's own space — both 2 for the two `case` arms, with the
2304 // `default` excluded. Revert-verified: re-adding `DefaultStatement` to
2305 // the PHP ABC condition arm makes `conditions()` 3 here while cyclomatic
2306 // stays at 2, failing the invariant.
2307 #[test]
2308 fn php_switch_default_not_a_condition() {
2309 check_func_space::<PhpParser, _>(
2310 "<?php
2311 function f($x) {
2312 switch ($x) {
2313 case 1: return 1;
2314 case 2: return 2;
2315 default: return 0;
2316 }
2317 }",
2318 "foo.php",
2319 |space| {
2320 fn assert_deepest(space: &crate::FuncSpace) {
2321 if let Some(child) = space.spaces.last() {
2322 assert_deepest(child);
2323 return;
2324 }
2325 let decisions = space.metrics.cyclomatic.cyclomatic() - 1;
2326 assert_eq!(decisions, 2);
2327 assert_eq!(space.metrics.abc.conditions(), decisions);
2328 }
2329 assert_deepest(&space);
2330 },
2331 );
2332 }
2333
2334 // Issue #473: PHP `match` `default =>` (`MatchDefaultExpression`) is the
2335 // unconditional fallthrough, mirroring the switch `default:` case above.
2336 // ABC `conditions()` must equal the cyclomatic decision count
2337 // (`cyclomatic() - 1`) — both 2 for the two non-default match arms.
2338 // Revert-verified: re-adding `MatchDefaultExpression` to the PHP ABC
2339 // condition arm makes `conditions()` 3 here while cyclomatic stays at 2.
2340 #[test]
2341 fn php_match_default_not_a_condition() {
2342 check_func_space::<PhpParser, _>(
2343 "<?php
2344 function g($x) {
2345 return match ($x) {
2346 1 => \"a\",
2347 2 => \"b\",
2348 default => \"z\",
2349 };
2350 }",
2351 "foo.php",
2352 |space| {
2353 fn assert_deepest(space: &crate::FuncSpace) {
2354 if let Some(child) = space.spaces.last() {
2355 assert_deepest(child);
2356 return;
2357 }
2358 let decisions = space.metrics.cyclomatic.cyclomatic() - 1;
2359 assert_eq!(decisions, 2);
2360 assert_eq!(space.metrics.abc.conditions(), decisions);
2361 }
2362 assert_deepest(&space);
2363 },
2364 );
2365 }
2366
2367 #[test]
2368 fn csharp_if_bare_identifier_condition() {
2369 check_metrics::<CsharpParser>(
2370 "class A {
2371 void M(bool x) {
2372 if (x) { System.Console.WriteLine(\"a\"); }
2373 }
2374 }",
2375 "foo.cs",
2376 |metric| {
2377 // `if (x)` contributes 1 condition (bare identifier).
2378 // `System.Console.WriteLine(...)` is the only call → 1 branch.
2379 // `*_sum()` is what the public JSON serializes as the
2380 // headline value (see `crate::wire::Abc`).
2381 assert_eq!(metric.abc.conditions_sum(), 1);
2382 assert_eq!(metric.abc.branches_sum(), 1);
2383 assert_eq!(metric.abc.assignments_sum(), 0);
2384 },
2385 );
2386 }
2387
2388 #[test]
2389 fn csharp_while_bare_identifier_condition() {
2390 check_metrics::<CsharpParser>(
2391 "class A {
2392 void M(bool x) {
2393 while (x) { x = false; }
2394 }
2395 }",
2396 "foo.cs",
2397 |metric| {
2398 // `while (x)` contributes 1 condition; `x = false` is 1 assignment.
2399 assert_eq!(metric.abc.conditions_sum(), 1);
2400 assert_eq!(metric.abc.assignments_sum(), 1);
2401 assert_eq!(metric.abc.branches_sum(), 0);
2402 },
2403 );
2404 }
2405
2406 #[test]
2407 fn csharp_do_while_bare_identifier_condition() {
2408 check_metrics::<CsharpParser>(
2409 "class A {
2410 void M(bool x) {
2411 do { x = true; } while (x);
2412 }
2413 }",
2414 "foo.cs",
2415 |metric| {
2416 // `do { ... } while (x)` contributes 1 condition;
2417 // `x = true` is 1 assignment.
2418 assert_eq!(metric.abc.conditions_sum(), 1);
2419 assert_eq!(metric.abc.assignments_sum(), 1);
2420 assert_eq!(metric.abc.branches_sum(), 0);
2421 },
2422 );
2423 }
2424
2425 #[test]
2426 fn csharp_if_unary_not_condition() {
2427 // Two cases share one test:
2428 //
2429 // if (!x) { … } — IfStatement is a known-boolean parent, so
2430 // the unary `!` arm in `csharp_inspect_container` is *one of
2431 // two* ways `has_boolean_content` gets set to true (the parent
2432 // seed sets it before the `!` does). A regression that broke
2433 // only the `is_not` branch wouldn't show up here.
2434 //
2435 // return !x; — ReturnStatement is *not* in the boolean-context
2436 // seed list (BinaryExpression | IfStatement | WhileStatement |
2437 // DoStatement | ForStatement | ConditionalExpression). So the
2438 // `!` wrapper is the *only* path that sets
2439 // `has_boolean_content = true`. Asserting the `return !x;`
2440 // case isolates the unary-unwrap logic from the parent-seed
2441 // path.
2442 check_metrics::<CsharpParser>(
2443 "class A {
2444 void M(bool x) {
2445 if (!x) { System.Console.WriteLine(\"a\"); }
2446 }
2447 bool N(bool x) {
2448 return !x;
2449 }
2450 }",
2451 "foo.cs",
2452 |metric| {
2453 // `if (!x)` contributes 1 condition (PrefixUnaryExpression
2454 // path with parent IfStatement seeding has_boolean_content).
2455 // `return !x;` contributes 1 condition (parent doesn't seed
2456 // — the unary `!` is the only path that sets the flag).
2457 // → 2 conditions total. 1 branch from WriteLine().
2458 assert_eq!(metric.abc.conditions_sum(), 2);
2459 assert_eq!(metric.abc.branches_sum(), 1);
2460 assert_eq!(metric.abc.assignments_sum(), 0);
2461 },
2462 );
2463 }
2464
2465 #[test]
2466 fn csharp_if_double_parenthesized_condition() {
2467 // Audit-tests follow-up: with only the
2468 // `csharp_prefix_unary_expr_kinds!()` arm covered by
2469 // `csharp_if_unary_not_condition`, the
2470 // `csharp_paren_expr_kinds!()` delegation arm in
2471 // `csharp_count_condition` was a pure dead-code candidate —
2472 // disabling it caused zero existing tests to fail (verified
2473 // 2026-05-26).
2474 //
2475 // `if ((x))` puts a `ParenthesizedExpression` at child(2) of
2476 // the IfStatement (child(1) is the literal `(`, child(2) is
2477 // the inner parenthesised expression, child(3) is the literal
2478 // `)`). `csharp_count_condition` must route that case to
2479 // `csharp_inspect_container`, which then sees parent =
2480 // IfStatement, seeds `has_boolean_content = true`, walks to
2481 // the inner Identifier, and counts it. A regression that
2482 // removed the paren arm would silently score 0.
2483 check_metrics::<CsharpParser>(
2484 "class A {
2485 void M(bool x) {
2486 if ((x)) { System.Console.WriteLine(\"a\"); }
2487 }
2488 }",
2489 "foo.cs",
2490 |metric| {
2491 assert_eq!(metric.abc.conditions_sum(), 1);
2492 assert_eq!(metric.abc.branches_sum(), 1);
2493 assert_eq!(metric.abc.assignments_sum(), 0);
2494 },
2495 );
2496 }
2497
2498 #[test]
2499 fn csharp_bool_returning_terminal_kinds_count() {
2500 // Regression for issue #372 (lesson #19): before the fix,
2501 // `csharp_count_condition` / `csharp_inspect_container` only
2502 // recognised invocation / identifier / boolean literal as
2503 // terminal-bool operands, so the five idiomatic boolean
2504 // expressions in the `if (...)` slots below silently scored
2505 // zero conditions:
2506 //
2507 // - `cfg.flag` — MemberAccessExpression
2508 // - `await c.Check()` — AwaitExpression
2509 // - `(bool)v` — CastExpression
2510 // - `v is not null` — IsPatternExpression
2511 // - `flags[0]` — ElementAccessExpression
2512 //
2513 // expected: 5 conditions (one per `if`), 0 assignments,
2514 // 1 branch (the single `c.Check()` invocation; the other
2515 // `if`-condition expressions are not invocations).
2516 check_metrics::<CsharpParser>(
2517 "using System.Threading.Tasks;
2518 class A {
2519 async Task M(object v, bool[] flags, Cfg cfg, C c) {
2520 if (cfg.flag) { }
2521 if (await c.Check()) { }
2522 if ((bool)v) { }
2523 if (v is not null) { }
2524 if (flags[0]) { }
2525 }
2526 }
2527 class Cfg { public bool flag; }
2528 class C { public Task<bool> Check() => null; }",
2529 "foo.cs",
2530 |metric| {
2531 assert_eq!(metric.abc.conditions_sum(), 5);
2532 assert_eq!(metric.abc.assignments_sum(), 0);
2533 assert_eq!(metric.abc.branches_sum(), 1);
2534 },
2535 );
2536 }
2537
2538 #[test]
2539 fn csharp_if_method_call_condition() {
2540 check_metrics::<CsharpParser>(
2541 "class A {
2542 void M(string s) {
2543 if (s.StartsWith(\"x\")) { System.Console.WriteLine(\"a\"); }
2544 }
2545 }",
2546 "foo.cs",
2547 |metric| {
2548 // `if (s.StartsWith("x"))` contributes 1 condition
2549 // (InvocationExpression) plus 1 branch for the call itself,
2550 // plus 1 branch for WriteLine.
2551 assert_eq!(metric.abc.conditions_sum(), 1);
2552 assert_eq!(metric.abc.branches_sum(), 2);
2553 assert_eq!(metric.abc.assignments_sum(), 0);
2554 },
2555 );
2556 }
2557
2558 #[test]
2559 fn csharp_if_while_boolean_literal_condition() {
2560 // Regression for #371: the tree-sitter-c-sharp grammar wraps a
2561 // bare `true` / `false` literal used as the condition of
2562 // `if` / `while` / `do` / `?:` in a `boolean_literal` node,
2563 // not the leaf `true` / `false` tokens. `csharp_count_condition`
2564 // must therefore match `BooleanLiteral` (the wrapper),
2565 // mirroring the existing `csharp_walk_for_statement` arm.
2566 // Without that, every literal-condition statement scored 0
2567 // conditions. The sibling `csharp_count_unary_conditions`
2568 // arm is covered separately by
2569 // `csharp_short_circuit_with_boolean_literal_operand` and
2570 // `csharp_inspect_container` is covered by
2571 // `csharp_declarations_with_conditions` (`!true` / `!false`).
2572 check_metrics::<CsharpParser>(
2573 "class A {
2574 void M() {
2575 if (true) { System.Console.WriteLine(\"a\"); }
2576 if (false) { System.Console.WriteLine(\"b\"); }
2577 while (true) { break; }
2578 do { break; } while (false);
2579 int t = true ? 1 : 0;
2580 }
2581 }",
2582 "foo.cs",
2583 |metric| {
2584 // Five literal-condition statements contribute 5
2585 // `BooleanLiteral` conditions (one per if/if/while/
2586 // do-while/ternary), plus the ternary's `?` token
2587 // adds one more via `csharp_count_token_condition`
2588 // → 6 total. The two `System.Console.WriteLine`
2589 // calls contribute 2 branches; the `int t = …`
2590 // initializer contributes 1 assignment.
2591 assert_eq!(metric.abc.conditions_sum(), 6);
2592 assert_eq!(metric.abc.branches_sum(), 2);
2593 assert_eq!(metric.abc.assignments_sum(), 1);
2594 },
2595 );
2596 }
2597
2598 #[test]
2599 fn csharp_short_circuit_with_boolean_literal_operand() {
2600 // Regression for #371 (companion to
2601 // `csharp_if_while_boolean_literal_condition`): a bare
2602 // `true` / `false` operand of `&&` / `||` lands in
2603 // `csharp_count_unary_conditions`, which iterates the parent
2604 // BinaryExpression's children. That helper must match the
2605 // `BooleanLiteral` wrapper just like `csharp_count_condition`
2606 // does — otherwise the operand silently scores zero. Mutation-
2607 // verified: removing `BooleanLiteral` from the
2608 // `csharp_count_unary_conditions` arm leaves every other test
2609 // in the suite passing, so this is the only test guarding
2610 // that helper's literal-operand path.
2611 check_metrics::<CsharpParser>(
2612 "class A {
2613 void M(bool x) {
2614 if (x && true) { System.Console.WriteLine(\"a\"); }
2615 if (false || x) { System.Console.WriteLine(\"b\"); }
2616 }
2617 }",
2618 "foo.cs",
2619 |metric| {
2620 // `&&` and `||` themselves are NOT in
2621 // `csharp_count_token_condition`'s match list — they
2622 // route through `csharp_walk_for_conditions::AMPAMP|
2623 // PIPEPIPE`, which calls
2624 // `csharp_count_unary_conditions` on the parent
2625 // BinaryExpression. Each invocation counts every
2626 // child that matches the terminal-operand kinds and
2627 // whose parent is a BinaryExpression. For
2628 // `x && true`: 1 (Identifier x) + 1 (BooleanLiteral
2629 // true) = 2. For `false || x`: 1 (BooleanLiteral
2630 // false) + 1 (Identifier x) = 2. Total 4. Without
2631 // the BooleanLiteral arm only the two Identifier
2632 // counts would land, giving 2.
2633 assert_eq!(metric.abc.conditions_sum(), 4);
2634 assert_eq!(metric.abc.branches_sum(), 2);
2635 assert_eq!(metric.abc.assignments_sum(), 0);
2636 },
2637 );
2638 }
2639
2640 #[test]
2641 fn csharp_return_without_conditions() {
2642 check_metrics::<CsharpParser>(
2643 "class A {
2644 int M() { return 42; }
2645 string N() { return \"hi\"; }
2646 }",
2647 "foo.cs",
2648 |metric| insta::assert_json_snapshot!(metric.abc),
2649 );
2650 }
2651
2652 #[test]
2653 fn csharp_lambda_expressions_return_with_conditions() {
2654 check_metrics::<CsharpParser>(
2655 "class A {
2656 public void M() {
2657 System.Func<int, bool> f = x => (x > 0);
2658 System.Func<int, bool> g = x => !(x < 0);
2659 }
2660 }",
2661 "foo.cs",
2662 |metric| insta::assert_json_snapshot!(metric.abc),
2663 );
2664 }
2665
2666 #[test]
2667 fn csharp_for_with_variable_declaration() {
2668 check_metrics::<CsharpParser>(
2669 "class A {
2670 void M() {
2671 for (int i = 0; i < 10; i++) {
2672 System.Console.WriteLine(i);
2673 }
2674 }
2675 }",
2676 "foo.cs",
2677 |metric| insta::assert_json_snapshot!(metric.abc),
2678 );
2679 }
2680
2681 #[test]
2682 fn csharp_for_without_variable_declaration() {
2683 check_metrics::<CsharpParser>(
2684 "class A {
2685 void M() {
2686 int i;
2687 for (i = 0; i < 10; i++) {
2688 System.Console.WriteLine(i);
2689 }
2690 }
2691 }",
2692 "foo.cs",
2693 |metric| insta::assert_json_snapshot!(metric.abc),
2694 );
2695 }
2696
2697 #[test]
2698 fn csharp_for_identifier_condition() {
2699 check_metrics::<CsharpParser>(
2700 "class A {
2701 void M(bool ready) {
2702 for (; ready ;) { }
2703 }
2704 }",
2705 "foo.cs",
2706 |metric| {
2707 // expected: assignments=0 (no `=` / `++` / `--`),
2708 // branches=0 (no invocation / object creation),
2709 // conditions=1 (bare-identifier for-loop condition).
2710 // Averages divide by 3 spaces (top-level + class + method).
2711 insta::assert_json_snapshot!(
2712 metric.abc,
2713 @r#"
2714 {
2715 "assignments": 0,
2716 "branches": 0,
2717 "conditions": 1,
2718 "magnitude": 1.0,
2719 "value": 0.0,
2720 "assignments_average": 0.0,
2721 "branches_average": 0.0,
2722 "conditions_average": 0.3333333333333333,
2723 "assignments_min": 0,
2724 "assignments_max": 0,
2725 "branches_min": 0,
2726 "branches_max": 0,
2727 "conditions_min": 0,
2728 "conditions_max": 1
2729 }
2730 "#
2731 );
2732 },
2733 );
2734 }
2735
2736 #[test]
2737 fn csharp_for_invocation_condition() {
2738 check_metrics::<CsharpParser>(
2739 "class A {
2740 bool Ok() { return true; }
2741 void M() {
2742 for (; Ok() ;) { }
2743 }
2744 }",
2745 "foo.cs",
2746 |metric| {
2747 // expected: assignments=0, branches=1 (the `Ok()` call),
2748 // conditions=1 (invocation as for-loop condition).
2749 // Averages divide by 4 spaces (top-level + class + two
2750 // methods).
2751 insta::assert_json_snapshot!(
2752 metric.abc,
2753 @r#"
2754 {
2755 "assignments": 0,
2756 "branches": 1,
2757 "conditions": 1,
2758 "magnitude": 1.4142135623730951,
2759 "value": 0.0,
2760 "assignments_average": 0.0,
2761 "branches_average": 0.25,
2762 "conditions_average": 0.25,
2763 "assignments_min": 0,
2764 "assignments_max": 0,
2765 "branches_min": 0,
2766 "branches_max": 1,
2767 "conditions_min": 0,
2768 "conditions_max": 1
2769 }
2770 "#
2771 );
2772 },
2773 );
2774 }
2775
2776 // Regression coverage for #279: the C# grammar wraps a literal
2777 // `true` / `false` for-loop condition in a `boolean_literal` node.
2778 // The `BooleanLiteral` arm in the `ForStatement` dispatch must
2779 // attribute one condition; without it, `for (; true ;)` would
2780 // contribute 0 (the bug fixed by this commit also affected this
2781 // shape).
2782 #[test]
2783 fn csharp_for_boolean_literal_condition() {
2784 check_metrics::<CsharpParser>(
2785 "class A {
2786 void M() {
2787 for (; true ;) { }
2788 }
2789 }",
2790 "foo.cs",
2791 |metric| {
2792 // expected: assignments=0, branches=0,
2793 // conditions=1 (the `true` literal as condition).
2794 assert_eq!(metric.abc.conditions_sum(), 1);
2795 assert_eq!(metric.abc.assignments_sum(), 0);
2796 assert_eq!(metric.abc.branches_sum(), 0);
2797 },
2798 );
2799 }
2800
2801 // Regression coverage for #279: an empty for-loop condition such as
2802 // `for (; ;) {}` must contribute 0 to conditions — there is no
2803 // condition node to count.
2804 #[test]
2805 fn csharp_for_empty_condition() {
2806 check_metrics::<CsharpParser>(
2807 "class A {
2808 void M() {
2809 for (; ;) { }
2810 }
2811 }",
2812 "foo.cs",
2813 |metric| {
2814 // expected: assignments=0, branches=0, conditions=0
2815 // (no condition expression in `for (; ;)`).
2816 insta::assert_json_snapshot!(
2817 metric.abc,
2818 @r#"
2819 {
2820 "assignments": 0,
2821 "branches": 0,
2822 "conditions": 0,
2823 "magnitude": 0.0,
2824 "value": 0.0,
2825 "assignments_average": 0.0,
2826 "branches_average": 0.0,
2827 "conditions_average": 0.0,
2828 "assignments_min": 0,
2829 "assignments_max": 0,
2830 "branches_min": 0,
2831 "branches_max": 0,
2832 "conditions_min": 0,
2833 "conditions_max": 0
2834 }
2835 "#
2836 );
2837 },
2838 );
2839 }
2840
2841 #[test]
2842 fn csharp_ternary_conditions() {
2843 check_metrics::<CsharpParser>(
2844 "class A {
2845 int Sign(int x) {
2846 return (x > 0) ? 1 : (x < 0 ? -1 : 0);
2847 }
2848 }",
2849 "foo.cs",
2850 |metric| insta::assert_json_snapshot!(metric.abc),
2851 );
2852 }
2853
2854 #[test]
2855 fn csharp_malformed_parenthesized_no_panic() {
2856 check_metrics::<CsharpParser>("class A { void M() { if (( }) }", "foo.cs", |metric| {
2857 // Don't panic on malformed source.
2858 assert_eq!(metric.abc.assignments(), 0);
2859 assert_eq!(metric.abc.branches(), 0);
2860 });
2861 }
2862
2863 #[test]
2864 fn csharp_function_pointer_type_no_double_count() {
2865 // EC1 extension — `<` and `>` are also parameter-list delimiters
2866 // for unsafe function-pointer types. `FunctionPointerType` must
2867 // be in the LT/GT exclusion list, otherwise these brackets
2868 // accumulate spurious `conditions` counts.
2869 check_metrics::<CsharpParser>(
2870 "unsafe class A {
2871 public delegate*<int, int, int> Adder;
2872 public delegate*<string, void> Logger;
2873 }",
2874 "foo.cs",
2875 |metric| {
2876 assert_eq!(
2877 metric.abc.conditions(),
2878 0,
2879 "function-pointer-type angle brackets must not count"
2880 );
2881 },
2882 );
2883 }
2884
2885 #[test]
2886 fn csharp_generic_type_args_no_double_count() {
2887 // EC1 — `<` and `>` inside TypeArgumentList must not count as
2888 // boolean conditions.
2889 check_metrics::<CsharpParser>(
2890 "class A {
2891 void M(System.Collections.Generic.Dictionary<string, System.Collections.Generic.List<int>> d) {
2892 System.Console.WriteLine(d);
2893 }
2894 }",
2895 "foo.cs",
2896 |metric| insta::assert_json_snapshot!(metric.abc),
2897 );
2898 }
2899
2900 #[test]
2901 fn csharp_aliased_invocation_expression_branches() {
2902 // Regression for issue #94 (lesson #2): the C# grammar emits three
2903 // aliased `kind_id`s for `invocation_expression`. Code that matches
2904 // only the unsuffixed `Csharp::InvocationExpression` undercounts ABC
2905 // branches whenever the AST emits an aliased variant. The three
2906 // method calls live in `M`, so the per-method maximum (visible at
2907 // the unit-space aggregate as `branches_max`) must be 3.
2908 check_metrics::<CsharpParser>(
2909 "class A {
2910 void M() {
2911 System.Console.WriteLine(1);
2912 System.Console.WriteLine(2);
2913 System.Console.WriteLine(3);
2914 }
2915 }",
2916 "foo.cs",
2917 |metric| {
2918 assert_eq!(metric.abc.branches_max(), 3);
2919 assert_eq!(metric.abc.conditions_max(), 0);
2920 },
2921 );
2922 }
2923
2924 #[test]
2925 fn php_zero_abc() {
2926 check_metrics::<PhpParser>("<?php\n", "foo.php", |metric| {
2927 assert_eq!(metric.abc.assignments_sum(), 0);
2928 assert_eq!(metric.abc.branches_sum(), 0);
2929 assert_eq!(metric.abc.conditions_sum(), 0);
2930 insta::assert_json_snapshot!(metric.abc);
2931 });
2932 }
2933
2934 #[test]
2935 fn php_simple_assignment() {
2936 check_metrics::<PhpParser>(
2937 "<?php
2938function f(): void {
2939 $a = 1;
2940 $b = 2;
2941}",
2942 "foo.php",
2943 |metric| insta::assert_json_snapshot!(metric.abc),
2944 );
2945 }
2946
2947 #[test]
2948 fn php_augmented_assignment() {
2949 check_metrics::<PhpParser>(
2950 "<?php
2951function f(int $x): int {
2952 $a = 0;
2953 $a += $x;
2954 $a -= 1;
2955 $a *= 2;
2956 return $a;
2957}",
2958 "foo.php",
2959 |metric| insta::assert_json_snapshot!(metric.abc),
2960 );
2961 }
2962
2963 #[test]
2964 fn php_const_excluded() {
2965 // Constant declarations and enum cases are NOT counted as
2966 // assignments — they declare immutable values.
2967 check_metrics::<PhpParser>(
2968 "<?php
2969class A {
2970 const PI = 3.14;
2971 const E = 2.71;
2972}
2973enum Color {
2974 case Red;
2975 case Green;
2976}",
2977 "foo.php",
2978 |metric| insta::assert_json_snapshot!(metric.abc),
2979 );
2980 }
2981
2982 #[test]
2983 fn php_function_call() {
2984 check_metrics::<PhpParser>(
2985 "<?php
2986function f(): void {
2987 foo();
2988 bar(1, 2);
2989}",
2990 "foo.php",
2991 |metric| insta::assert_json_snapshot!(metric.abc),
2992 );
2993 }
2994
2995 #[test]
2996 fn php_method_call() {
2997 check_metrics::<PhpParser>(
2998 "<?php
2999function f($obj): void {
3000 $obj->m1();
3001 $obj->m2(1);
3002}",
3003 "foo.php",
3004 |metric| insta::assert_json_snapshot!(metric.abc),
3005 );
3006 }
3007
3008 #[test]
3009 fn php_static_call() {
3010 check_metrics::<PhpParser>(
3011 "<?php
3012function f(): void {
3013 Foo::bar();
3014 Foo::baz(1);
3015}",
3016 "foo.php",
3017 |metric| insta::assert_json_snapshot!(metric.abc),
3018 );
3019 }
3020
3021 #[test]
3022 fn php_nullsafe_call() {
3023 check_metrics::<PhpParser>(
3024 "<?php
3025function f($obj): void {
3026 $obj?->m1();
3027 $obj?->m2(1);
3028}",
3029 "foo.php",
3030 |metric| insta::assert_json_snapshot!(metric.abc),
3031 );
3032 }
3033
3034 #[test]
3035 fn php_object_creation() {
3036 check_metrics::<PhpParser>(
3037 "<?php
3038function f(): void {
3039 new Foo();
3040 new Bar(1);
3041}",
3042 "foo.php",
3043 |metric| insta::assert_json_snapshot!(metric.abc),
3044 );
3045 }
3046
3047 #[test]
3048 fn php_comparison_eq() {
3049 check_metrics::<PhpParser>(
3050 "<?php
3051function f(int $a, int $b): bool {
3052 return $a == $b || $a != $b;
3053}",
3054 "foo.php",
3055 |metric| insta::assert_json_snapshot!(metric.abc),
3056 );
3057 }
3058
3059 #[test]
3060 fn php_comparison_strict() {
3061 check_metrics::<PhpParser>(
3062 "<?php
3063function f(int $a, int $b): bool {
3064 return $a === $b || $a !== $b;
3065}",
3066 "foo.php",
3067 |metric| insta::assert_json_snapshot!(metric.abc),
3068 );
3069 }
3070
3071 #[test]
3072 fn php_spaceship() {
3073 check_metrics::<PhpParser>(
3074 "<?php
3075function f(int $a, int $b): int {
3076 return $a <=> $b;
3077}",
3078 "foo.php",
3079 |metric| insta::assert_json_snapshot!(metric.abc),
3080 );
3081 }
3082
3083 #[test]
3084 fn php_instanceof() {
3085 check_metrics::<PhpParser>(
3086 "<?php
3087function f($x): bool {
3088 return $x instanceof Foo;
3089}",
3090 "foo.php",
3091 |metric| insta::assert_json_snapshot!(metric.abc),
3092 );
3093 }
3094
3095 #[test]
3096 fn php_complex_function() {
3097 // One snippet exercising A, B, C buckets together.
3098 check_metrics::<PhpParser>(
3099 "<?php
3100function f(int $a, int $b): int {
3101 $sum = $a + $b;
3102 $prod = $a * $b;
3103 if ($sum > 0 && $prod === 0) {
3104 return foo($sum);
3105 }
3106 return bar()->double();
3107}",
3108 "foo.php",
3109 |metric| insta::assert_json_snapshot!(metric.abc),
3110 );
3111 }
3112
3113 #[test]
3114 fn php_if_boolean_literal_condition() {
3115 check_metrics::<PhpParser>(
3116 "<?php\n\
3117 function f() {\n\
3118 \x20 if (true) {} // +1c\n\
3119 \x20 if (!false) {} // +1c\n\
3120 \x20 while (true) {} // +1c\n\
3121 \x20 do {} while (false); // +1c\n\
3122 }\n",
3123 "foo.php",
3124 |metric| {
3125 assert_eq!(metric.abc.conditions_sum(), 4);
3126 insta::assert_json_snapshot!(metric.abc);
3127 },
3128 );
3129 }
3130
3131 #[test]
3132 fn php_methods_arguments_with_conditions() {
3133 check_metrics::<PhpParser>(
3134 "<?php\n\
3135 function f($a, $b) {\n\
3136 \x20 m($a, $b); // +1b\n\
3137 \x20 m(!$a, !$b); // +1b +2c\n\
3138 }\n",
3139 "foo.php",
3140 |metric| {
3141 assert_eq!(metric.abc.branches_sum(), 2);
3142 assert_eq!(metric.abc.conditions_sum(), 2);
3143 insta::assert_json_snapshot!(metric.abc);
3144 },
3145 );
3146 }
3147
3148 #[test]
3149 fn php_return_with_conditions() {
3150 check_metrics::<PhpParser>(
3151 "<?php\n\
3152 function m1($z) { return !($z >= 0); }\n\
3153 function m2($x) { return (((!$x))); }\n\
3154 function m3($x, $y) { return $x && $y; }\n",
3155 "foo.php",
3156 |metric| {
3157 // m1: `>=` (1). m2: walker unwraps to $x (1).
3158 // m3: `&&` walker counts both (2). Sum: 4.
3159 assert_eq!(metric.abc.conditions_sum(), 4);
3160 insta::assert_json_snapshot!(metric.abc);
3161 },
3162 );
3163 }
3164
3165 #[test]
3166 fn php_name2_hidden_rule_drift_marker() {
3167 // Drift marker (findings.md round-2 #3): `Php::Name2` maps
3168 // to the hidden grammar rule `_name`. At the pinned
3169 // tree-sitter-php version it is never emitted as a concrete
3170 // node — the visible `Name` (= 1) carries every name.
3171 // We list `Name2` defensively in `php_bool_terminal_kinds!()`
3172 // (lesson 34); if a future grammar bump promotes `_name`
3173 // to a visible rule, this assertion fails loudly.
3174 let src = "<?php\nfunction f($x) { if ($x) { foo($x); } }\n";
3175 let parser = PhpParser::new(
3176 src.as_bytes().to_vec(),
3177 &std::path::PathBuf::from("foo.php"),
3178 None,
3179 );
3180 assert!(!ast_has_kind_id(&parser, Php::Name2 as u16));
3181 }
3182
3183 #[test]
3184 fn php_scoped_property_access_condition_counts() {
3185 // Regression for findings.md round-2 #1 (PHP):
3186 // `if (Config::$enabled) {}` parses with
3187 // `scoped_property_access_expression` as the condition
3188 // node (kind_id 333 at the pinned grammar version — the
3189 // `*2` alias). Pre-fix, neither `ScopedPropertyAccessExpression`
3190 // nor its alias was in `php_bool_terminal_kinds!()`. The
3191 // walker reached the access node, found it non-terminal,
3192 // and broke. Mirrors C#'s `MemberAccessExpression` rule
3193 // (lesson 19, #372).
3194 check_metrics::<PhpParser>(
3195 "<?php\n\
3196 class Config { public static $enabled = true; }\n\
3197 function f() { if (Config::$enabled) { } }\n",
3198 "foo.php",
3199 |metric| {
3200 assert_eq!(metric.abc.conditions_sum(), 1);
3201 insta::assert_json_snapshot!(metric.abc);
3202 },
3203 );
3204 }
3205
3206 #[test]
3207 fn php_named_argument_unary_conditional_counts() {
3208 // Regression for the code-review finding: PHP 8 named-argument
3209 // syntax `m(name: !$a)` parses as `argument(name, ':',
3210 // unary_op_expression)`. Pre-fix, the count walker took
3211 // `argument.child(0)` (the name) and missed the value at the
3212 // last child. Now it picks the last named child as the value.
3213 check_metrics::<PhpParser>(
3214 "<?php\nfunction f($a) { m(name: !$a); }\n",
3215 "foo.php",
3216 |metric| {
3217 // 1 call (branch) + 1 unary-conditional named argument.
3218 assert_eq!(metric.abc.branches_sum(), 1);
3219 assert_eq!(metric.abc.conditions_sum(), 1);
3220 insta::assert_json_snapshot!(metric.abc);
3221 },
3222 );
3223 }
3224
3225 #[test]
3226 fn php_low_precedence_keyword_logical_ops_trigger_walker() {
3227 // Regression: pre-fix, `$a or $b` reported 0 conditions
3228 // because the dispatcher only handled `AMPAMP|PIPEPIPE`,
3229 // skipping the PHP-specific `and` / `or` / `xor` keyword
3230 // forms even though they parse under the same
3231 // `binary_expression` shape.
3232 check_metrics::<PhpParser>(
3233 "<?php\n\
3234 function f($a, $b) {\n\
3235 \x20 return $a or $b;\n\
3236 }\n",
3237 "foo.php",
3238 |metric| {
3239 assert_eq!(metric.abc.conditions_sum(), 2);
3240 insta::assert_json_snapshot!(metric.abc);
3241 },
3242 );
3243 }
3244
3245 #[test]
3246 fn php_if_multiple_conditions() {
3247 check_metrics::<PhpParser>(
3248 "<?php\n\
3249 function f($a, $b, $c, $d) {\n\
3250 \x20 if ($a || $b || $c || $d) {} // +4c\n\
3251 \x20 if ($a && $b && $c) {} // +3c\n\
3252 \x20 if (!$a && !$b) {} // +2c\n\
3253 }\n",
3254 "foo.php",
3255 |metric| {
3256 assert_eq!(metric.abc.conditions_sum(), 9);
3257 insta::assert_json_snapshot!(metric.abc);
3258 },
3259 );
3260 }
3261
3262 #[test]
3263 fn php_while_and_do_while_conditions() {
3264 check_metrics::<PhpParser>(
3265 "<?php\n\
3266 function f($a, $b) {\n\
3267 \x20 while ($a || $b) {} // +2c\n\
3268 \x20 do {} while ($a && !$b); // +2c\n\
3269 }\n",
3270 "foo.php",
3271 |metric| {
3272 assert_eq!(metric.abc.conditions_sum(), 4);
3273 insta::assert_json_snapshot!(metric.abc);
3274 },
3275 );
3276 }
3277
3278 #[test]
3279 fn php_short_circuit_with_boolean_literal_operand() {
3280 check_metrics::<PhpParser>(
3281 "<?php\nfunction f($a) { return $a && true; }\n",
3282 "foo.php",
3283 |metric| {
3284 assert_eq!(metric.abc.conditions_sum(), 2);
3285 insta::assert_json_snapshot!(metric.abc);
3286 },
3287 );
3288 }
3289
3290 // --- Kotlin ABC tests -------------------------------------------------
3291
3292 #[test]
3293 fn kotlin_empty_class() {
3294 check_metrics::<KotlinParser>("class C {}", "foo.kt", |metric| {
3295 assert_eq!(metric.abc.assignments_sum(), 0);
3296 assert_eq!(metric.abc.branches_sum(), 0);
3297 assert_eq!(metric.abc.conditions_sum(), 0);
3298 insta::assert_json_snapshot!(metric.abc);
3299 });
3300 }
3301
3302 #[test]
3303 fn kotlin_val_declarations_are_not_assignments() {
3304 // `val` introduces an immutable binding — the `=` initialising it
3305 // is not an assignment in the ABC sense.
3306 check_metrics::<KotlinParser>(
3307 "class C {
3308 val a: Int = 1
3309 val b: Int = 2
3310 val c: Int = 3
3311 }",
3312 "foo.kt",
3313 |metric| {
3314 assert_eq!(metric.abc.assignments_sum(), 0);
3315 assert_eq!(metric.abc.branches_sum(), 0);
3316 insta::assert_json_snapshot!(metric.abc);
3317 },
3318 );
3319 }
3320
3321 #[test]
3322 fn kotlin_var_declarations_count_assignment() {
3323 // `var` initialisers count as assignments (mutable binding).
3324 check_metrics::<KotlinParser>(
3325 "class C {
3326 var a: Int = 1
3327 var b: Int = 2
3328 }",
3329 "foo.kt",
3330 |metric| {
3331 assert_eq!(metric.abc.assignments_sum(), 2);
3332 insta::assert_json_snapshot!(metric.abc);
3333 },
3334 );
3335 }
3336
3337 #[test]
3338 fn kotlin_val_then_assignments_count() {
3339 // Regression for #455: a `val` initialiser must not suppress the
3340 // standalone `=` assignments that follow it. tree-sitter-kotlin
3341 // emits no `SEMI` token (even for explicit semicolons), so the
3342 // pre-#455 `SEMI`-cleared declaration stack never cleared and the
3343 // immutable-`val` sentinel leaked, reporting A=0 here.
3344 check_metrics::<KotlinParser>(
3345 "fun f() {
3346 val cfg = 0
3347 a = 1
3348 b = 2
3349 }",
3350 "foo.kt",
3351 |metric| {
3352 // val initialiser suppressed; `a = 1` and `b = 2` count.
3353 assert_eq!(metric.abc.assignments_sum(), 2);
3354 insta::assert_json_snapshot!(metric.abc);
3355 },
3356 );
3357 }
3358
3359 #[test]
3360 fn kotlin_var_then_assignments_count() {
3361 // Companion to the #455 regression: a `var` declaration leaves a
3362 // mutable-binding sentinel that *permits* the `=` — this path
3363 // accidentally masked the leak (its `Var` sentinel never suppressed
3364 // anything), so it must keep counting both the initialiser and the
3365 // following standalone assignments.
3366 check_metrics::<KotlinParser>(
3367 "fun f() {
3368 var cfg = 0
3369 a = 1
3370 b = 2
3371 }",
3372 "foo.kt",
3373 |metric| {
3374 // var initialiser (+1) plus `a = 1` and `b = 2` (+2).
3375 assert_eq!(metric.abc.assignments_sum(), 3);
3376 insta::assert_json_snapshot!(metric.abc);
3377 },
3378 );
3379 }
3380
3381 #[test]
3382 fn kotlin_augmented_assignments_count() {
3383 // Augmented operators (+=, -=, etc.) and ++/-- always count.
3384 check_metrics::<KotlinParser>(
3385 "fun m() {
3386 var x = 0
3387 x += 1
3388 x -= 2
3389 x *= 3
3390 x++
3391 --x
3392 }",
3393 "foo.kt",
3394 |metric| {
3395 // var declaration (var x = 0): +1
3396 // x += 1, x -= 2, x *= 3, x++, --x: +5
3397 assert_eq!(metric.abc.assignments_sum(), 6);
3398 insta::assert_json_snapshot!(metric.abc);
3399 },
3400 );
3401 }
3402
3403 #[test]
3404 fn kotlin_branches_call_expression() {
3405 check_metrics::<KotlinParser>(
3406 "fun m() {
3407 println(\"a\")
3408 println(\"b\")
3409 println(\"c\")
3410 }",
3411 "foo.kt",
3412 |metric| {
3413 assert_eq!(metric.abc.branches_sum(), 3);
3414 insta::assert_json_snapshot!(metric.abc);
3415 },
3416 );
3417 }
3418
3419 #[test]
3420 fn kotlin_object_construction_branch() {
3421 // Kotlin's object construction is just `Foo()` — a `CallExpression`.
3422 check_metrics::<KotlinParser>(
3423 "class P(val x: Int)
3424 fun m(): P = P(1)",
3425 "foo.kt",
3426 |metric| {
3427 assert_eq!(metric.abc.branches_sum(), 1);
3428 insta::assert_json_snapshot!(metric.abc);
3429 },
3430 );
3431 }
3432
3433 #[test]
3434 fn kotlin_comparisons_count_conditions() {
3435 check_metrics::<KotlinParser>(
3436 "fun m(a: Int, b: Int): Boolean {
3437 val r1 = a < b
3438 val r2 = a > b
3439 val r3 = a <= b
3440 val r4 = a >= b
3441 val r5 = a == b
3442 val r6 = a != b
3443 return r1 || r2 || r3 || r4 || r5 || r6
3444 }",
3445 "foo.kt",
3446 |metric| {
3447 // Six comparison operators in the `val` initialisers
3448 // (<, >, <=, >=, ==, !=) → 6, plus the six bare-identifier
3449 // operands of the `r1 || … || r6` return chain, each a
3450 // Fitzpatrick Rule 9 unary condition (issue #557) → 6.
3451 // Total 12. Before the Kotlin walker was wired the chain
3452 // operands were silently dropped and this read 6.
3453 assert_eq!(metric.abc.conditions_sum(), 12);
3454 insta::assert_json_snapshot!(metric.abc);
3455 },
3456 );
3457 }
3458
3459 #[test]
3460 fn kotlin_identity_equality_conditions() {
3461 // `===` / `!==` are referential equality in Kotlin; they count too.
3462 check_metrics::<KotlinParser>(
3463 "fun m(a: Any, b: Any): Boolean {
3464 return a === b || a !== b
3465 }",
3466 "foo.kt",
3467 |metric| {
3468 assert_eq!(metric.abc.conditions_sum(), 2);
3469 insta::assert_json_snapshot!(metric.abc);
3470 },
3471 );
3472 }
3473
3474 #[test]
3475 fn kotlin_else_branch_counts() {
3476 check_metrics::<KotlinParser>(
3477 "fun m(x: Int): Int {
3478 return if (x > 0) 1 else -1
3479 }",
3480 "foo.kt",
3481 |metric| {
3482 // condition: > (1) + else (1) = 2
3483 assert_eq!(metric.abc.conditions_sum(), 2);
3484 insta::assert_json_snapshot!(metric.abc);
3485 },
3486 );
3487 }
3488
3489 #[test]
3490 fn kotlin_when_entries_count() {
3491 check_metrics::<KotlinParser>(
3492 "fun m(x: Int): Int {
3493 return when (x) {
3494 1 -> 10
3495 2 -> 20
3496 else -> 0
3497 }
3498 }",
3499 "foo.kt",
3500 |metric| {
3501 // Non-`else` WhenEntry arms count; the `else ->` fallback
3502 // arm does not (issue #456). Two case arms + zero for the
3503 // `else` arm = 2.
3504 assert_eq!(metric.abc.conditions_sum(), 2);
3505 insta::assert_json_snapshot!(metric.abc);
3506 },
3507 );
3508 }
3509
3510 // Pins the `else ->` exclusion directly: a `when` whose only fallback
3511 // is `else ->` must not count that arm. Revert-verified — gating the
3512 // `WhenEntry` arm on `!kotlin_when_entry_is_else` is what drops this
3513 // from 3 to 2 (issue #456, lesson 11). Mirrors the cyclomatic gate.
3514 #[test]
3515 fn kotlin_when_else_not_a_condition() {
3516 check_metrics::<KotlinParser>(
3517 "fun m(x: Int): Int {
3518 return when (x) { 1 -> 10; 2 -> 20; else -> 0 }
3519 }",
3520 "foo.kt",
3521 |metric| {
3522 // case `1 ->` (+1) + case `2 ->` (+1) + `else ->` (+0) = 2.
3523 assert_eq!(metric.abc.conditions_sum(), 2);
3524 },
3525 );
3526 }
3527
3528 #[test]
3529 fn kotlin_catch_block_counts() {
3530 check_metrics::<KotlinParser>(
3531 "fun m() {
3532 try {
3533 println(\"ok\")
3534 } catch (e: Exception) {
3535 println(\"err\")
3536 }
3537 }",
3538 "foo.kt",
3539 |metric| {
3540 // `try` (+1) and `catch` (+1) each contribute one condition,
3541 // matching Java / C# / C++ / Groovy (Fitzpatrick counts both
3542 // keywords). Before #696 Kotlin counted only the catch block.
3543 assert_eq!(metric.abc.conditions_sum(), 2);
3544 insta::assert_json_snapshot!(metric.abc);
3545 },
3546 );
3547 }
3548
3549 #[test]
3550 fn kotlin_elvis_and_safe_cast() {
3551 // `?:` (elvis) and `as?` (safe cast) are condition-like.
3552 check_metrics::<KotlinParser>(
3553 "fun m(s: String?): Int {
3554 val n = (s as? Int) ?: 0
3555 return n
3556 }",
3557 "foo.kt",
3558 |metric| {
3559 // as? (+1) + ?: (+1) = 2 conditions.
3560 assert_eq!(metric.abc.conditions_sum(), 2);
3561 insta::assert_json_snapshot!(metric.abc);
3562 },
3563 );
3564 }
3565
3566 #[test]
3567 fn kotlin_generic_brackets_not_conditions() {
3568 // `<` / `>` used as type-parameter brackets must not be counted.
3569 check_metrics::<KotlinParser>(
3570 "class Box<T>(val v: T)
3571 fun <T> wrap(x: T): Box<T> = Box(x)",
3572 "foo.kt",
3573 |metric| {
3574 // No comparisons — only generic brackets.
3575 assert_eq!(metric.abc.conditions_sum(), 0);
3576 insta::assert_json_snapshot!(metric.abc);
3577 },
3578 );
3579 }
3580
3581 #[test]
3582 fn kotlin_class_with_methods_and_branches() {
3583 check_metrics::<KotlinParser>(
3584 "class C {
3585 var counter: Int = 0
3586 fun bump() {
3587 counter += 1
3588 println(counter)
3589 }
3590 }",
3591 "foo.kt",
3592 |metric| {
3593 // assignments: var counter = 0 (+1), counter += 1 (+1) = 2
3594 // branches: println(counter) = 1
3595 assert_eq!(metric.abc.assignments_sum(), 2);
3596 assert_eq!(metric.abc.branches_sum(), 1);
3597 assert_eq!(metric.abc.conditions_sum(), 0);
3598 insta::assert_json_snapshot!(metric.abc);
3599 },
3600 );
3601 }
3602
3603 #[test]
3604 fn kotlin_object_singleton_abc() {
3605 check_metrics::<KotlinParser>(
3606 "object Util {
3607 fun work(x: Int): Int {
3608 var y = x
3609 y += 1
3610 if (y > 0) {
3611 return y
3612 }
3613 return -1
3614 }
3615 }",
3616 "foo.kt",
3617 |metric| {
3618 // assignments: var y = x (+1), y += 1 (+1) = 2
3619 // branches: 0 (return is not a call)
3620 // conditions: y > 0 (+1) = 1
3621 assert_eq!(metric.abc.assignments_sum(), 2);
3622 assert_eq!(metric.abc.branches_sum(), 0);
3623 assert_eq!(metric.abc.conditions_sum(), 1);
3624 insta::assert_json_snapshot!(metric.abc);
3625 },
3626 );
3627 }
3628
3629 #[test]
3630 fn kotlin_interface_abc() {
3631 // Pure-abstract interface with no bodies — all-zero.
3632 check_metrics::<KotlinParser>(
3633 "interface I {
3634 fun work(): Int
3635 fun describe(): String
3636 }",
3637 "foo.kt",
3638 |metric| {
3639 assert_eq!(metric.abc.assignments_sum(), 0);
3640 assert_eq!(metric.abc.branches_sum(), 0);
3641 assert_eq!(metric.abc.conditions_sum(), 0);
3642 insta::assert_json_snapshot!(metric.abc);
3643 },
3644 );
3645 }
3646
3647 #[test]
3648 fn kotlin_nested_class_abc() {
3649 check_metrics::<KotlinParser>(
3650 "class Outer {
3651 var o: Int = 0
3652 class Nested {
3653 var n: Int = 0
3654 fun bump() { n += 1 }
3655 }
3656 }",
3657 "foo.kt",
3658 |metric| {
3659 // Outer: var o = 0 (+1)
3660 // Nested: var n = 0 (+1), n += 1 (+1) = 2
3661 // total assignments = 3
3662 assert_eq!(metric.abc.assignments_sum(), 3);
3663 insta::assert_json_snapshot!(metric.abc);
3664 },
3665 );
3666 }
3667
3668 #[test]
3669 fn kotlin_data_class_abc() {
3670 // `data class` with primary-constructor `val`s — no assignments
3671 // (vals don't count) and no body conditions.
3672 check_metrics::<KotlinParser>(
3673 "data class Point(val x: Int, val y: Int)",
3674 "foo.kt",
3675 |metric| {
3676 assert_eq!(metric.abc.assignments_sum(), 0);
3677 assert_eq!(metric.abc.branches_sum(), 0);
3678 assert_eq!(metric.abc.conditions_sum(), 0);
3679 insta::assert_json_snapshot!(metric.abc);
3680 },
3681 );
3682 }
3683
3684 #[test]
3685 fn kotlin_primary_constructor_default_value_not_assignment() {
3686 // Regression: default values on primary-constructor `val`
3687 // parameters are initialisers, not assignments. Without
3688 // `ClassParameter` pushing a declaration sentinel, the `=` token
3689 // here would be counted unconditionally as a standalone
3690 // assignment.
3691 check_metrics::<KotlinParser>("class C(val a: Int = 5)", "foo.kt", |metric| {
3692 // `val a = 5` → suppressed (Const sentinel).
3693 assert_eq!(metric.abc.assignments_sum(), 0);
3694 insta::assert_json_snapshot!(metric.abc);
3695 });
3696 }
3697
3698 #[test]
3699 fn kotlin_unary_conditions_in_chain() {
3700 // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
3701 // `&&` / `||` chain is one condition. `a && b || c` → a, b, c each
3702 // contribute one; the `&&` / `||` operators contribute nothing.
3703 // expected: 3 unary conditions, no comparisons, no `if`-keyword
3704 // condition in Kotlin (matches the Java byte-equivalent of 3).
3705 check_metrics::<KotlinParser>(
3706 "fun f(a: Boolean, b: Boolean, c: Boolean) {
3707 if (a && b || c) { println(\"x\") }
3708 }",
3709 "foo.kt",
3710 |metric| {
3711 assert_eq!(metric.abc.conditions_sum(), 3);
3712 },
3713 );
3714 }
3715
3716 #[test]
3717 fn kotlin_comparison_operands_add_nothing() {
3718 // Isolation check: comparison operands of a `&&` chain are nested
3719 // `binary_expression` nodes, not bare boolean leaves, so the
3720 // walker adds nothing — only the two `>` comparisons count.
3721 // expected: 2 (the two `>` tokens), walker contributes 0.
3722 check_metrics::<KotlinParser>(
3723 "fun g(x: Int, y: Int) {
3724 if (x > 0 && y > 0) { println(\"x\") }
3725 }",
3726 "foo.kt",
3727 |metric| {
3728 assert_eq!(metric.abc.conditions_sum(), 2);
3729 },
3730 );
3731 }
3732
3733 #[test]
3734 fn kotlin_negated_operand_is_unary_condition() {
3735 // A `!`-negated operand is still a unary condition: `a && !b`
3736 // unwraps the `unary_expression` to reach the inner identifier.
3737 // expected: 2 (`a` and the `!b` operand).
3738 check_metrics::<KotlinParser>(
3739 "fun f(a: Boolean, b: Boolean) {
3740 if (a && !b) { println(\"x\") }
3741 }",
3742 "foo.kt",
3743 |metric| {
3744 assert_eq!(metric.abc.conditions_sum(), 2);
3745 },
3746 );
3747 }
3748
3749 #[test]
3750 fn kotlin_bare_if_predicate_is_one_condition() {
3751 // Issue #773: a bare-boolean `if` predicate (`if (flag)`) is one
3752 // Fitzpatrick unary condition. Before the Phase-2B arm it counted
3753 // 0, so `if (flag) 1 else -1` scored 1 (only the `else`) instead of
3754 // 2, dropping below Kotlin's own cyclomatic decision count.
3755 // expected: predicate (1) + else (1) = 2.
3756 check_metrics::<KotlinParser>(
3757 "fun m(flag: Boolean): Int { return if (flag) 1 else -1 }",
3758 "foo.kt",
3759 |metric| {
3760 assert_eq!(metric.abc.conditions_sum(), 2);
3761 },
3762 );
3763 }
3764
3765 #[test]
3766 fn kotlin_bare_while_predicate_is_one_condition() {
3767 // Issue #773: the bare predicate of a `while` loop counts one
3768 // condition via the `condition` field. expected: 1.
3769 check_metrics::<KotlinParser>(
3770 "fun m(running: Boolean) { while (running) { println(\"x\") } }",
3771 "foo.kt",
3772 |metric| {
3773 assert_eq!(metric.abc.conditions_sum(), 1);
3774 },
3775 );
3776 }
3777
3778 #[test]
3779 fn kotlin_bare_do_while_predicate_is_one_condition() {
3780 // Issue #773: the bare predicate of a `do`/`while` loop counts one
3781 // condition. expected: 1.
3782 check_metrics::<KotlinParser>(
3783 "fun m(ok: Boolean) { do { println(\"x\") } while (ok) }",
3784 "foo.kt",
3785 |metric| {
3786 assert_eq!(metric.abc.conditions_sum(), 1);
3787 },
3788 );
3789 }
3790
3791 #[test]
3792 fn kotlin_comparison_predicate_not_double_counted() {
3793 // Double-count guard (#773): a comparison predicate (`if (a == b)`)
3794 // is a nested `binary_expression` already counted by the `==` token
3795 // arm, so the Phase-2B condition-slot arm must add nothing here.
3796 // expected: `==` (1) + else (1) = 2 — unchanged by the new arm.
3797 check_metrics::<KotlinParser>(
3798 "fun m(a: Int, b: Int): Int { return if (a == b) 1 else -1 }",
3799 "foo.kt",
3800 |metric| {
3801 assert_eq!(metric.abc.conditions_sum(), 2);
3802 },
3803 );
3804 }
3805
3806 #[test]
3807 fn kotlin_short_circuit_predicate_not_double_counted() {
3808 // Double-count guard (#773): an `&&`/`||` predicate is counted by
3809 // the Rule 9 chain walker (each operand once); the Phase-2B arm
3810 // must add nothing for it. expected: `x` (1) + `y` (1) + else (1)
3811 // = 3 — unchanged by the new arm.
3812 check_metrics::<KotlinParser>(
3813 "fun m(x: Boolean, y: Boolean): Int { return if (x && y) 1 else -1 }",
3814 "foo.kt",
3815 |metric| {
3816 assert_eq!(metric.abc.conditions_sum(), 3);
3817 },
3818 );
3819 }
3820
3821 #[test]
3822 fn kotlin_parenthesised_bare_predicate_is_one_condition() {
3823 // A parenthesised bare predicate (`if ((flag))`) is unwrapped by
3824 // `kotlin_inspect_container` and still counts one condition (#773).
3825 // expected: 1.
3826 check_metrics::<KotlinParser>(
3827 "fun m(flag: Boolean) { if ((flag)) { println(\"x\") } }",
3828 "foo.kt",
3829 |metric| {
3830 assert_eq!(metric.abc.conditions_sum(), 1);
3831 },
3832 );
3833 }
3834
3835 // --- TypeScript / TSX ABC tests --------------------------------------
3836 //
3837 // Assignment, branch, condition counting per Fitzpatrick:
3838 // - Augmented assignment / `++` / `--` always count.
3839 // - Plain `=` counts unless inside `const` declaration.
3840 // - `call_expression` / `new_expression` count as branches.
3841 // - Comparison / equality operators, ternary `?`, `??`, control-flow
3842 // arms (`else`, `case`, `default`, `catch`, `try`, `instanceof`),
3843 // and `<`/`>` (outside `type_arguments` / `type_parameters`) count
3844 // as conditions.
3845
3846 #[test]
3847 fn typescript_assignments_basic() {
3848 check_metrics::<TypescriptParser>(
3849 "class C {
3850 m(): void {
3851 let x = 0; // const-sentinel suppressed since `let`, but x is Var → +1
3852 x = 1; // +1
3853 x += 2; // +1
3854 x++; // +1
3855 }
3856 }",
3857 "foo.ts",
3858 |metric| {
3859 assert_eq!(metric.abc.assignments_sum(), 4);
3860 insta::assert_json_snapshot!(metric.abc);
3861 },
3862 );
3863 }
3864
3865 #[test]
3866 fn typescript_const_excluded_from_assignments() {
3867 check_metrics::<TypescriptParser>(
3868 "class C {
3869 m(): void {
3870 const a = 1; // suppressed (Const sentinel)
3871 const b = 2; // suppressed
3872 let c = 3; // +1 (Var sentinel)
3873 }
3874 }",
3875 "foo.ts",
3876 |metric| {
3877 assert_eq!(metric.abc.assignments_sum(), 1);
3878 insta::assert_json_snapshot!(metric.abc);
3879 },
3880 );
3881 }
3882
3883 #[test]
3884 fn typescript_branches_function_calls() {
3885 check_metrics::<TypescriptParser>(
3886 "class C {
3887 m(): void {
3888 foo(); // +1
3889 bar(1, 2); // +1
3890 new Date(); // +1
3891 }
3892 }",
3893 "foo.ts",
3894 |metric| {
3895 assert_eq!(metric.abc.branches_sum(), 3);
3896 insta::assert_json_snapshot!(metric.abc);
3897 },
3898 );
3899 }
3900
3901 #[test]
3902 fn typescript_conditions_comparison_operators() {
3903 check_metrics::<TypescriptParser>(
3904 "class C {
3905 m(x: number, y: number): boolean {
3906 return x == y // +1
3907 || x === y // +1
3908 || x != y // +1
3909 || x !== y // +1
3910 || x < y // +1
3911 || x <= y // +1
3912 || x > y // +1
3913 || x >= y; // +1
3914 }
3915 }",
3916 "foo.ts",
3917 |metric| {
3918 assert_eq!(metric.abc.conditions_sum(), 8);
3919 insta::assert_json_snapshot!(metric.abc);
3920 },
3921 );
3922 }
3923
3924 #[test]
3925 fn typescript_conditions_control_flow_arms() {
3926 check_metrics::<TypescriptParser>(
3927 "class C {
3928 m(x: number): number {
3929 try { // +1 (try)
3930 if (x > 0) { // +1 (>)
3931 return 1;
3932 } else { // +1 (else)
3933 return -1;
3934 }
3935 } catch (e) { // +1 (catch)
3936 return 0;
3937 }
3938 }
3939 }",
3940 "foo.ts",
3941 |metric| {
3942 assert_eq!(metric.abc.conditions_sum(), 4);
3943 insta::assert_json_snapshot!(metric.abc);
3944 },
3945 );
3946 }
3947
3948 #[test]
3949 fn typescript_conditions_switch_case() {
3950 check_metrics::<TypescriptParser>(
3951 "class C {
3952 m(x: number): number {
3953 switch (x) {
3954 case 1: // +1
3955 return 1;
3956 case 2: // +1
3957 return 2;
3958 default: // +0 (fallthrough, #469)
3959 return 0;
3960 }
3961 }
3962 }",
3963 "foo.ts",
3964 |metric| {
3965 assert_eq!(metric.abc.conditions_sum(), 2);
3966 insta::assert_json_snapshot!(metric.abc);
3967 },
3968 );
3969 }
3970
3971 #[test]
3972 fn typescript_ternary_and_nullish() {
3973 check_metrics::<TypescriptParser>(
3974 "class C {
3975 m(x: number | null): number {
3976 return x !== null // +1 (!==)
3977 ? x // +1 (ternary ?)
3978 : 0;
3979 }
3980 n(x: number | null): number {
3981 return x ?? 0; // +1 (??)
3982 }
3983 }",
3984 "foo.ts",
3985 |metric| {
3986 assert_eq!(metric.abc.conditions_sum(), 3);
3987 insta::assert_json_snapshot!(metric.abc);
3988 },
3989 );
3990 }
3991
3992 #[test]
3993 fn typescript_instanceof_counts_as_condition() {
3994 check_metrics::<TypescriptParser>(
3995 "class C {
3996 m(o: unknown): boolean {
3997 return o instanceof C; // +1
3998 }
3999 }",
4000 "foo.ts",
4001 |metric| {
4002 assert_eq!(metric.abc.conditions_sum(), 1);
4003 insta::assert_json_snapshot!(metric.abc);
4004 },
4005 );
4006 }
4007
4008 #[test]
4009 fn typescript_generic_lt_gt_not_a_condition() {
4010 // `<T>` in `class C<T>` and `Array<number>` should not contribute
4011 // to conditions even though the tokens are `<` and `>`.
4012 check_metrics::<TypescriptParser>(
4013 "class C<T> {
4014 xs: Array<number> = [];
4015 m(): void {
4016 const arr: Array<string> = []; // suppressed const
4017 void arr;
4018 }
4019 }",
4020 "foo.ts",
4021 |metric| {
4022 assert_eq!(metric.abc.conditions_sum(), 0);
4023 insta::assert_json_snapshot!(metric.abc);
4024 },
4025 );
4026 }
4027
4028 #[test]
4029 fn typescript_abstract_class_abc() {
4030 // Abstract methods have no body — they contribute nothing.
4031 check_metrics::<TypescriptParser>(
4032 "abstract class C {
4033 abstract a(): void;
4034 m(x: number): number {
4035 if (x > 0) return 1; // +1 condition
4036 return 0;
4037 }
4038 }",
4039 "foo.ts",
4040 |metric| {
4041 assert_eq!(metric.abc.conditions_sum(), 1);
4042 assert_eq!(metric.abc.branches_sum(), 0);
4043 insta::assert_json_snapshot!(metric.abc);
4044 },
4045 );
4046 }
4047
4048 #[test]
4049 fn typescript_interface_abc_zero() {
4050 check_metrics::<TypescriptParser>(
4051 "interface I {
4052 a(): void;
4053 b(): number;
4054 p: string;
4055 }",
4056 "foo.ts",
4057 |metric| {
4058 assert_eq!(metric.abc.assignments_sum(), 0);
4059 assert_eq!(metric.abc.branches_sum(), 0);
4060 assert_eq!(metric.abc.conditions_sum(), 0);
4061 insta::assert_json_snapshot!(metric.abc);
4062 },
4063 );
4064 }
4065
4066 #[test]
4067 fn typescript_arrow_field_contributes_abc() {
4068 // Arrow function class members are function spaces; their
4069 // assignments/branches/conditions are counted.
4070 check_metrics::<TypescriptParser>(
4071 "class C {
4072 arrow = (x: number) => {
4073 if (x > 0) { // +1 condition
4074 return foo(); // +1 branch
4075 }
4076 return 0;
4077 };
4078 }",
4079 "foo.ts",
4080 |metric| {
4081 assert_eq!(metric.abc.conditions_sum(), 1);
4082 assert_eq!(metric.abc.branches_sum(), 1);
4083 insta::assert_json_snapshot!(metric.abc);
4084 },
4085 );
4086 }
4087
4088 #[test]
4089 fn typescript_parameter_property_init_not_assignment() {
4090 // Parameter properties don't introduce a `=` token themselves;
4091 // only the explicit `let z = 0` body assignment is counted.
4092 // The class field initializer `f: number = 0` likewise has a `=`
4093 // that DOES count (matches `typescript_assignments_basic`).
4094 check_metrics::<TypescriptParser>(
4095 "class C {
4096 f: number = 0;
4097 constructor(public x: number, private y: string) {
4098 let z = 0;
4099 }
4100 }",
4101 "foo.ts",
4102 |metric| {
4103 // f's initializer + `let z = 0` = 2 assignments; the
4104 // parameter properties contribute zero.
4105 assert_eq!(metric.abc.assignments_sum(), 2);
4106 insta::assert_json_snapshot!(metric.abc);
4107 },
4108 );
4109 }
4110
4111 // TSX parity
4112
4113 #[test]
4114 fn tsx_assignments_basic() {
4115 check_metrics::<TsxParser>(
4116 "class C {
4117 m(): void {
4118 let x = 0;
4119 x = 1;
4120 x += 2;
4121 x++;
4122 }
4123 }",
4124 "foo.tsx",
4125 |metric| {
4126 assert_eq!(metric.abc.assignments_sum(), 4);
4127 insta::assert_json_snapshot!(metric.abc);
4128 },
4129 );
4130 }
4131
4132 #[test]
4133 fn tsx_const_excluded_from_assignments() {
4134 check_metrics::<TsxParser>(
4135 "class C {
4136 m(): void {
4137 const a = 1;
4138 let b = 2;
4139 }
4140 }",
4141 "foo.tsx",
4142 |metric| {
4143 assert_eq!(metric.abc.assignments_sum(), 1);
4144 insta::assert_json_snapshot!(metric.abc);
4145 },
4146 );
4147 }
4148
4149 #[test]
4150 fn tsx_branches_function_calls() {
4151 check_metrics::<TsxParser>(
4152 "class C {
4153 m(): void {
4154 foo();
4155 new Date();
4156 }
4157 }",
4158 "foo.tsx",
4159 |metric| {
4160 assert_eq!(metric.abc.branches_sum(), 2);
4161 insta::assert_json_snapshot!(metric.abc);
4162 },
4163 );
4164 }
4165
4166 #[test]
4167 fn tsx_conditions_comparison_operators() {
4168 check_metrics::<TsxParser>(
4169 "class C {
4170 m(x: number, y: number): boolean {
4171 return x == y || x < y || x >= y;
4172 }
4173 }",
4174 "foo.tsx",
4175 |metric| {
4176 assert_eq!(metric.abc.conditions_sum(), 3);
4177 insta::assert_json_snapshot!(metric.abc);
4178 },
4179 );
4180 }
4181
4182 #[test]
4183 fn tsx_conditions_control_flow_arms() {
4184 check_metrics::<TsxParser>(
4185 "class C {
4186 m(x: number): number {
4187 try {
4188 if (x > 0) return 1;
4189 else return -1;
4190 } catch (e) {
4191 return 0;
4192 }
4193 }
4194 }",
4195 "foo.tsx",
4196 |metric| {
4197 assert_eq!(metric.abc.conditions_sum(), 4);
4198 insta::assert_json_snapshot!(metric.abc);
4199 },
4200 );
4201 }
4202
4203 #[test]
4204 fn tsx_conditions_switch_case() {
4205 check_metrics::<TsxParser>(
4206 "class C {
4207 m(x: number): number {
4208 switch (x) {
4209 case 1: return 1; // +1
4210 case 2: return 2; // +1
4211 default: return 0; // +0 (fallthrough, #469)
4212 }
4213 }
4214 }",
4215 "foo.tsx",
4216 |metric| {
4217 assert_eq!(metric.abc.conditions_sum(), 2);
4218 insta::assert_json_snapshot!(metric.abc);
4219 },
4220 );
4221 }
4222
4223 #[test]
4224 fn tsx_ternary_and_nullish() {
4225 check_metrics::<TsxParser>(
4226 "class C {
4227 m(x: number | null): number {
4228 return x !== null ? x : 0;
4229 }
4230 n(x: number | null): number { return x ?? 0; }
4231 }",
4232 "foo.tsx",
4233 |metric| {
4234 assert_eq!(metric.abc.conditions_sum(), 3);
4235 insta::assert_json_snapshot!(metric.abc);
4236 },
4237 );
4238 }
4239
4240 #[test]
4241 fn tsx_instanceof_counts_as_condition() {
4242 check_metrics::<TsxParser>(
4243 "class C { m(o: unknown): boolean { return o instanceof C; } }",
4244 "foo.tsx",
4245 |metric| {
4246 assert_eq!(metric.abc.conditions_sum(), 1);
4247 insta::assert_json_snapshot!(metric.abc);
4248 },
4249 );
4250 }
4251
4252 #[test]
4253 fn tsx_generic_lt_gt_not_a_condition() {
4254 check_metrics::<TsxParser>(
4255 "class C<T> { xs: Array<number> = []; }",
4256 "foo.tsx",
4257 |metric| {
4258 assert_eq!(metric.abc.conditions_sum(), 0);
4259 insta::assert_json_snapshot!(metric.abc);
4260 },
4261 );
4262 }
4263
4264 #[test]
4265 fn tsx_abstract_class_abc() {
4266 check_metrics::<TsxParser>(
4267 "abstract class C {
4268 abstract a(): void;
4269 m(x: number): number {
4270 if (x > 0) return 1;
4271 return 0;
4272 }
4273 }",
4274 "foo.tsx",
4275 |metric| {
4276 assert_eq!(metric.abc.conditions_sum(), 1);
4277 assert_eq!(metric.abc.branches_sum(), 0);
4278 insta::assert_json_snapshot!(metric.abc);
4279 },
4280 );
4281 }
4282
4283 #[test]
4284 fn tsx_interface_abc_zero() {
4285 check_metrics::<TsxParser>(
4286 "interface I { a(): void; p: string; }",
4287 "foo.tsx",
4288 |metric| {
4289 assert_eq!(metric.abc.assignments_sum(), 0);
4290 assert_eq!(metric.abc.branches_sum(), 0);
4291 assert_eq!(metric.abc.conditions_sum(), 0);
4292 insta::assert_json_snapshot!(metric.abc);
4293 },
4294 );
4295 }
4296
4297 #[test]
4298 fn tsx_arrow_field_contributes_abc() {
4299 check_metrics::<TsxParser>(
4300 "class C {
4301 arrow = (x: number) => {
4302 if (x > 0) return foo();
4303 return 0;
4304 };
4305 }",
4306 "foo.tsx",
4307 |metric| {
4308 assert_eq!(metric.abc.conditions_sum(), 1);
4309 assert_eq!(metric.abc.branches_sum(), 1);
4310 insta::assert_json_snapshot!(metric.abc);
4311 },
4312 );
4313 }
4314
4315 #[test]
4316 fn tsx_parameter_property_init_not_assignment() {
4317 // Parameter properties contribute no `=`; the body's `let z = 0`
4318 // and the field initializer do.
4319 check_metrics::<TsxParser>(
4320 "class C {
4321 f: number = 0;
4322 constructor(public x: number) { let z = 0; }
4323 }",
4324 "foo.tsx",
4325 |metric| {
4326 assert_eq!(metric.abc.assignments_sum(), 2);
4327 insta::assert_json_snapshot!(metric.abc);
4328 },
4329 );
4330 }
4331
4332 // --- Ruby ABC tests ---------------------------------------------------
4333 //
4334 // Each Ruby `assignment` / `operator_assignment` is one assignment
4335 // regardless of whether the LHS is a local, instance, or class
4336 // variable. Every `call` / `super` / `yield` is one branch. Every
4337 // comparison-operator token inside a `binary` node plus each
4338 // `else` / `elsif` / `when` / `then` / `?` / `rescue` clause is
4339 // one condition.
4340
4341 #[test]
4342 fn ruby_zero_abc() {
4343 check_metrics::<RubyParser>("\n", "foo.rb", |metric| {
4344 assert_eq!(metric.abc.assignments_sum(), 0);
4345 assert_eq!(metric.abc.branches_sum(), 0);
4346 assert_eq!(metric.abc.conditions_sum(), 0);
4347 insta::assert_json_snapshot!(metric.abc);
4348 });
4349 }
4350
4351 #[test]
4352 fn ruby_simple_assignment() {
4353 check_metrics::<RubyParser>("def f\n a = 1\n b = 2\nend\n", "foo.rb", |metric| {
4354 assert_eq!(metric.abc.assignments_sum(), 2);
4355 assert_eq!(metric.abc.branches_sum(), 0);
4356 assert_eq!(metric.abc.conditions_sum(), 0);
4357 insta::assert_json_snapshot!(metric.abc);
4358 });
4359 }
4360
4361 #[test]
4362 fn ruby_augmented_assignment() {
4363 // `+=`, `-=`, `*=` are `operator_assignment` nodes — each is
4364 // one assignment. Plain `=` to set the initial value adds one
4365 // more.
4366 check_metrics::<RubyParser>(
4367 "def f(x)\n a = 0\n a += x\n a -= 1\n a *= 2\nend\n",
4368 "foo.rb",
4369 |metric| {
4370 assert_eq!(metric.abc.assignments_sum(), 4);
4371 insta::assert_json_snapshot!(metric.abc);
4372 },
4373 );
4374 }
4375
4376 #[test]
4377 fn ruby_logical_augmented_assignment() {
4378 // `||=` and `&&=` are also `operator_assignment` nodes.
4379 check_metrics::<RubyParser>("def f\n @x ||= 0\n @x &&= 1\nend\n", "foo.rb", |metric| {
4380 assert_eq!(metric.abc.assignments_sum(), 2);
4381 insta::assert_json_snapshot!(metric.abc);
4382 });
4383 }
4384
4385 #[test]
4386 fn ruby_method_call_branch() {
4387 // Each method invocation is one branch.
4388 check_metrics::<RubyParser>(
4389 "def f(obj)\n foo()\n obj.bar(1)\nend\n",
4390 "foo.rb",
4391 |metric| {
4392 assert_eq!(metric.abc.branches_sum(), 2);
4393 insta::assert_json_snapshot!(metric.abc);
4394 },
4395 );
4396 }
4397
4398 #[test]
4399 fn ruby_super_and_yield_branches() {
4400 // `super` and `yield` both count as branches (control-pass).
4401 check_metrics::<RubyParser>("def f\n super\n yield\nend\n", "foo.rb", |metric| {
4402 assert_eq!(metric.abc.branches_sum(), 2);
4403 assert_eq!(metric.abc.assignments_sum(), 0);
4404 insta::assert_json_snapshot!(metric.abc);
4405 });
4406 }
4407
4408 #[test]
4409 fn ruby_attr_macro_is_branch() {
4410 // `attr_accessor` is a `Call3` node and registers as a branch
4411 // like any method invocation.
4412 check_metrics::<RubyParser>("class A\n attr_accessor :x\nend\n", "foo.rb", |metric| {
4413 assert_eq!(metric.abc.branches_sum(), 1);
4414 insta::assert_json_snapshot!(metric.abc);
4415 });
4416 }
4417
4418 #[test]
4419 fn ruby_comparison_conditions() {
4420 // Each comparison operator is one condition.
4421 check_metrics::<RubyParser>(
4422 "def f(a, b)\n a == b\n a != b\n a < b\n a > b\n a <= b\n a >= b\nend\n",
4423 "foo.rb",
4424 |metric| {
4425 assert_eq!(metric.abc.conditions_sum(), 6);
4426 insta::assert_json_snapshot!(metric.abc);
4427 },
4428 );
4429 }
4430
4431 #[test]
4432 fn ruby_case_match_in_arms_are_conditions() {
4433 // Regression for #977: each non-wildcard `case … in` arm is one
4434 // ABC condition, matching Python's `case_clause` handling. Using
4435 // literal patterns (no comparison operators) isolates the
4436 // `in_clause` contribution from any operand tokens.
4437 // expected: 2 conditions — one per `in 1` / `in 2` arm.
4438 check_metrics::<RubyParser>(
4439 "def f(x)\n case x\n in 1 then :one\n in 2 then :two\n end\nend\n",
4440 "foo.rb",
4441 |metric| {
4442 assert_eq!(metric.abc.conditions_sum(), 2);
4443 },
4444 );
4445 }
4446
4447 #[test]
4448 fn ruby_case_match_guarded_wildcard_is_a_condition() {
4449 // Regression for #977: a guarded wildcard arm `in _ if x` is not a
4450 // bare default and counts as one ABC condition, while the trailing
4451 // bare `in _` adds none. The guard predicate here is a bare
4452 // identifier (no comparison operator), so the single counted
4453 // condition is the guarded `in_clause` itself.
4454 // expected: 1 condition — the guarded `in _ if x` arm only.
4455 check_metrics::<RubyParser>(
4456 "def f(x)\n case x\n in _ if x then :y\n in _ then :default\n end\nend\n",
4457 "foo.rb",
4458 |metric| {
4459 assert_eq!(metric.abc.conditions_sum(), 1);
4460 },
4461 );
4462 }
4463
4464 #[test]
4465 fn ruby_case_match_bare_wildcard_is_not_a_condition() {
4466 // Regression for #977: a `case … in` whose only arm is the bare
4467 // wildcard `in _` (no guard) is the default arm and contributes no
4468 // ABC condition, keeping ABC and cyclomatic in lockstep on the
4469 // same construct.
4470 // expected: 0 conditions.
4471 check_metrics::<RubyParser>(
4472 "def f(x)\n case x\n in _ then :default\n end\nend\n",
4473 "foo.rb",
4474 |metric| {
4475 assert_eq!(metric.abc.conditions_sum(), 0);
4476 },
4477 );
4478 }
4479
4480 #[test]
4481 fn ruby_bare_predicate_control_flow_counts_one_condition() {
4482 // Regression for #696: idiomatic Ruby bare predicates
4483 // (`if flag` / `while flag` / `unless flag` / `until flag`) each
4484 // count one unary condition, matching Rust / C# / PHP / Python. The
4485 // condition field is read for both block and modifier forms.
4486 //
4487 // expected: 8 conditions — four block forms (`if`/`unless`/`while`/
4488 // `until`) plus the same four as modifiers, one each.
4489 check_metrics::<RubyParser>(
4490 "def f(flag)\n if flag\n a\n end\n unless flag\n b\n end\n while flag\n c\n end\n until flag\n d\n end\n a if flag\n b unless flag\n c while flag\n d until flag\nend\n",
4491 "foo.rb",
4492 |metric| {
4493 assert_eq!(metric.abc.conditions_sum(), 8);
4494 },
4495 );
4496 }
4497
4498 #[test]
4499 fn ruby_bare_predicate_does_not_double_count_comparison_or_chain() {
4500 // `if a == b` counts only the `==` comparison (the condition field
4501 // is a `binary` node, adding nothing). `if a && b` counts the two
4502 // chain operands via the `&&` walker, again with the condition-field
4503 // arm adding nothing — so neither shape is double-counted (#696).
4504 //
4505 // expected: 3 — `==` (1) + the `a`,`b` operands of `&&` (2).
4506 check_metrics::<RubyParser>(
4507 "def f(a, b)\n if a == b\n x\n end\n if a && b\n y\n end\nend\n",
4508 "foo.rb",
4509 |metric| {
4510 assert_eq!(metric.abc.conditions_sum(), 3);
4511 },
4512 );
4513 }
4514
4515 #[test]
4516 fn ruby_spaceship_and_case_equality() {
4517 // `<=>` and `===` are comparison operators (conditions).
4518 check_metrics::<RubyParser>(
4519 "def f(a, b)\n a <=> b\n a === b\nend\n",
4520 "foo.rb",
4521 |metric| {
4522 assert_eq!(metric.abc.conditions_sum(), 2);
4523 insta::assert_json_snapshot!(metric.abc);
4524 },
4525 );
4526 }
4527
4528 #[test]
4529 fn ruby_ternary_condition() {
4530 // The `?` ternary marker is one condition; the inner `==` is
4531 // another.
4532 check_metrics::<RubyParser>("def f(x)\n x == 0 ? :z : :nz\nend\n", "foo.rb", |metric| {
4533 assert_eq!(metric.abc.conditions_sum(), 2);
4534 insta::assert_json_snapshot!(metric.abc);
4535 });
4536 }
4537
4538 #[test]
4539 fn ruby_case_when_arms() {
4540 // Each `when` named clause and the `else` clause count as one
4541 // condition each; the `case` head and the implicit `then`
4542 // wrappers do not.
4543 check_metrics::<RubyParser>(
4544 "def f(x)\n case x\n when 1 then 'one'\n when 2 then 'two'\n else 'other'\n end\nend\n",
4545 "foo.rb",
4546 |metric| {
4547 // 2 `when` + 1 `else` = 3 conditions.
4548 assert_eq!(metric.abc.conditions_sum(), 3);
4549 insta::assert_json_snapshot!(metric.abc);
4550 },
4551 );
4552 }
4553
4554 #[test]
4555 fn ruby_elsif_and_else() {
4556 // `elsif` and `else` named clauses are conditions; their inner
4557 // `then` wrappers are not.
4558 check_metrics::<RubyParser>(
4559 "def f(x)\n if x > 0\n 1\n elsif x < 0\n -1\n else\n 0\n end\nend\n",
4560 "foo.rb",
4561 |metric| {
4562 // `>`(1) + `elsif`(1) + `<`(1) + `else`(1) = 4.
4563 assert_eq!(metric.abc.conditions_sum(), 4);
4564 insta::assert_json_snapshot!(metric.abc);
4565 },
4566 );
4567 }
4568
4569 #[test]
4570 fn ruby_rescue_clause_condition() {
4571 // The `rescue` named clause is one condition; the `rescue`
4572 // keyword token (`Rescue2`) is not counted on its own.
4573 // `do_it` without parens is an `identifier`, not a `call`, so
4574 // it contributes no branch. `handle(e)` is a `call` (1 branch).
4575 check_metrics::<RubyParser>(
4576 "def f\n begin\n do_it\n rescue StandardError => e\n handle(e)\n end\nend\n",
4577 "foo.rb",
4578 |metric| {
4579 assert_eq!(metric.abc.conditions_sum(), 1);
4580 assert_eq!(metric.abc.branches_sum(), 1);
4581 insta::assert_json_snapshot!(metric.abc);
4582 },
4583 );
4584 }
4585
4586 #[test]
4587 fn ruby_class_complex_function() {
4588 // Mixed: assignment(=), branch(call), conditions(`>` and `==`).
4589 check_metrics::<RubyParser>(
4590 "class A\n def f(a, b)\n sum = a + b\n if sum > 0 && b == 0\n foo(sum)\n end\n end\nend\n",
4591 "foo.rb",
4592 |metric| {
4593 assert_eq!(metric.abc.assignments_sum(), 1);
4594 assert_eq!(metric.abc.branches_sum(), 1);
4595 // `>`(1) + `==`(1) = 2 conditions. `if` is not a
4596 // token; `&&` is `AMPAMP` and is not counted (see
4597 // the module-level `Stats` doc-comment for the
4598 // cross-language policy; #395, walker tracked in
4599 // #403).
4600 assert_eq!(metric.abc.conditions_sum(), 2);
4601 insta::assert_json_snapshot!(metric.abc);
4602 },
4603 );
4604 }
4605
4606 #[test]
4607 fn ruby_unary_conditions_in_chain() {
4608 // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
4609 // `&&` / `||` chain is one condition. `a && b || c` → a, b, c each
4610 // contribute one. Ruby's `if` keyword is not a condition token.
4611 // expected: 3 unary conditions (matches the Java byte-equivalent).
4612 check_metrics::<RubyParser>(
4613 "def f(a, b, c)\n if a && b || c\n puts \"x\"\n end\nend\n",
4614 "foo.rb",
4615 |metric| {
4616 assert_eq!(metric.abc.conditions_sum(), 3);
4617 },
4618 );
4619 }
4620
4621 #[test]
4622 fn ruby_keyword_and_or_chain_counts_operands() {
4623 // The keyword forms `and` / `or` get the same Rule 9 treatment as
4624 // `&&` / `||`. expected: 3 unary conditions (a, b, c).
4625 check_metrics::<RubyParser>(
4626 "def f(a, b, c)\n if a and b or c\n puts \"x\"\n end\nend\n",
4627 "foo.rb",
4628 |metric| {
4629 assert_eq!(metric.abc.conditions_sum(), 3);
4630 },
4631 );
4632 }
4633
4634 #[test]
4635 fn ruby_negated_operand_is_unary_condition() {
4636 // A `!`-negated operand unwraps the `unary` node to the inner
4637 // identifier. expected: 2 (`a` and the `!b` operand).
4638 check_metrics::<RubyParser>(
4639 "def f(a, b)\n if a && !b\n puts \"x\"\n end\nend\n",
4640 "foo.rb",
4641 |metric| {
4642 assert_eq!(metric.abc.conditions_sum(), 2);
4643 },
4644 );
4645 }
4646
4647 #[test]
4648 fn ruby_comparison_operands_add_nothing() {
4649 // Isolation for Rule 9 (issue #557): when the `&&` operands are
4650 // themselves comparisons, the unary-condition walker must add
4651 // nothing on top of the two `>` comparisons already counted as
4652 // conditions — distinguishing the gap (bare boolean operands)
4653 // from ordinary relational conditions. Mirrors the Kotlin and
4654 // Elixir isolation tests. expected: 2 (the two `>` comparisons).
4655 check_metrics::<RubyParser>(
4656 "def f(x, y)\n if x > 0 && y > 0\n puts \"x\"\n end\nend\n",
4657 "foo.rb",
4658 |metric| {
4659 assert_eq!(metric.abc.conditions_sum(), 2);
4660 },
4661 );
4662 }
4663
4664 // ---------------------------------------------------------------
4665 // Default-impl placeholder smoke tests (audited in #188).
4666 //
4667 // These tests assert that the *current* default-impl languages
4668 // return ABC = 0/0/0 for source that DOES contain branches,
4669 // conditions, and assignments. When the real impl lands for any
4670 // of these languages, the corresponding assertion below will fire
4671 // — the implementer must update the expected values, which is the
4672 // gate. Tag the follow-up issue in each test.
4673 // ---------------------------------------------------------------
4674
4675 // --- Python ABC ---------------------------------------------------
4676
4677 #[test]
4678 fn python_empty_module_zero() {
4679 check_metrics::<PythonParser>("", "empty.py", |metric| {
4680 assert_eq!(metric.abc.assignments_sum(), 0);
4681 assert_eq!(metric.abc.branches_sum(), 0);
4682 assert_eq!(metric.abc.conditions_sum(), 0);
4683 insta::assert_json_snapshot!(metric.abc);
4684 });
4685 }
4686
4687 #[test]
4688 fn python_plain_assignments_count() {
4689 // Three plain `=` assignments → A=3. No branches, no conditions.
4690 check_metrics::<PythonParser>("x = 1\ny = 2\nz = x\n", "foo.py", |metric| {
4691 assert_eq!(metric.abc.assignments_sum(), 3);
4692 assert_eq!(metric.abc.branches_sum(), 0);
4693 assert_eq!(metric.abc.conditions_sum(), 0);
4694 insta::assert_json_snapshot!(metric.abc);
4695 });
4696 }
4697
4698 #[test]
4699 fn python_typed_assignment_counts_bare_annotation_does_not() {
4700 // `x: int = 1` carries an `=`, so it counts.
4701 // `y: int` is a bare annotation (no `=`) — declares a type but
4702 // binds nothing; it must NOT inflate the assignment count.
4703 check_metrics::<PythonParser>("x: int = 1\ny: int\n", "foo.py", |metric| {
4704 assert_eq!(metric.abc.assignments_sum(), 1);
4705 insta::assert_json_snapshot!(metric.abc);
4706 });
4707 }
4708
4709 #[test]
4710 fn python_augmented_assignments_count() {
4711 // Each augmented op counts once.
4712 check_metrics::<PythonParser>("x = 0\nx += 1\nx -= 1\nx *= 2\n", "foo.py", |metric| {
4713 // 1 plain `=` + 3 augmented = 4 assignments.
4714 assert_eq!(metric.abc.assignments_sum(), 4);
4715 insta::assert_json_snapshot!(metric.abc);
4716 });
4717 }
4718
4719 #[test]
4720 fn python_walrus_counts_as_assignment() {
4721 // `x := 10` is a `NamedExpression` (PEP 572). It binds a value
4722 // → one assignment under Fitzpatrick's rule.
4723 check_metrics::<PythonParser>("if (n := 10) > 5:\n pass\n", "foo.py", |metric| {
4724 // 1 assignment (walrus) + 1 condition (`> 5` is a
4725 // ComparisonOperator).
4726 assert_eq!(metric.abc.assignments_sum(), 1);
4727 assert_eq!(metric.abc.conditions_sum(), 1);
4728 insta::assert_json_snapshot!(metric.abc);
4729 });
4730 }
4731
4732 #[test]
4733 fn python_calls_are_branches() {
4734 // `foo()`, `bar()`, `Baz()` (constructor) all parse as `Call`
4735 // → three branches.
4736 check_metrics::<PythonParser>(
4737 "def foo():\n pass\ndef bar():\n pass\nclass Baz:\n pass\nfoo()\nbar()\nBaz()\n",
4738 "foo.py",
4739 |metric| {
4740 assert_eq!(metric.abc.branches_sum(), 3);
4741 assert_eq!(metric.abc.assignments_sum(), 0);
4742 insta::assert_json_snapshot!(metric.abc);
4743 },
4744 );
4745 }
4746
4747 #[test]
4748 fn python_comparisons_count_conditions() {
4749 // `x > 0`, `x == y`, `x is None` are each a single
4750 // `ComparisonOperator` node — three conditions.
4751 check_metrics::<PythonParser>(
4752 "def f(x, y):\n a = x > 0\n b = x == y\n c = x is None\n",
4753 "foo.py",
4754 |metric| {
4755 assert_eq!(metric.abc.conditions_sum(), 3);
4756 // 3 plain assignments; the comparisons are operands.
4757 assert_eq!(metric.abc.assignments_sum(), 3);
4758 insta::assert_json_snapshot!(metric.abc);
4759 },
4760 );
4761 }
4762
4763 #[test]
4764 fn python_chained_comparison_counts_once() {
4765 // tree-sitter-python collapses `0 < x < 10` into a single
4766 // `ComparisonOperator` — one condition, not two.
4767 check_metrics::<PythonParser>("def f(x):\n return 0 < x < 10\n", "foo.py", |metric| {
4768 assert_eq!(metric.abc.conditions_sum(), 1);
4769 insta::assert_json_snapshot!(metric.abc);
4770 });
4771 }
4772
4773 #[test]
4774 fn python_number_truthy_condition_counts() {
4775 // Regression for #772: Python treats every non-zero number as
4776 // truthy, so `if 5:` and `x and 5` should each count their
4777 // numeric literal as a Fitzpatrick unary condition. Pre-fix
4778 // `python_bool_terminal_kinds!()` listed `True` / `False` but
4779 // omitted `Integer` / `Float`, so the walker dropped every
4780 // numeric-truthy operand (mirrors the Lua `Number` fix).
4781 check_metrics::<PythonParser>(
4782 "def f(a):\n if 5:\n pass\n return a and 2\n",
4783 "foo.py",
4784 |metric| {
4785 // `if 5:` → walker counts the Integer literal (+1).
4786 // `a and 2` → `and` walker counts both operands:
4787 // identifier `a` (+1), Integer `2` (+1).
4788 // Total: 3.
4789 assert_eq!(metric.abc.conditions_sum(), 3);
4790 insta::assert_json_snapshot!(metric.abc);
4791 },
4792 );
4793 }
4794
4795 #[test]
4796 fn python_boolean_operators_not_counted_directly() {
4797 // Python's `and` / `or` are not counted as conditions on
4798 // their own (Fitzpatrick Rule 5; #395). Each operand is
4799 // instead counted as a unary conditional by the walker
4800 // (Rule 9; #403). `if a and b or c:` parses left-to-right
4801 // with `or` lower precedence: `(a and b) or c`. Walker
4802 // tallies: inner `and` counts `a`, `b` (+2); outer `or`
4803 // counts only the new outer operand `c` (+1; the inner
4804 // `(a and b)` BooleanOperator is not a terminal). Total
4805 // C = 3.
4806 check_metrics::<PythonParser>(
4807 "def f(a, b, c):\n if a and b or c:\n pass\n",
4808 "foo.py",
4809 |metric| {
4810 assert_eq!(metric.abc.conditions_sum(), 3);
4811 insta::assert_json_snapshot!(metric.abc);
4812 },
4813 );
4814 }
4815
4816 /// Python's unary `not` operator parses as `NotOperator` and now
4817 /// counts as one condition, matching Java's `!x` rule. Closes
4818 /// the parity gap noted in #214: without this, `if not flag:`
4819 /// reported 0 conditions while the Java equivalent reports 1.
4820 #[test]
4821 fn python_unary_not_counts_as_condition() {
4822 check_metrics::<PythonParser>(
4823 "def f(flag):\n if not flag:\n return 1\n return 0\n",
4824 "foo.py",
4825 |metric| {
4826 // One `NotOperator` -> 1 condition. The `if` itself
4827 // is structural and doesn't add an Abc condition.
4828 assert_eq!(metric.abc.conditions_sum(), 1);
4829 insta::assert_json_snapshot!(metric.abc);
4830 },
4831 );
4832 }
4833
4834 /// `return not flag` — the unary `not` is the entire return
4835 /// expression. Without `NotOperator` counted, this reports zero
4836 /// conditions; with it, one. Java's `return !flag;` is one.
4837 #[test]
4838 fn python_return_unary_not_counts() {
4839 check_metrics::<PythonParser>("def f(flag):\n return not flag\n", "foo.py", |metric| {
4840 assert_eq!(metric.abc.conditions_sum(), 1);
4841 insta::assert_json_snapshot!(metric.abc);
4842 });
4843 }
4844
4845 /// `foo(not ready, value)` — the unary `not` inside an argument
4846 /// list still contributes. Mirrors Java's
4847 /// `java_count_unary_conditions` walk over argument lists.
4848 #[test]
4849 fn python_unary_not_in_argument_list_counts() {
4850 check_metrics::<PythonParser>(
4851 "def f(ready, value):\n log(not ready, value)\n",
4852 "foo.py",
4853 |metric| {
4854 // 1 Call (log) -> 1 branch.
4855 // 1 NotOperator (not ready) -> 1 condition.
4856 assert_eq!(metric.abc.branches_sum(), 1);
4857 assert_eq!(metric.abc.conditions_sum(), 1);
4858 insta::assert_json_snapshot!(metric.abc);
4859 },
4860 );
4861 }
4862
4863 /// Nested `not` + comparison counts each unique node once.
4864 /// `not (x > 0)` parses as `NotOperator(ParenthesizedExpression(
4865 /// ComparisonOperator))`; both the unary and the comparison
4866 /// contribute one condition (mirrors Java's `!(x > 0)` = 2
4867 /// conditions).
4868 #[test]
4869 fn python_unary_not_with_comparison_counts_each_once() {
4870 check_metrics::<PythonParser>(
4871 "def f(x):\n if not (x > 0):\n return 1\n return 0\n",
4872 "foo.py",
4873 |metric| {
4874 // NotOperator (1) + ComparisonOperator (1) = 2.
4875 assert_eq!(metric.abc.conditions_sum(), 2);
4876 insta::assert_json_snapshot!(metric.abc);
4877 },
4878 );
4879 }
4880
4881 /// `not x and y` parses as `BooleanOperator(NotOperator(x), and,
4882 /// y)`. The `and` itself is NOT counted (Fitzpatrick Rule 5
4883 /// lists only comparison operators); the `NotOperator` is
4884 /// counted at the top level (Rule 7); and the `y` operand is
4885 /// counted by the Rule 9 walker (issue #403). Total: 2.
4886 /// `NotOperator` is intentionally not walked-into a second
4887 /// time — the walker skips it to avoid double-counting.
4888 #[test]
4889 fn python_unary_not_with_boolean_combinator_counts_each() {
4890 check_metrics::<PythonParser>(
4891 "def f(x, y):\n if not x and y:\n return 1\n return 0\n",
4892 "foo.py",
4893 |metric| {
4894 // NotOperator (1) + walker on `and` finds `y` (1) = 2.
4895 assert_eq!(metric.abc.conditions_sum(), 2);
4896 insta::assert_json_snapshot!(metric.abc);
4897 },
4898 );
4899 }
4900
4901 #[test]
4902 fn python_control_flow_arms_count_conditions() {
4903 // `elif`, `else`, `except`, `finally`, `case` each contribute
4904 // one condition. The comparisons in the `if`/`elif`/`while`
4905 // headers contribute their own ComparisonOperator counts.
4906 check_metrics::<PythonParser>(
4907 "def f(x):\n if x > 0:\n a = 1\n elif x > -1:\n a = 2\n else:\n a = 3\n",
4908 "foo.py",
4909 |metric| {
4910 // 2 ComparisonOperator (`x > 0`, `x > -1`) + 1
4911 // ElifClause + 1 ElseClause = 4 conditions.
4912 assert_eq!(metric.abc.conditions_sum(), 4);
4913 insta::assert_json_snapshot!(metric.abc);
4914 },
4915 );
4916 }
4917
4918 #[test]
4919 fn python_ternary_counts_as_condition() {
4920 // `a if c else b` is `ConditionalExpression` → 1 condition.
4921 // `c > 0` adds 1 more (ComparisonOperator).
4922 check_metrics::<PythonParser>(
4923 "def f(c):\n return 1 if c > 0 else 0\n",
4924 "foo.py",
4925 |metric| {
4926 assert_eq!(metric.abc.conditions_sum(), 2);
4927 insta::assert_json_snapshot!(metric.abc);
4928 },
4929 );
4930 }
4931
4932 #[test]
4933 fn python_try_except_finally_count_conditions() {
4934 // ExceptClause + FinallyClause → 2 conditions.
4935 check_metrics::<PythonParser>(
4936 "def f():\n try:\n pass\n except ValueError:\n pass\n finally:\n pass\n",
4937 "foo.py",
4938 |metric| {
4939 assert_eq!(metric.abc.conditions_sum(), 2);
4940 insta::assert_json_snapshot!(metric.abc);
4941 },
4942 );
4943 }
4944
4945 #[test]
4946 fn python_match_case_counts_conditions() {
4947 // Each non-wildcard `CaseClause` → 1 condition. The bare
4948 // `case _:` arm is the language-neutral `default:` equivalent
4949 // and is excluded (matches Rust's bare-`_` MatchArm filter and
4950 // Java/C#'s `default:` rule). Source has `case 1:` (counts) +
4951 // `case _:` (excluded) → C = 1.
4952 check_metrics::<PythonParser>(
4953 "def f(x):\n match x:\n case 1:\n pass\n case _:\n pass\n",
4954 "foo.py",
4955 |metric| {
4956 assert_eq!(metric.abc.conditions_sum(), 1);
4957 insta::assert_json_snapshot!(metric.abc);
4958 },
4959 );
4960 }
4961
4962 #[test]
4963 fn python_match_case_guarded_wildcard_counts() {
4964 // `case _ if g:` is NOT a bare wildcard — the guard
4965 // contributes real branching, so the arm counts as a
4966 // condition. Mirrors Rust's `_ if g => ...` behavior.
4967 // Source: `case 1:` (counts) + `case _ if x > 0:` (guarded
4968 // wildcard, counts) + `case _:` (bare wildcard, excluded) →
4969 // C from CaseClause = 2; the guard's `x > 0` adds one
4970 // ComparisonOperator → total C = 3.
4971 check_metrics::<PythonParser>(
4972 "def f(x):\n match x:\n case 1:\n pass\n case _ if x > 0:\n pass\n case _:\n pass\n",
4973 "foo.py",
4974 |metric| {
4975 assert_eq!(metric.abc.conditions_sum(), 3);
4976 insta::assert_json_snapshot!(metric.abc);
4977 },
4978 );
4979 }
4980
4981 #[test]
4982 fn python_complex_function_abc() {
4983 // Mixed-shape regression: assignments, calls, conditions all in
4984 // a single function.
4985 check_metrics::<PythonParser>(
4986 "def f(items, threshold):\n\
4987 \x20 result = []\n\
4988 \x20 for item in items:\n\
4989 \x20 if item > threshold:\n\
4990 \x20 result.append(item)\n\
4991 \x20 return result\n",
4992 "foo.py",
4993 |metric| {
4994 // assignments: `result = []` → 1
4995 // branches: `result.append(item)` is one call → 1
4996 // conditions: `item > threshold` is one
4997 // ComparisonOperator → 1
4998 assert_eq!(metric.abc.assignments_sum(), 1);
4999 assert_eq!(metric.abc.branches_sum(), 1);
5000 assert_eq!(metric.abc.conditions_sum(), 1);
5001 insta::assert_json_snapshot!(metric.abc);
5002 },
5003 );
5004 }
5005
5006 #[test]
5007 fn python_if_multiple_conditions() {
5008 // Fitzpatrick Rule 9 walker on `and` / `or` (issue #403).
5009 // - `if a or b or c or d:` → 4 (each operand counted once)
5010 // - `if a and b and c:` → 3
5011 // - `if not a and not b:` → 2 (two `NotOperator`s counted
5012 // by the top-level dispatcher arm; the walker SKIPS
5013 // `NotOperator` children to avoid double-counting)
5014 // Total: 4 + 3 + 2 = 9.
5015 check_metrics::<PythonParser>(
5016 "def f(a, b, c, d):\n\
5017 \x20 if a or b or c or d: # +4c\n\
5018 \x20 pass\n\
5019 \x20 if a and b and c: # +3c\n\
5020 \x20 pass\n\
5021 \x20 if not a and not b: # +2c (NotOperator x2)\n\
5022 \x20 pass\n",
5023 "foo.py",
5024 |metric| {
5025 assert_eq!(metric.abc.conditions_sum(), 9);
5026 insta::assert_json_snapshot!(metric.abc);
5027 },
5028 );
5029 }
5030
5031 #[test]
5032 fn python_while_conditions() {
5033 // Python has no `do { ... } while(cond);` construct, so this
5034 // mirrors only the `while` half of the Java suite. The
5035 // walker fires on each `and` / `or` token inside the loop
5036 // header.
5037 check_metrics::<PythonParser>(
5038 "def f(a, b):\n\
5039 \x20 while a or b: # +2c\n\
5040 \x20 break\n\
5041 \x20 while a and not b: # +2c (a + NotOperator)\n\
5042 \x20 break\n",
5043 "foo.py",
5044 |metric| {
5045 assert_eq!(metric.abc.conditions_sum(), 4);
5046 insta::assert_json_snapshot!(metric.abc);
5047 },
5048 );
5049 }
5050
5051 #[test]
5052 fn python_short_circuit_with_boolean_literal_operand() {
5053 // `a and True` reports 2 conditions: one identifier, one
5054 // True literal. Confirms `True` / `False` are in the walker
5055 // terminal set.
5056 check_metrics::<PythonParser>("def f(a):\n return a and True\n", "foo.py", |metric| {
5057 assert_eq!(metric.abc.conditions_sum(), 2);
5058 insta::assert_json_snapshot!(metric.abc);
5059 });
5060 }
5061
5062 #[test]
5063 fn python_await_expression_condition_counts() {
5064 // Regression for findings.md round-2 #2 (Python):
5065 // `if await ready(): pass` parses with `await` as the
5066 // condition node. Adding `Python::Await` to the
5067 // terminal-bool set mirrors the C# reference (lesson 19).
5068 check_metrics::<PythonParser>(
5069 "async def ready(): return True\n\
5070 async def f():\n if await ready(): pass\n",
5071 "foo.py",
5072 |metric| {
5073 // ready() is a call (1 branch); await is the
5074 // condition (1).
5075 assert_eq!(metric.abc.branches_sum(), 1);
5076 assert_eq!(metric.abc.conditions_sum(), 1);
5077 insta::assert_json_snapshot!(metric.abc);
5078 },
5079 );
5080 }
5081
5082 #[test]
5083 fn python_if_call_terminal_condition_counts_once() {
5084 // Pins the Phase-2B behaviour for Python's `Call` terminal-bool
5085 // kind: `if foo():` is a Fitzpatrick Rule 6 unary conditional
5086 // (a bare boolean-evaluating call as the if-condition). The
5087 // walker's terminal-at-top check fires once per call-condition;
5088 // the call itself separately contributes 1 branch. Surfaced
5089 // (and verified intentional) by the code-review pass on
5090 // Phase 2B.
5091 check_metrics::<PythonParser>("def f():\n if foo(): pass\n", "foo.py", |metric| {
5092 assert_eq!(metric.abc.branches_sum(), 1);
5093 assert_eq!(metric.abc.conditions_sum(), 1);
5094 insta::assert_json_snapshot!(metric.abc);
5095 });
5096 }
5097
5098 #[test]
5099 fn python_if_boolean_literal_condition() {
5100 // Phase 2B (issue #403): bare-boolean conditions count once.
5101 // Python has no paren wrap around if-conditions, so the
5102 // condition node is checked directly. The existing
5103 // NotOperator / ComparisonOperator arms continue to fire
5104 // for those shapes; only the bare-terminal cases (Identifier,
5105 // True, False, etc.) are added by the new arm.
5106 check_metrics::<PythonParser>(
5107 "def f(a):\n\
5108 \x20 if True: pass # +1c\n\
5109 \x20 if False: pass # +1c\n\
5110 \x20 while True: break # +1c\n\
5111 \x20 if a: pass # +1c (Rule 6 — bare identifier as condition)\n",
5112 "foo.py",
5113 |metric| {
5114 assert_eq!(metric.abc.conditions_sum(), 4);
5115 insta::assert_json_snapshot!(metric.abc);
5116 },
5117 );
5118 }
5119
5120 #[test]
5121 fn python_methods_arguments_with_conditions() {
5122 // `m(not a, not b)` reports 2 conditions — both `NotOperator`
5123 // nodes are counted by Python's pre-existing top-level
5124 // NotOperator dispatcher arm. The argument-list walker does
5125 // not need a separate Python arm.
5126 check_metrics::<PythonParser>(
5127 "def f(a, b):\n\
5128 \x20 m(a, b) # +1b\n\
5129 \x20 m(not a, not b) # +1b +2c\n",
5130 "foo.py",
5131 |metric| {
5132 assert_eq!(metric.abc.branches_sum(), 2);
5133 assert_eq!(metric.abc.conditions_sum(), 2);
5134 insta::assert_json_snapshot!(metric.abc);
5135 },
5136 );
5137 }
5138
5139 #[test]
5140 fn python_return_with_conditions() {
5141 // Phase 2B (issue #403). Python uses the pre-existing top-
5142 // level NotOperator / ComparisonOperator arms for return
5143 // expressions; no dedicated ReturnStatement walker arm is
5144 // needed.
5145 check_metrics::<PythonParser>(
5146 "def m1(z): return not (z >= 0)\n\
5147 def m2(x): return (((not x)))\n\
5148 def m3(x, y): return x and y\n",
5149 "foo.py",
5150 |metric| {
5151 // m1: NotOperator (1) + ComparisonOperator (1) = 2.
5152 // m2: NotOperator (1).
5153 // m3: walker on `and` counts both operands = 2.
5154 // Sum: 5.
5155 assert_eq!(metric.abc.conditions_sum(), 5);
5156 insta::assert_json_snapshot!(metric.abc);
5157 },
5158 );
5159 }
5160
5161 #[test]
5162 fn rust_empty_unit_zero() {
5163 // No code at all → A=B=C=0. Establishes the trait is wired up
5164 // and the per-language compute is reachable.
5165 check_metrics::<RustParser>("", "empty.rs", |metric| {
5166 assert_eq!(metric.abc.assignments_sum(), 0);
5167 assert_eq!(metric.abc.branches_sum(), 0);
5168 assert_eq!(metric.abc.conditions_sum(), 0);
5169 insta::assert_json_snapshot!(metric.abc);
5170 });
5171 }
5172
5173 #[test]
5174 fn rust_assignments_let_init_plain_and_compound() {
5175 // `let mut x = 0` is a `let_declaration` carrying an `=`
5176 // initializer → counts as 1 (matches Fitzpatrick's literal
5177 // "every `=` is an assignment" rule and the JS impl's
5178 // treatment of `let x = 5`). `x = 5` and `x = 7` are plain
5179 // `=` assignments → 2. `x += 2` is a compound assignment → 1.
5180 // Total A = 4.
5181 check_metrics::<RustParser>(
5182 "fn f() { let mut x = 0; x = 5; x += 2; x = 7; }",
5183 "foo.rs",
5184 |metric| {
5185 assert_eq!(metric.abc.assignments_sum(), 4);
5186 assert_eq!(metric.abc.branches_sum(), 0);
5187 assert_eq!(metric.abc.conditions_sum(), 0);
5188 insta::assert_json_snapshot!(metric.abc);
5189 },
5190 );
5191 }
5192
5193 #[test]
5194 fn rust_let_without_initializer_does_not_count() {
5195 // `let a;` is a `let_declaration` with NO `=` and no `value`
5196 // field — the binding is uninitialised. The arm only fires
5197 // when `value` is present, so this contributes zero to A.
5198 // `let _b;` is the same shape (the `_` pattern is still a
5199 // pattern, not a wildcard suppression of the binding).
5200 // Regression test for issue #393: only `=` counts, not the
5201 // bare declaration.
5202 check_metrics::<RustParser>(
5203 "fn f() { let a: i32; let _b: i32; a = 5; }",
5204 "foo.rs",
5205 |metric| {
5206 // Only `a = 5` (assignment_expression) → A = 1.
5207 assert_eq!(metric.abc.assignments_sum(), 1);
5208 insta::assert_json_snapshot!(metric.abc);
5209 },
5210 );
5211 }
5212
5213 #[test]
5214 fn rust_let_initializers_immutable_and_mutable_count() {
5215 // Issue #393: `let a = 1;`, `let b = 2;`, `let c = a + b;`,
5216 // `let mut d = 0;` are all `let_declaration` nodes carrying
5217 // an `=` initializer — each counts as 1 (Option B in the
5218 // issue body: literal Fitzpatrick, both `let` and `let mut`
5219 // count). `d = 5;` is one plain assignment_expression, `d
5220 // += 1;` is one compound. Total A = 4 + 1 + 1 = 6.
5221 check_metrics::<RustParser>(
5222 "fn f() { let a=1; let b=2; let c=a+b; let mut d=0; d=5; d+=1; }",
5223 "foo.rs",
5224 |metric| {
5225 assert_eq!(metric.abc.assignments_sum(), 6);
5226 insta::assert_json_snapshot!(metric.abc);
5227 },
5228 );
5229 }
5230
5231 #[test]
5232 fn rust_calls_are_branches() {
5233 // Free function call + method call (parses as call_expression
5234 // with a field_expression callee) + associated-fn call. All
5235 // three are `call_expression` → B = 3. Macro invocations like
5236 // `println!` parse as `macro_invocation`, NOT `call_expression`,
5237 // so they are not branches.
5238 check_metrics::<RustParser>(
5239 "fn f() { g(); 1.to_string(); String::new(); }\nfn g() {}\n",
5240 "foo.rs",
5241 |metric| {
5242 assert_eq!(metric.abc.branches_sum(), 3);
5243 assert_eq!(metric.abc.assignments_sum(), 0);
5244 assert_eq!(metric.abc.conditions_sum(), 0);
5245 insta::assert_json_snapshot!(metric.abc);
5246 },
5247 );
5248 }
5249
5250 #[test]
5251 fn rust_try_operator_is_branch() {
5252 // `?` parses as `try_expression` and counts as one branch
5253 // (short-circuit return on Err / None). The `Err(())` call
5254 // contributes one branch in addition (call_expression).
5255 check_metrics::<RustParser>(
5256 "fn f() -> Result<i32, ()> { let r: Result<i32, ()> = Err(()); Ok(r?) }",
5257 "foo.rs",
5258 |metric| {
5259 // Err(()) + Ok(...) + r? → 2 calls + 1 try = 3 branches.
5260 assert_eq!(metric.abc.branches_sum(), 3);
5261 insta::assert_json_snapshot!(metric.abc);
5262 },
5263 );
5264 }
5265
5266 #[test]
5267 fn rust_comparisons_count_conditions() {
5268 // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six
5269 // comparisons → C = 6.
5270 check_metrics::<RustParser>(
5271 "fn f(a: i32, b: i32) -> bool { a < b || a > b || a <= b || a >= b || a == b || a != b }",
5272 "foo.rs",
5273 |metric| {
5274 assert_eq!(metric.abc.conditions_sum(), 6);
5275 insta::assert_json_snapshot!(metric.abc);
5276 },
5277 );
5278 }
5279
5280 #[test]
5281 fn rust_generic_brackets_not_conditions() {
5282 // `<` / `>` in `Vec<i32>` are TypeArguments delimiters, not
5283 // comparison operators. The parent-check in the LT/GT arms
5284 // must filter them out. Expected C = 0.
5285 check_metrics::<RustParser>(
5286 "fn f() -> Vec<i32> { Vec::<i32>::new() }",
5287 "foo.rs",
5288 |metric| {
5289 assert_eq!(metric.abc.conditions_sum(), 0);
5290 insta::assert_json_snapshot!(metric.abc);
5291 },
5292 );
5293 }
5294
5295 #[test]
5296 fn rust_if_let_counts_as_condition() {
5297 // `if let Some(v) = opt { ... }` introduces a `let_condition`
5298 // → 1 condition. The `if` keyword itself does not add another
5299 // count — Fitzpatrick counts conditions, not branch keywords.
5300 check_metrics::<RustParser>(
5301 "fn f(opt: Option<i32>) { if let Some(_v) = opt { } }",
5302 "foo.rs",
5303 |metric| {
5304 assert_eq!(metric.abc.conditions_sum(), 1);
5305 insta::assert_json_snapshot!(metric.abc);
5306 },
5307 );
5308 }
5309
5310 #[test]
5311 fn rust_while_let_counts_as_condition() {
5312 // `while let Some(y) = it.next() { ... }` is also a
5313 // `let_condition` (the `while` form). One condition; the
5314 // `it.next()` call adds one branch.
5315 check_metrics::<RustParser>(
5316 "fn f(mut it: std::vec::IntoIter<i32>) { while let Some(_y) = it.next() { } }",
5317 "foo.rs",
5318 |metric| {
5319 assert_eq!(metric.abc.conditions_sum(), 1);
5320 assert_eq!(metric.abc.branches_sum(), 1);
5321 insta::assert_json_snapshot!(metric.abc);
5322 },
5323 );
5324 }
5325
5326 #[test]
5327 fn rust_match_arms_count_conditions_wildcard_excluded() {
5328 // Three arms: `0 => 1`, `n if n > 0 => n`, `_ => -1`. The
5329 // bare wildcard is the `default:` equivalent and is skipped.
5330 // The guarded arm has a `n if n > 0` pattern (more than one
5331 // child in the match_pattern) and still counts. Two non-wildcard
5332 // arms → C = 2 from MatchArm. Plus the comparison `n > 0`
5333 // adds one more → C = 3.
5334 check_metrics::<RustParser>(
5335 "fn f(x: i32) -> i32 { match x { 0 => 1, n if n > 0 => n, _ => -1, } }",
5336 "foo.rs",
5337 |metric| {
5338 assert_eq!(metric.abc.conditions_sum(), 3);
5339 insta::assert_json_snapshot!(metric.abc);
5340 },
5341 );
5342 }
5343
5344 #[test]
5345 fn rust_else_counts_as_condition() {
5346 // `if a > b { ... } else { ... }` → `a > b` is one condition,
5347 // `else` is one condition → C = 2.
5348 check_metrics::<RustParser>(
5349 "fn f(a: i32, b: i32) -> i32 { if a > b { a } else { b } }",
5350 "foo.rs",
5351 |metric| {
5352 assert_eq!(metric.abc.conditions_sum(), 2);
5353 insta::assert_json_snapshot!(metric.abc);
5354 },
5355 );
5356 }
5357
5358 #[test]
5359 fn rust_let_chain2_hidden_rule_drift_marker() {
5360 // Drift marker (findings.md round-2 #3): `Rust::LetChain2`
5361 // maps to the hidden grammar rule `_let_chain`. At the
5362 // pinned tree-sitter-rust version it is never emitted as a
5363 // concrete node — the visible `LetChain` (= 352) carries
5364 // every let-chain. We list `LetChain2` defensively in
5365 // `rust_inspect_container` and `rust_count_unary_conditions`
5366 // (lesson 34); if a future grammar bump promotes
5367 // `_let_chain` to a visible rule, this assertion fails
5368 // loudly so the maintainer knows to verify the walker still
5369 // counts correctly for the new shape.
5370 let src = "fn f(a: bool, b: Option<i32>) {\n\
5371 \x20 if a && let Some(_) = b { }\n\
5372 }\n";
5373 let parser = RustParser::new(
5374 src.as_bytes().to_vec(),
5375 &std::path::PathBuf::from("foo.rs"),
5376 None,
5377 );
5378 assert!(!ast_has_kind_id(&parser, Rust::LetChain2 as u16));
5379 }
5380
5381 #[test]
5382 fn rust_scoped_identifier_condition_counts() {
5383 // Regression for findings.md round-2 #1 (Rust):
5384 // `if crate::FLAG {}` parses with `scoped_identifier` as the
5385 // condition node. Pre-fix, `rust_bool_terminal_kinds!()`
5386 // listed only `Identifier` so the walker reached the
5387 // `scoped_identifier` child, found it non-terminal /
5388 // non-paren / non-unary, and broke without counting.
5389 // Mirrors the C# fix in #372 (lesson 19) for
5390 // `MemberAccessExpression`.
5391 check_metrics::<RustParser>("fn f() { if crate::FLAG { } }\n", "foo.rs", |metric| {
5392 assert_eq!(metric.abc.conditions_sum(), 1);
5393 insta::assert_json_snapshot!(metric.abc);
5394 });
5395 }
5396
5397 #[test]
5398 fn rust_await_expression_condition_counts() {
5399 // Regression for findings.md round-2 #2 (Rust):
5400 // `if ready().await {}` parses with `await_expression` as
5401 // the condition node. Adding `Rust::AwaitExpression` to the
5402 // terminal-bool set closes the parity gap with the C#
5403 // reference (`csharp_bool_terminal_kinds!()`).
5404 check_metrics::<RustParser>(
5405 "async fn ready() -> bool { true }\n\
5406 async fn f() { if ready().await { } }\n",
5407 "foo.rs",
5408 |metric| {
5409 // ready() is a call (1 branch); `ready().await` is
5410 // the unary boolean condition (1).
5411 assert_eq!(metric.abc.branches_sum(), 1);
5412 assert_eq!(metric.abc.conditions_sum(), 1);
5413 insta::assert_json_snapshot!(metric.abc);
5414 },
5415 );
5416 }
5417
5418 #[test]
5419 fn rust_complex_function_abc() {
5420 // Mixed-shape regression: assignments, calls, conditions, `?`,
5421 // `if let`, `match` in one body. Verified by hand:
5422 // - assignments: `let mut x = 0` (let init), `x = 5`, `x += 2`,
5423 // `let _ = ...` (let init), `let r: ... = Err(())` (let init),
5424 // `let _v = r?` (let init) → A = 6 (post-#393: every `=`
5425 // initializer in a `let_declaration` is one assignment, in
5426 // line with the literal Fitzpatrick reading).
5427 // - branches: `xs.iter()`, `.next()`, `Err(())`, `r?` → B = 4
5428 // (3 calls + 1 try).
5429 // - conditions: `if let Some(v) = opt` → 1, `match x` arms
5430 // `0`, `n if n>0` (wildcard excluded) → 2, `n > 0` → 1.
5431 // Total C = 4.
5432 check_metrics::<RustParser>(
5433 "fn f(opt: Option<i32>, xs: Vec<i32>) -> Result<i32, ()> {\n\
5434 \x20 let mut x = 0;\n\
5435 \x20 x = 5;\n\
5436 \x20 x += 2;\n\
5437 \x20 if let Some(_v) = opt { }\n\
5438 \x20 let _ = xs.iter().next();\n\
5439 \x20 let r: Result<i32, ()> = Err(());\n\
5440 \x20 let _v = r?;\n\
5441 \x20 Ok(match x {\n\
5442 \x20 0 => 1,\n\
5443 \x20 n if n > 0 => n,\n\
5444 \x20 _ => -1,\n\
5445 \x20 })\n\
5446 }\n",
5447 "foo.rs",
5448 |metric| {
5449 assert_eq!(metric.abc.assignments_sum(), 6);
5450 // calls: xs.iter(), .next(), Err(()), Ok(...) → 4 calls
5451 // plus 1 try (`r?`) → 5 branches.
5452 assert_eq!(metric.abc.branches_sum(), 5);
5453 // 1 let_condition + 2 non-wildcard match_arms + 1
5454 // comparison (`n > 0`) → 4.
5455 assert_eq!(metric.abc.conditions_sum(), 4);
5456 insta::assert_json_snapshot!(metric.abc);
5457 },
5458 );
5459 }
5460
5461 #[test]
5462 fn rust_let_chain_bare_identifier_operand_counts() {
5463 // Regression: pre-fix, `if a && let Some(_z) = y { }` reported
5464 // 1 condition (only the LetCondition). The bare-identifier
5465 // `a` operand was lost because Rust 2024 wraps let-chain
5466 // `&&` operands in a `LetChain` node (not `BinaryExpression`)
5467 // and `rust_count_unary_conditions` only counted terminals
5468 // under a `BinaryExpression` parent. Allowing `LetChain` /
5469 // `LetChain2` as known-bool list parents fixes the loss.
5470 // Expected: LetCondition (1) + walker on `a` (1) = 2.
5471 check_metrics::<RustParser>(
5472 "fn f(a: bool, y: Option<i32>) {\n\
5473 \x20 if a && let Some(_z) = y { }\n\
5474 }\n",
5475 "foo.rs",
5476 |metric| {
5477 assert_eq!(metric.abc.conditions_sum(), 2);
5478 insta::assert_json_snapshot!(metric.abc);
5479 },
5480 );
5481 }
5482
5483 #[test]
5484 fn rust_if_multiple_conditions() {
5485 // Fitzpatrick Rule 7 / Listing 2 (issue #403): every operand of
5486 // a `&&` / `||` chain is one condition. Mirrors
5487 // `java_if_multiple_conditions`. Rust's `if` head has no
5488 // parentheses, but the walker fires on each `&&` / `||` token
5489 // and walks the parent `binary_expression` regardless.
5490 check_metrics::<RustParser>(
5491 "fn f(a: bool, b: bool, c: bool, d: bool) -> i32 {\n\
5492 \x20 if a || b || c || d { return 1; } // +4c\n\
5493 \x20 if a && b && c { return 2; } // +3c\n\
5494 \x20 if !a && !b { return 3; } // +2c\n\
5495 \x20 0\n\
5496 }\n",
5497 "foo.rs",
5498 |metric| {
5499 // 4 + 3 + 2 = 9
5500 assert_eq!(metric.abc.conditions_sum(), 9);
5501 insta::assert_json_snapshot!(metric.abc);
5502 },
5503 );
5504 }
5505
5506 #[test]
5507 fn rust_while_conditions() {
5508 // Rust has no `do { ... } while(cond);` construct, so this
5509 // mirrors only the `while` half of `java_while_and_do_while_conditions`.
5510 // Each operand of the `&&` / `||` chain in the loop condition
5511 // counts as one Fitzpatrick condition (Rule 7).
5512 check_metrics::<RustParser>(
5513 "fn f(a: bool, b: bool) {\n\
5514 \x20 while a || b { break; } // +2c\n\
5515 \x20 while a && !b { break; } // +2c\n\
5516 }\n",
5517 "foo.rs",
5518 |metric| {
5519 assert_eq!(metric.abc.conditions_sum(), 4);
5520 insta::assert_json_snapshot!(metric.abc);
5521 },
5522 );
5523 }
5524
5525 #[test]
5526 fn rust_if_boolean_literal_condition() {
5527 // Phase 2B (issue #403): a condition whose entire body is a
5528 // boolean literal counts as one Fitzpatrick condition.
5529 // `if true {}` → 1, `if !false {}` → 1 (unary unwrap), and
5530 // `while true { break }` → 1.
5531 check_metrics::<RustParser>(
5532 "fn f() {\n\
5533 \x20 if true { } // +1c\n\
5534 \x20 if !false { } // +1c\n\
5535 \x20 while true { break; } // +1c\n\
5536 }\n",
5537 "foo.rs",
5538 |metric| {
5539 assert_eq!(metric.abc.conditions_sum(), 3);
5540 insta::assert_json_snapshot!(metric.abc);
5541 },
5542 );
5543 }
5544
5545 #[test]
5546 fn rust_methods_arguments_with_conditions() {
5547 // Phase 2B (issue #403): unary-conditional arguments to a
5548 // call each count once. `m(!a, !b)` → 2 conditions + 1
5549 // branch (the call itself). Bare identifier arguments do
5550 // NOT count (they reach the count_unary_conditions list with
5551 // list_kind = Arguments, not BinaryExpression).
5552 check_metrics::<RustParser>(
5553 "fn f(a: bool, b: bool) {\n\
5554 \x20 m(a, b); // +1b\n\
5555 \x20 m(!a, !b); // +1b +2c\n\
5556 \x20 m(!a, b, !a); // +1b +2c\n\
5557 }\n",
5558 "foo.rs",
5559 |metric| {
5560 assert_eq!(metric.abc.branches_sum(), 3);
5561 assert_eq!(metric.abc.conditions_sum(), 4);
5562 insta::assert_json_snapshot!(metric.abc);
5563 },
5564 );
5565 }
5566
5567 #[test]
5568 fn rust_return_with_conditions() {
5569 // Phase 2B (issue #403). Mirrors `java_return_with_conditions`
5570 // — `return !a` / `return x && y` count their unary
5571 // conditional operands. Per Fitzpatrick Rule 7, a `!`-wrapped
5572 // relational expression contributes ONE condition (the
5573 // relational op itself) — the `!` does not add a second
5574 // count when its operand is already a comparison.
5575 check_metrics::<RustParser>(
5576 "fn m1(z: i32) -> bool { return !(z >= 0); }\n\
5577 fn m2(x: bool) -> bool { return (((!x))); }\n\
5578 fn m3(x: bool, y: bool) -> bool { return x && y; }\n\
5579 fn m4(y: bool, z: i32) -> bool { return y || (z < 0); }\n",
5580 "foo.rs",
5581 |metric| {
5582 // m1: !(z >= 0) → the `>=` contributes 1; the unary
5583 // `!` wraps a paren'd BinaryExpression, which
5584 // inspect_container does not unwrap further →
5585 // no walker count. Total: 1.
5586 // m2: (((!x))) → ReturnExpression arm walks (((!x))).
5587 // inspect_container unwraps three parens and one
5588 // unary, reaches Identifier `x`, has_boolean_content
5589 // was seeded true by the unary-not flip. +1.
5590 // m3: x && y → `&&` walker counts both terminals → 2.
5591 // m4: y || (z < 0) → `||` walker counts `y` (terminal,
5592 // +1); the `<` contributes 1 via its own arm; the
5593 // paren'd BinaryExpression `(z < 0)` is not
5594 // terminal under the walker → no extra count.
5595 // Total: 2.
5596 // Sum: 1 + 1 + 2 + 2 = 6.
5597 assert_eq!(metric.abc.conditions_sum(), 6);
5598 insta::assert_json_snapshot!(metric.abc);
5599 },
5600 );
5601 }
5602
5603 #[test]
5604 fn rust_short_circuit_with_boolean_literal_operand() {
5605 // `if a && true` reports 2 conditions: one for the identifier
5606 // operand, one for the boolean-literal operand. Confirms the
5607 // walker terminal set includes `BooleanLiteral`.
5608 check_metrics::<RustParser>(
5609 "fn f(a: bool) -> bool { a && true }\n",
5610 "foo.rs",
5611 |metric| {
5612 assert_eq!(metric.abc.conditions_sum(), 2);
5613 insta::assert_json_snapshot!(metric.abc);
5614 },
5615 );
5616 }
5617
5618 // ----- Go -----
5619
5620 #[test]
5621 fn go_empty_unit_zero() {
5622 // Package declaration only — no Fitzpatrick events. Confirms the
5623 // GoCode Abc trait is wired up and emits zero counts.
5624 check_metrics::<GoParser>("package main\n", "empty.go", |metric| {
5625 assert_eq!(metric.abc.assignments_sum(), 0);
5626 assert_eq!(metric.abc.branches_sum(), 0);
5627 assert_eq!(metric.abc.conditions_sum(), 0);
5628 insta::assert_json_snapshot!(metric.abc);
5629 });
5630 }
5631
5632 #[test]
5633 fn go_assignments_count_plain_compound_short_var_and_incdec() {
5634 // `x := 0` (short var decl), `x = 5` and `x = 7` (plain `=`),
5635 // `x += 2` (compound), `x++` (inc) → A = 5. `var y = 1` is a
5636 // declaration — its `=` is not counted (matches the Rust/Java
5637 // rule for `let` / `int y = 1`).
5638 check_metrics::<GoParser>(
5639 "package main\nfunc f() { var y = 1; _ = y; x := 0; x = 5; x += 2; x = 7; x++ }\n",
5640 "foo.go",
5641 |metric| {
5642 // `_ = y` is itself an assignment_statement → +1.
5643 // x:= + x=5 + x+=2 + x=7 + x++ + _=y → 6
5644 assert_eq!(metric.abc.assignments_sum(), 6);
5645 assert_eq!(metric.abc.branches_sum(), 0);
5646 assert_eq!(metric.abc.conditions_sum(), 0);
5647 insta::assert_json_snapshot!(metric.abc);
5648 },
5649 );
5650 }
5651
5652 #[test]
5653 fn go_calls_are_branches() {
5654 // Three calls: free function `g()`, method call `r.Inc()`, and
5655 // builtin call `len(s)`. All parse as `call_expression` → B = 3.
5656 // Composite literal `Foo{}` is NOT a call.
5657 check_metrics::<GoParser>(
5658 "package main\n\
5659 type R struct{}\n\
5660 func (r R) Inc() {}\n\
5661 func g() {}\n\
5662 func f(s string) { g(); var r R = R{}; r.Inc(); _ = len(s) }\n",
5663 "foo.go",
5664 |metric| {
5665 assert_eq!(metric.abc.branches_sum(), 3);
5666 insta::assert_json_snapshot!(metric.abc);
5667 },
5668 );
5669 }
5670
5671 #[test]
5672 fn go_comparisons_count_conditions() {
5673 // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six
5674 // comparisons → C = 6.
5675 check_metrics::<GoParser>(
5676 "package main\nfunc f(a, b int) bool { return a < b || a > b || a <= b || a >= b || a == b || a != b }\n",
5677 "foo.go",
5678 |metric| {
5679 assert_eq!(metric.abc.conditions_sum(), 6);
5680 insta::assert_json_snapshot!(metric.abc);
5681 },
5682 );
5683 }
5684
5685 #[test]
5686 fn go_generic_brackets_not_conditions() {
5687 // Generic instantiation `Min[int](a, b)` puts `int` inside
5688 // `TypeArguments`, not `BinaryExpression`. The parent guard on
5689 // `<` / `>` must not count these. Expected C = 0; B = 1 (one call).
5690 check_metrics::<GoParser>(
5691 "package main\nfunc Min[T int | float64](a, b T) T { return a }\nfunc f() { _ = Min[int](1, 2) }\n",
5692 "foo.go",
5693 |metric| {
5694 assert_eq!(metric.abc.conditions_sum(), 0);
5695 assert_eq!(metric.abc.branches_sum(), 1);
5696 insta::assert_json_snapshot!(metric.abc);
5697 },
5698 );
5699 }
5700
5701 #[test]
5702 fn go_switch_arms_count_conditions_default_excluded() {
5703 // Four arms: `case 1:`, `case 2:`, `case 3:`, `default:`. The
5704 // bare `default` is the C/Java `default:` equivalent and is
5705 // excluded — 3 conditions from ExpressionCase. The switch
5706 // expression `x` is bare (no comparison), so no extra
5707 // condition from `==`-style operators.
5708 check_metrics::<GoParser>(
5709 "package main\nfunc f(x int) int { switch x { case 1: return 1; case 2: return 2; case 3: return 3; default: return 0 } }\n",
5710 "foo.go",
5711 |metric| {
5712 assert_eq!(metric.abc.conditions_sum(), 3);
5713 insta::assert_json_snapshot!(metric.abc);
5714 },
5715 );
5716 }
5717
5718 #[test]
5719 fn go_type_switch_arms_count_conditions() {
5720 // Type switch: `case int:`, `case string:`, `default:`. Two
5721 // non-default type-case arms → C = 2.
5722 check_metrics::<GoParser>(
5723 "package main\nfunc f(v interface{}) { switch v.(type) { case int: return; case string: return; default: return } }\n",
5724 "foo.go",
5725 |metric| {
5726 assert_eq!(metric.abc.conditions_sum(), 2);
5727 insta::assert_json_snapshot!(metric.abc);
5728 },
5729 );
5730 }
5731
5732 #[test]
5733 fn go_select_arms_count_conditions() {
5734 // `select { case <-ch: ...; case ch <- 1: ...; default: ... }`.
5735 // Two non-default communication cases → C = 2.
5736 check_metrics::<GoParser>(
5737 "package main\nfunc f(ch chan int) { select { case <-ch: return; case ch <- 1: return; default: return } }\n",
5738 "foo.go",
5739 |metric| {
5740 assert_eq!(metric.abc.conditions_sum(), 2);
5741 insta::assert_json_snapshot!(metric.abc);
5742 },
5743 );
5744 }
5745
5746 #[test]
5747 fn go_else_counts_as_condition() {
5748 // `if a > b { ... } else { ... }` → `a > b` is one condition,
5749 // `else` is one condition → C = 2.
5750 check_metrics::<GoParser>(
5751 "package main\nfunc f(a, b int) int { if a > b { return a } else { return b } }\n",
5752 "foo.go",
5753 |metric| {
5754 assert_eq!(metric.abc.conditions_sum(), 2);
5755 insta::assert_json_snapshot!(metric.abc);
5756 },
5757 );
5758 }
5759
5760 #[test]
5761 fn go_complex_function_abc() {
5762 // Mixed shape, verified by hand:
5763 // - Assignments: `_ = x` (after `var`), `n := 0`,
5764 // `n = n + 1`, `n += 2`, `n++`, `_ = len(s)` → A = 6.
5765 // `var x = 10` is a declaration, not counted. Every `_ = ...`
5766 // IS counted as an assignment_statement.
5767 // - Branches: `len(s)` → B = 1.
5768 // - Conditions: `n < 10` → 1, `else` → 1, switch arms `case 0:`
5769 // and `case 1:` (default excluded) → 2 → total C = 4.
5770 check_metrics::<GoParser>(
5771 "package main\nfunc f(s string) int {\n\
5772 \x20 var x = 10\n\
5773 \x20 _ = x\n\
5774 \x20 n := 0\n\
5775 \x20 if n < 10 { n = n + 1 } else { n += 2 }\n\
5776 \x20 n++\n\
5777 \x20 _ = len(s)\n\
5778 \x20 switch n {\n\
5779 \x20 case 0: return 0\n\
5780 \x20 case 1: return 1\n\
5781 \x20 default: return n\n\
5782 \x20 }\n\
5783 }\n",
5784 "foo.go",
5785 |metric| {
5786 assert_eq!(metric.abc.assignments_sum(), 6);
5787 assert_eq!(metric.abc.branches_sum(), 1);
5788 assert_eq!(metric.abc.conditions_sum(), 4);
5789 insta::assert_json_snapshot!(metric.abc);
5790 },
5791 );
5792 }
5793
5794 #[test]
5795 fn go_if_multiple_conditions() {
5796 // Fitzpatrick Rule 7 walker fan-out (issue #403). Mirrors
5797 // `rust_if_multiple_conditions`.
5798 check_metrics::<GoParser>(
5799 "package p\n\
5800 func F(a, b, c, d bool) int {\n\
5801 \x20 if a || b || c || d { return 1 } // +4c\n\
5802 \x20 if a && b && c { return 2 } // +3c\n\
5803 \x20 if !a && !b { return 3 } // +2c\n\
5804 \x20 return 0\n\
5805 }\n",
5806 "foo.go",
5807 |metric| {
5808 assert_eq!(metric.abc.conditions_sum(), 9);
5809 insta::assert_json_snapshot!(metric.abc);
5810 },
5811 );
5812 }
5813
5814 #[test]
5815 fn go_for_with_conditions() {
5816 // Go has no `while` or `do { … } while(…);` — the `for` loop
5817 // header is the sole condition slot. Each operand of the
5818 // `&&` / `||` chain in the for-condition counts as one
5819 // Fitzpatrick condition.
5820 check_metrics::<GoParser>(
5821 "package p\n\
5822 func F(a, b bool) {\n\
5823 \x20 for a || b { break } // +2c\n\
5824 \x20 for a && !b { break } // +2c\n\
5825 }\n",
5826 "foo.go",
5827 |metric| {
5828 assert_eq!(metric.abc.conditions_sum(), 4);
5829 insta::assert_json_snapshot!(metric.abc);
5830 },
5831 );
5832 }
5833
5834 #[test]
5835 fn go_for_bare_condition_counts() {
5836 // Regression for findings.md #1: `for true {}` / `for !ready {}`
5837 // are Go's only loop-condition slot. Pre-fix, the Phase-2B
5838 // dispatcher had no `G::ForStatement` arm, so bare-boolean
5839 // and `!`-wrapped `for` conditions silently reported zero.
5840 // `go_count_condition`'s terminal-bool / paren / unary filter
5841 // makes the arm safe across all three for-statement shapes:
5842 // bare condition, `for_clause` (init; cond; post), and
5843 // `range_clause` (the latter two fall through harmlessly).
5844 check_metrics::<GoParser>(
5845 "package p\n\
5846 func F(ready bool) {\n\
5847 \x20 for true { break } // +1c\n\
5848 \x20 for !ready { break } // +1c\n\
5849 \x20 for i := 0; i < 3; i++ { _ = i } // +1c (the `<`)\n\
5850 }\n",
5851 "foo.go",
5852 |metric| {
5853 // `for true`: walker counts True (+1).
5854 // `for !ready`: walker on unary unwraps to `ready`
5855 // (+1).
5856 // `for_clause` falls through go_count_condition with
5857 // no count; the inner `i < 3` contributes 1 via the
5858 // pre-existing LT/GT arm.
5859 // Total: 3.
5860 assert_eq!(metric.abc.conditions_sum(), 3);
5861 insta::assert_json_snapshot!(metric.abc);
5862 },
5863 );
5864 }
5865
5866 #[test]
5867 fn go_if_init_statement_condition_counts() {
5868 // Regression for the code-review finding: Go's
5869 // `if x := f(); x { ... }` init-statement form puts the
5870 // short-var declaration at child(1) and the condition at
5871 // child(2). Pre-fix, the dispatcher used child(1) and
5872 // counted zero conditions for this idiomatic Go shape.
5873 // The fix uses `child_by_field_name("condition")` which
5874 // returns the condition regardless of init presence.
5875 check_metrics::<GoParser>(
5876 "package p\nfunc F() { if x := g(); x { } }\n",
5877 "foo.go",
5878 |metric| {
5879 // `x` bare-identifier condition contributes 1
5880 // (Rule 6 — bare boolean identifier in if-condition).
5881 // `g()` call contributes 1 branch but no condition.
5882 assert_eq!(metric.abc.branches_sum(), 1);
5883 assert_eq!(metric.abc.conditions_sum(), 1);
5884 insta::assert_json_snapshot!(metric.abc);
5885 },
5886 );
5887 }
5888
5889 #[test]
5890 fn go_if_boolean_literal_condition() {
5891 check_metrics::<GoParser>(
5892 "package p\n\
5893 func F() {\n\
5894 \x20 if true {} // +1c\n\
5895 \x20 if !false {} // +1c\n\
5896 }\n",
5897 "foo.go",
5898 |metric| {
5899 assert_eq!(metric.abc.conditions_sum(), 2);
5900 insta::assert_json_snapshot!(metric.abc);
5901 },
5902 );
5903 }
5904
5905 #[test]
5906 fn go_methods_arguments_with_conditions() {
5907 check_metrics::<GoParser>(
5908 "package p\n\
5909 func F(a, b bool) {\n\
5910 \x20 m(a, b) // +1b\n\
5911 \x20 m(!a, !b) // +1b +2c\n\
5912 }\n",
5913 "foo.go",
5914 |metric| {
5915 assert_eq!(metric.abc.branches_sum(), 2);
5916 assert_eq!(metric.abc.conditions_sum(), 2);
5917 insta::assert_json_snapshot!(metric.abc);
5918 },
5919 );
5920 }
5921
5922 #[test]
5923 fn go_return_with_conditions() {
5924 check_metrics::<GoParser>(
5925 "package p\n\
5926 func M1(z int) bool { return !(z >= 0) }\n\
5927 func M2(x bool) bool { return !x }\n\
5928 func M3(x, y bool) bool { return x && y }\n",
5929 "foo.go",
5930 |metric| {
5931 // M1: `>=` (1). `!(z >= 0)` walker on the unary
5932 // doesn't reach a terminal — stops at the
5933 // BinaryExpression z>=0 inside the parens. +1.
5934 // M2: walker on `!x` → 1.
5935 // M3: `&&` walker counts both → 2.
5936 // Sum: 1 + 1 + 2 = 4.
5937 assert_eq!(metric.abc.conditions_sum(), 4);
5938 insta::assert_json_snapshot!(metric.abc);
5939 },
5940 );
5941 }
5942
5943 #[test]
5944 fn go_short_circuit_with_boolean_literal_operand() {
5945 // `a && true` reports 2 conditions: one identifier, one
5946 // boolean literal. Confirms the terminal set includes
5947 // `True` / `False`.
5948 check_metrics::<GoParser>(
5949 "package p\nfunc F(a bool) bool { return a && true }\n",
5950 "foo.go",
5951 |metric| {
5952 assert_eq!(metric.abc.conditions_sum(), 2);
5953 insta::assert_json_snapshot!(metric.abc);
5954 },
5955 );
5956 }
5957
5958 // ----- Elixir -----
5959
5960 // No top-level Calls and no operators → all three vectors are
5961 // zero. Uses a bare expression rather than a `defmodule` wrapper
5962 // (which would itself be a Call → 1 branch). Confirms the
5963 // ElixirCode Abc trait is wired up and the metric emits.
5964 #[test]
5965 fn elixir_empty_unit_zero() {
5966 check_metrics::<ElixirParser>(":ok\n", "foo.ex", |metric| {
5967 assert_eq!(metric.abc.assignments_sum(), 0);
5968 assert_eq!(metric.abc.branches_sum(), 0);
5969 assert_eq!(metric.abc.conditions_sum(), 0);
5970 insta::assert_json_snapshot!(metric.abc);
5971 });
5972 }
5973
5974 // An empty `defmodule Foo do ... end` is itself ONE `Call` →
5975 // Documents that module-/function-defining macros (`defmodule`,
5976 // `def`, `defp`, `defmacro`, `defmacrop`) and declarative
5977 // directives (`alias`, `import`, `require`, `use`) are NOT
5978 // runtime dispatch and therefore do NOT inflate `branches`,
5979 // matching Cognitive's treatment.
5980 #[test]
5981 fn elixir_defmodule_is_zero_branches() {
5982 check_metrics::<ElixirParser>("defmodule Foo do\nend\n", "foo.ex", |metric| {
5983 assert_eq!(metric.abc.branches_sum(), 0);
5984 assert_eq!(metric.abc.assignments_sum(), 0);
5985 assert_eq!(metric.abc.conditions_sum(), 0);
5986 insta::assert_json_snapshot!(metric.abc);
5987 });
5988 }
5989
5990 // Pattern-match `=` counts as an assignment. Two bindings → A = 2.
5991 // `defmodule` and `def` are declarative-Call wrappers and are
5992 // filtered out of branches; the assertion focuses on assignments
5993 // so we only pin that vector.
5994 #[test]
5995 fn elixir_pattern_match_is_assignment() {
5996 check_metrics::<ElixirParser>(
5997 "defmodule Foo do\n def f do\n x = 1\n y = x + 1\n y\n end\nend\n",
5998 "foo.ex",
5999 |metric| {
6000 assert_eq!(metric.abc.assignments_sum(), 2);
6001 insta::assert_json_snapshot!(metric.abc);
6002 },
6003 );
6004 }
6005
6006 // `|>` pipeline operator: each `|>` token contributes one branch.
6007 // Two `|>` ops → +2 from the pipe operator itself. Each pipeline
6008 // step also dispatches a Call (`String.upcase(...)`,
6009 // `String.trim(...)`) — these are wrapped inside the outer
6010 // pipeline Call tree, contributing additional Call branches.
6011 // The headline assertion confirms (a) `|>` is detected and (b)
6012 // pipeline steps are not silently dropped.
6013 #[test]
6014 fn elixir_pipeline_each_step_is_branch() {
6015 check_metrics::<ElixirParser>(
6016 "defmodule Foo do\n def normalize(s) do\n s |> String.trim() |> String.upcase()\n end\nend\n",
6017 "foo.ex",
6018 |metric| {
6019 // Pipeline yields 2 `|>` branches plus Calls for
6020 // String.trim, String.upcase, and the outer pipeline
6021 // (which surfaces as a Call wrapping the binary
6022 // operator). `def` and `defmodule` are declarative
6023 // and excluded. Empirical total: B = 5.
6024 assert_eq!(metric.abc.branches_sum(), 5);
6025 assert_eq!(metric.abc.assignments_sum(), 0);
6026 insta::assert_json_snapshot!(metric.abc);
6027 },
6028 );
6029 }
6030
6031 // Comparison operators all count as conditions. Six comparisons
6032 // (`==`, `!=`, `<`, `>`, `<=`, `>=`) → C = 6.
6033 #[test]
6034 fn elixir_comparisons_are_conditions() {
6035 check_metrics::<ElixirParser>(
6036 "defmodule Foo do\n def f(a, b) do\n a == b or a != b or a < b or a > b or a <= b or a >= b\n end\nend\n",
6037 "foo.ex",
6038 |metric| {
6039 assert_eq!(metric.abc.conditions_sum(), 6);
6040 insta::assert_json_snapshot!(metric.abc);
6041 },
6042 );
6043 }
6044
6045 // Strict-equality operators `===` / `!==` count as conditions too.
6046 #[test]
6047 fn elixir_strict_equality_is_condition() {
6048 check_metrics::<ElixirParser>(
6049 "defmodule Foo do\n def f(a, b) do\n a === b or a !== b\n end\nend\n",
6050 "foo.ex",
6051 |metric| {
6052 assert_eq!(metric.abc.conditions_sum(), 2);
6053 insta::assert_json_snapshot!(metric.abc);
6054 },
6055 );
6056 }
6057
6058 // Guard `when` clause counts as a condition. One `when` → +1.
6059 // `def f(x) when x > 0` also has `>` → +1, totalling 2.
6060 #[test]
6061 fn elixir_guard_when_is_condition() {
6062 check_metrics::<ElixirParser>(
6063 "defmodule Foo do\n def f(x) when x > 0 do\n :pos\n end\nend\n",
6064 "foo.ex",
6065 |metric| {
6066 // when (+1) + > (+1) = 2
6067 assert_eq!(metric.abc.conditions_sum(), 2);
6068 insta::assert_json_snapshot!(metric.abc);
6069 },
6070 );
6071 }
6072
6073 // Keyword-shaped Calls (`case`, `cond`, `if`, `with`) each count
6074 // as one condition AND one branch. `case` here adds 1 condition
6075 // (the keyword Call) + 1 branch (the Call itself).
6076 #[test]
6077 fn elixir_case_is_condition_and_branch() {
6078 check_metrics::<ElixirParser>(
6079 "defmodule Foo do\n def f(x) do\n case x do\n 1 -> :one\n _ -> :other\n end\n end\nend\n",
6080 "foo.ex",
6081 |metric| {
6082 // conditions: case → 1
6083 assert_eq!(metric.abc.conditions_sum(), 1);
6084 insta::assert_json_snapshot!(metric.abc);
6085 },
6086 );
6087 }
6088
6089 // `cond` is structurally identical to `case` for Abc.
6090 #[test]
6091 fn elixir_cond_is_condition() {
6092 check_metrics::<ElixirParser>(
6093 "defmodule Foo do\n def f(x) do\n cond do\n x > 0 -> :pos\n true -> :other\n end\n end\nend\n",
6094 "foo.ex",
6095 |metric| {
6096 // conditions: cond (+1) + > (+1) = 2
6097 assert_eq!(metric.abc.conditions_sum(), 2);
6098 insta::assert_json_snapshot!(metric.abc);
6099 },
6100 );
6101 }
6102
6103 // `for` is a comprehension/loop, NOT in the issue's condition
6104 // list. It is still a Call so it contributes one branch, but no
6105 // condition.
6106 #[test]
6107 fn elixir_for_is_branch_not_condition() {
6108 check_metrics::<ElixirParser>(
6109 "defmodule Foo do\n def f(xs) do\n for x <- xs, do: x * 2\n end\nend\n",
6110 "foo.ex",
6111 |metric| {
6112 assert_eq!(metric.abc.conditions_sum(), 0);
6113 insta::assert_json_snapshot!(metric.abc);
6114 },
6115 );
6116 }
6117
6118 // Mixed shape, verified by hand: defmodule Call + def Call + if Call
6119 // + Call to side_effect/0 + assignment `x = 1` + comparison `x > 0`.
6120 // - Assignments: `x = 1` → A = 1.
6121 // - Branches: `defmodule` and `def` are declarative and excluded;
6122 // `if` Call + `side_effect()` Call → 2 Calls, plus 0 `|>` → B = 2.
6123 // - Conditions: `if` keyword → 1, `x > 0` → 1 → C = 2.
6124 #[test]
6125 fn elixir_mixed_abc() {
6126 check_metrics::<ElixirParser>(
6127 "defmodule Foo do\n def f do\n x = 1\n if x > 0 do\n side_effect()\n end\n end\nend\n",
6128 "foo.ex",
6129 |metric| {
6130 assert_eq!(metric.abc.assignments_sum(), 1);
6131 assert_eq!(metric.abc.branches_sum(), 2);
6132 assert_eq!(metric.abc.conditions_sum(), 2);
6133 insta::assert_json_snapshot!(metric.abc);
6134 },
6135 );
6136 }
6137
6138 #[test]
6139 fn elixir_unary_conditions_in_chain() {
6140 // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
6141 // `&&` / `||` chain is one condition. For `if a && b || c`: the
6142 // `if` keyword Call contributes 1 condition, and the walker adds
6143 // a, b, c → 3. expected: 4 conditions, consistent with the
6144 // function's cyclomatic complexity of 4 (base 1 + if + && + ||).
6145 check_metrics::<ElixirParser>(
6146 "defmodule Foo do\n def f(a, b, c) do\n if a && b || c do\n IO.puts(\"x\")\n end\n end\nend\n",
6147 "foo.ex",
6148 |metric| {
6149 assert_eq!(metric.abc.conditions_sum(), 4);
6150 },
6151 );
6152 }
6153
6154 #[test]
6155 fn elixir_comparison_operands_add_nothing() {
6156 // Isolation check: comparison operands of a `&&` chain are nested
6157 // `binary_operator` nodes, not bare boolean leaves, so the walker
6158 // adds nothing. expected: 3 = `if` (1) + `>` (1) + `>` (1); the
6159 // `&&` walker contributes 0.
6160 check_metrics::<ElixirParser>(
6161 "defmodule Foo do\n def f(x, y) do\n if x > 0 && y > 0 do\n IO.puts(\"x\")\n end\n end\nend\n",
6162 "foo.ex",
6163 |metric| {
6164 assert_eq!(metric.abc.conditions_sum(), 3);
6165 },
6166 );
6167 }
6168
6169 #[test]
6170 fn elixir_keyword_and_or_chain_counts_operands() {
6171 // The keyword forms `and` / `or` get the same Rule 9 treatment as
6172 // `&&` / `||`. expected: 4 = `if` (1) + operands a, b, c (3).
6173 check_metrics::<ElixirParser>(
6174 "defmodule Foo do\n def f(a, b, c) do\n if a and b or c do\n IO.puts(\"x\")\n end\n end\nend\n",
6175 "foo.ex",
6176 |metric| {
6177 assert_eq!(metric.abc.conditions_sum(), 4);
6178 },
6179 );
6180 }
6181
6182 // ----- C++ -----
6183
6184 #[test]
6185 fn cpp_empty_unit_zero() {
6186 // No code → A=B=C=0. Wires up the trait and exercises the
6187 // per-language compute reachability.
6188 check_metrics::<CppParser>("", "empty.cpp", |metric| {
6189 assert_eq!(metric.abc.assignments_sum(), 0);
6190 assert_eq!(metric.abc.branches_sum(), 0);
6191 assert_eq!(metric.abc.conditions_sum(), 0);
6192 insta::assert_json_snapshot!(metric.abc);
6193 });
6194 }
6195
6196 #[test]
6197 fn cpp_plain_and_compound_assignments_count() {
6198 // `int x = 0` is an `init_declarator` carrying an `=` token
6199 // and counts as 1 (post-#393: the literal Fitzpatrick rule
6200 // counts every `=` operator, matching the JS impl's
6201 // `let x = 5` treatment). `x = 5`, `x += 2`, `x = 7` all
6202 // parse as `assignment_expression` → 3. Total A = 4.
6203 check_metrics::<CppParser>(
6204 "void f() { int x = 0; x = 5; x += 2; x = 7; }",
6205 "foo.cpp",
6206 |metric| {
6207 assert_eq!(metric.abc.assignments_sum(), 4);
6208 assert_eq!(metric.abc.branches_sum(), 0);
6209 assert_eq!(metric.abc.conditions_sum(), 0);
6210 insta::assert_json_snapshot!(metric.abc);
6211 },
6212 );
6213 }
6214
6215 #[test]
6216 fn cpp_increment_and_decrement_count_as_assignment() {
6217 // `x++` / `--x` / prefix and postfix forms each parse as
6218 // `update_expression` and count as 1 assignment per
6219 // Fitzpatrick — 4. `int x = 0` (init_declarator with `=`)
6220 // adds 1 (post-#393). Total A = 5.
6221 check_metrics::<CppParser>(
6222 "void f() { int x = 0; x++; --x; ++x; x--; }",
6223 "foo.cpp",
6224 |metric| {
6225 assert_eq!(metric.abc.assignments_sum(), 5);
6226 insta::assert_json_snapshot!(metric.abc);
6227 },
6228 );
6229 }
6230
6231 #[test]
6232 fn cpp_init_declarators_count_as_assignments() {
6233 // Issue #393 regression: `int a=1;`, `int b=2;`, `int c=a+b;`,
6234 // `int d=0;` are all `init_declarator` nodes with `=` → 4
6235 // assignments. `d=5;` is one plain `assignment_expression`,
6236 // `d+=1;` is one compound. Total A = 4 + 1 + 1 = 6.
6237 check_metrics::<CppParser>(
6238 "void f() { int a=1; int b=2; int c=a+b; int d=0; d=5; d+=1; }",
6239 "foo.cpp",
6240 |metric| {
6241 assert_eq!(metric.abc.assignments_sum(), 6);
6242 insta::assert_json_snapshot!(metric.abc);
6243 },
6244 );
6245 }
6246
6247 #[test]
6248 fn cpp_declaration_without_initializer_does_not_count() {
6249 // `int a;` parses as a plain declarator inside `declaration`,
6250 // NOT an `init_declarator` (the latter only appears when an
6251 // initializer is present). Regression test for issue #393:
6252 // un-initialised declarations contribute zero to A.
6253 check_metrics::<CppParser>("void f() { int a; a = 5; }", "foo.cpp", |metric| {
6254 // Only `a = 5` (assignment_expression) → A = 1.
6255 assert_eq!(metric.abc.assignments_sum(), 1);
6256 insta::assert_json_snapshot!(metric.abc);
6257 });
6258 }
6259
6260 #[test]
6261 fn cpp_init_declarator_brace_paren_init_does_not_count() {
6262 // `init_declarator` has two grammar forms: `declarator = value`
6263 // (the `=` form) and `declarator argument_list_or_initializer_list`
6264 // (the `int x(5);` / `int x{5};` direct-init forms). Only the
6265 // first form contains an `=` token, so only it should count.
6266 // Regression test pinning that distinction so that
6267 // refactorings of the init_declarator arm don't accidentally
6268 // start counting direct-init too.
6269 check_metrics::<CppParser>(
6270 "void f() { int x(5); int y{7}; x = 1; }",
6271 "foo.cpp",
6272 |metric| {
6273 // Only `x = 1` (assignment_expression) → A = 1.
6274 assert_eq!(metric.abc.assignments_sum(), 1);
6275 insta::assert_json_snapshot!(metric.abc);
6276 },
6277 );
6278 }
6279
6280 #[test]
6281 fn cpp_calls_are_branches() {
6282 // Free call + member-fn call (parses as `call_expression` with
6283 // a `field_expression` callee) + `new` allocation. All three
6284 // are branches → B = 3. `auto* p = new int(5)` is also an
6285 // `init_declarator` with `=` so it contributes one assignment
6286 // (post-#393); the snapshot pins that magnitude.
6287 check_metrics::<CppParser>(
6288 "struct S { void m(); }; void g(); void f() { g(); S s; s.m(); auto* p = new int(5); }",
6289 "foo.cpp",
6290 |metric| {
6291 assert_eq!(metric.abc.branches_sum(), 3);
6292 assert_eq!(metric.abc.assignments_sum(), 1);
6293 insta::assert_json_snapshot!(metric.abc);
6294 },
6295 );
6296 }
6297
6298 #[test]
6299 fn cpp_comparisons_count_conditions() {
6300 // `<`, `>`, `<=`, `>=`, `==`, `!=`, and the C++20 spaceship
6301 // `<=>` each contribute one condition. The `||` short-
6302 // circuits add 0 (Fitzpatrick Rule 5, issue #395). Six
6303 // comparisons in the `||` chain plus `<=>` (1) plus the
6304 // outer `== 0` (1) → C = 8.
6305 check_metrics::<CppParser>(
6306 "#include <compare>\n\
6307 bool f(int a, int b) {\n\
6308 return a < b || a > b || a <= b || a >= b || a == b || a != b || (a <=> b) == 0;\n\
6309 }\n",
6310 "foo.cpp",
6311 |metric| {
6312 // `<`, `>`, `<=`, `>=`, `==`, `!=` → 6 comparisons
6313 // from the chained `||` expression. `(a <=> b) == 0`
6314 // adds the spaceship `<=>` (1) + the outer `== 0`
6315 // (1) → 8 total. The six `||` short-circuits add 0
6316 // (Fitzpatrick Rule 5; issue #395).
6317 assert_eq!(metric.abc.conditions_sum(), 8);
6318 insta::assert_json_snapshot!(metric.abc);
6319 },
6320 );
6321 }
6322
6323 #[test]
6324 fn cpp_short_circuit_ops_not_counted_directly() {
6325 // `&&` and `||` do NOT count on their own (see the
6326 // module-level `Stats` doc-comment; #395). Phase-2 walker
6327 // counts each operand of a logical chain once (#403), but
6328 // when every operand is itself a relational expression
6329 // (`a == b`, `a > 0`, `b < 0`) the walker doesn't add
6330 // anything on top of the existing comparison-token tally
6331 // — relational sub-expressions are not in
6332 // `cpp_bool_terminal_kinds!()` and `cpp_inspect_container`
6333 // does not recurse into them.
6334 check_metrics::<CppParser>(
6335 "bool f(int a, int b) { return a == b && a > 0 || b < 0; }",
6336 "foo.cpp",
6337 |metric| {
6338 // == 1, > 1, < 1; the walker on && and || finds
6339 // BinaryExpression operands (not terminal-bool) and
6340 // adds nothing. Total: 3.
6341 assert_eq!(metric.abc.conditions_sum(), 3);
6342 insta::assert_json_snapshot!(metric.abc);
6343 },
6344 );
6345 }
6346
6347 #[test]
6348 fn cpp_generic_brackets_not_conditions() {
6349 // `<` / `>` in `std::vector<int>` are `template_argument_list`
6350 // delimiters, NOT comparison operators. The `binary_expression`
6351 // parent check must filter them out → C = 0.
6352 check_metrics::<CppParser>(
6353 "#include <vector>\nstd::vector<int> f() { return std::vector<int>{}; }",
6354 "foo.cpp",
6355 |metric| {
6356 assert_eq!(metric.abc.conditions_sum(), 0);
6357 insta::assert_json_snapshot!(metric.abc);
6358 },
6359 );
6360 }
6361
6362 #[test]
6363 fn cpp_else_and_ternary_count_conditions() {
6364 // `if (cond) ... else ...` + ternary `cond ? a : b`. The
6365 // `if`-keyword is NOT a condition (its condition is the
6366 // comparison inside, which counts separately). `else` adds 1,
6367 // `?` adds 1. Two comparisons (`a > b`, `b < 0`) → 2. Total = 4.
6368 check_metrics::<CppParser>(
6369 "int f(int a, int b) {\n\
6370 if (a > b) { return a; } else { return b; }\n\
6371 return (b < 0) ? -b : b;\n\
6372 }\n",
6373 "foo.cpp",
6374 |metric| {
6375 assert_eq!(metric.abc.conditions_sum(), 4);
6376 insta::assert_json_snapshot!(metric.abc);
6377 },
6378 );
6379 }
6380
6381 #[test]
6382 fn cpp_switch_cases_count_default_excluded() {
6383 // `case 1`, `case 2` → 2 conditions. `default` is intentionally
6384 // excluded (the unconditional fallthrough, mirroring cyclomatic's
6385 // `Case`-only count). Since #469 every C-family language —
6386 // Java, C#, Groovy, JS, TS — agrees on this; C++ already did.
6387 // C = 2.
6388 check_metrics::<CppParser>(
6389 "void f(int x) {\n\
6390 switch (x) {\n\
6391 case 1: break;\n\
6392 case 2: break;\n\
6393 default: break;\n\
6394 }\n\
6395 }\n",
6396 "foo.cpp",
6397 |metric| {
6398 assert_eq!(metric.abc.conditions_sum(), 2);
6399 insta::assert_json_snapshot!(metric.abc);
6400 },
6401 );
6402 }
6403
6404 #[test]
6405 fn cpp_try_catch_count_conditions() {
6406 // `try` and `catch` each add one condition (Fitzpatrick's rule;
6407 // Java's impl above counts them too).
6408 check_metrics::<CppParser>(
6409 "void f() { try { } catch (int) { } catch (...) { } }",
6410 "foo.cpp",
6411 |metric| {
6412 // 1 `try` + 2 `catch` arms = 3.
6413 assert_eq!(metric.abc.conditions_sum(), 3);
6414 insta::assert_json_snapshot!(metric.abc);
6415 },
6416 );
6417 }
6418
6419 #[test]
6420 fn cpp_complex_function_abc() {
6421 // Mixed-shape regression: assignments, calls, conditions,
6422 // ternary, switch, new. Verified by hand:
6423 // - assignments: `int x = 0` (init_declarator with `=`),
6424 // `x = 5`, `x += 2`, `x++`, `x = (a > b) ? a : b`, `x = b`,
6425 // `auto* p = new int(5)` (init_declarator with `=`) → A = 7
6426 // (post-#393: every `=` in an init_declarator counts).
6427 // - branches: `f(a, b)` self-call + `new int(5)` → B = 2.
6428 // - conditions: `a == b` (1) + `a > 0` (1) inside the if;
6429 // `&&` itself is NOT a condition (Fitzpatrick Rule 5,
6430 // issue #395). `a > b` (1) + `?` (1) in the ternary.
6431 // `else` (1, from the `else if` keyword) + `a < b` (1)
6432 // in the else-if. `!x` contributes 1 via the unary-
6433 // conditional walker (Fitzpatrick Rule 9, issue #403):
6434 // the `||` walker treats `!x` as a unary boolean operand
6435 // and counts the wrapped Identifier once. `case 1`,
6436 // `case 2` → 2. `default` excluded. Total C = 9.
6437 check_metrics::<CppParser>(
6438 "int f(int a, int b) {\n\
6439 int x = 0;\n\
6440 x = 5;\n\
6441 x += 2;\n\
6442 x++;\n\
6443 if (a == b && a > 0) {\n\
6444 x = (a > b) ? a : b;\n\
6445 } else if (a < b || !x) {\n\
6446 x = b;\n\
6447 }\n\
6448 switch (x) {\n\
6449 case 1: break;\n\
6450 case 2: break;\n\
6451 default: break;\n\
6452 }\n\
6453 auto* p = new int(5);\n\
6454 return f(a, b);\n\
6455 }\n",
6456 "foo.cpp",
6457 |metric| {
6458 assert_eq!(metric.abc.assignments_sum(), 7);
6459 assert_eq!(metric.abc.branches_sum(), 2);
6460 assert_eq!(metric.abc.conditions_sum(), 9);
6461 insta::assert_json_snapshot!(metric.abc);
6462 },
6463 );
6464 }
6465
6466 #[test]
6467 fn cpp_if_multiple_conditions() {
6468 // Fitzpatrick Rule 9 walker (issue #403): each operand of a
6469 // `&&` / `||` chain is one condition.
6470 check_metrics::<CppParser>(
6471 "void f(bool a, bool b, bool c, bool d) {\n\
6472 \x20 if (a || b || c || d) {} // +4c\n\
6473 \x20 if (a && b && c) {} // +3c\n\
6474 \x20 if (!a && !b) {} // +2c\n\
6475 }\n",
6476 "foo.cpp",
6477 |metric| {
6478 assert_eq!(metric.abc.conditions_sum(), 9);
6479 insta::assert_json_snapshot!(metric.abc);
6480 },
6481 );
6482 }
6483
6484 #[test]
6485 fn cpp_while_and_do_while_conditions() {
6486 // Exercise both the WhileStatement and DoStatement arms via
6487 // the walker on the `&&` / `||` tokens inside their parens.
6488 check_metrics::<CppParser>(
6489 "void f(bool a, bool b) {\n\
6490 \x20 while (a || b) {} // +2c\n\
6491 \x20 do {} while (a && !b); // +2c\n\
6492 }\n",
6493 "foo.cpp",
6494 |metric| {
6495 assert_eq!(metric.abc.conditions_sum(), 4);
6496 insta::assert_json_snapshot!(metric.abc);
6497 },
6498 );
6499 }
6500
6501 #[test]
6502 fn cpp_if_constexpr_condition_counts() {
6503 // Regression for the code-review finding: C++ `if constexpr
6504 // (cond)` puts the `constexpr` keyword at child(1) and the
6505 // condition_clause at child(2). Pre-fix, the dispatcher used
6506 // child(1) and counted zero conditions for the `constexpr`
6507 // form. The fix uses `child_by_field_name("condition")`
6508 // which returns the condition_clause regardless of the
6509 // optional `constexpr` keyword.
6510 check_metrics::<CppParser>(
6511 "template <int N> void f() {\n\
6512 \x20 if constexpr (true) { } // +1c\n\
6513 \x20 if (false) { } // +1c\n\
6514 }\n",
6515 "foo.cpp",
6516 |metric| {
6517 assert_eq!(metric.abc.conditions_sum(), 2);
6518 insta::assert_json_snapshot!(metric.abc);
6519 },
6520 );
6521 }
6522
6523 #[test]
6524 fn cpp_cast_expression_in_logical_chain_counts() {
6525 // Regression for findings.md round-2 #1 (C++):
6526 // `if ((bool)ptr && ready) {}` had the `||` walker missing
6527 // the `(bool)ptr` operand because `CastExpression` was not
6528 // in `cpp_bool_terminal_kinds!()`. Mirrors C#'s
6529 // `csharp_bool_terminal_kinds!()` which lists
6530 // `CastExpression` (lesson 19, #372).
6531 check_metrics::<CppParser>(
6532 "void f(void* ptr, bool ready) { if ((bool)ptr && ready) { } }\n",
6533 "foo.cpp",
6534 |metric| {
6535 // `&&` walker counts both operands: `(bool)ptr` (1)
6536 // and `ready` (1). Total: 2.
6537 assert_eq!(metric.abc.conditions_sum(), 2);
6538 insta::assert_json_snapshot!(metric.abc);
6539 },
6540 );
6541 }
6542
6543 #[test]
6544 fn cpp_qualified_identifier_condition_counts() {
6545 // Regression for findings.md #3 (C++): tree-sitter-cpp emits
6546 // `qualified_identifier` under four kind_ids (573..576) per
6547 // the production-rule path; runtime kind for `ns::flag` is
6548 // 574 (`QualifiedIdentifier2`). Pre-fix the
6549 // `cpp_bool_terminal_kinds!()` macro listed neither the
6550 // primary nor any alias, so `if (n::flag) {}` reported zero
6551 // conditions. The macro now includes all four variants
6552 // (lesson #2).
6553 check_metrics::<CppParser>(
6554 "namespace n { extern bool flag; }\n\
6555 void f() { if (n::flag) { } }\n",
6556 "foo.cpp",
6557 |metric| {
6558 assert_eq!(metric.abc.conditions_sum(), 1);
6559 insta::assert_json_snapshot!(metric.abc);
6560 },
6561 );
6562 }
6563
6564 #[test]
6565 fn cpp_if_boolean_literal_condition() {
6566 check_metrics::<CppParser>(
6567 "void f() {\n\
6568 \x20 if (true) {} // +1c\n\
6569 \x20 if (!false) {} // +1c\n\
6570 \x20 while (true) {} // +1c\n\
6571 \x20 do {} while (false); // +1c\n\
6572 }\n",
6573 "foo.cpp",
6574 |metric| {
6575 assert_eq!(metric.abc.conditions_sum(), 4);
6576 insta::assert_json_snapshot!(metric.abc);
6577 },
6578 );
6579 }
6580
6581 #[test]
6582 fn cpp_methods_arguments_with_conditions() {
6583 check_metrics::<CppParser>(
6584 "void f(bool a, bool b) {\n\
6585 \x20 m(a, b); // +1b\n\
6586 \x20 m(!a, !b); // +1b +2c\n\
6587 }\n",
6588 "foo.cpp",
6589 |metric| {
6590 assert_eq!(metric.abc.branches_sum(), 2);
6591 assert_eq!(metric.abc.conditions_sum(), 2);
6592 insta::assert_json_snapshot!(metric.abc);
6593 },
6594 );
6595 }
6596
6597 #[test]
6598 fn cpp_return_with_conditions() {
6599 check_metrics::<CppParser>(
6600 "bool m1(int z) { return !(z >= 0); }\n\
6601 bool m2(bool x) { return (((!x))); }\n\
6602 bool m3(bool x, bool y) { return x && y; }\n",
6603 "foo.cpp",
6604 |metric| {
6605 // m1: !(z >= 0) → `>=` (1). `!` wraps a paren'd
6606 // BinaryExpression — inspect_container reaches
6607 // the inner BinaryExpression and stops, no
6608 // walker count. +1.
6609 // m2: (((!x))) → ReturnStatement → inspect_container
6610 // unwraps three parens + one unary → reaches `x`
6611 // in has_boolean_content=true (seeded by the
6612 // unary `!`). +1.
6613 // m3: x && y → `&&` walker counts both → +2.
6614 // Sum: 1 + 1 + 2 = 4.
6615 assert_eq!(metric.abc.conditions_sum(), 4);
6616 insta::assert_json_snapshot!(metric.abc);
6617 },
6618 );
6619 }
6620
6621 #[test]
6622 fn cpp_short_circuit_with_boolean_literal_operand() {
6623 // `a && true` reports 2 conditions: one for the identifier
6624 // operand, one for the `True` literal operand.
6625 check_metrics::<CppParser>(
6626 "bool f(bool a) { return a && true; }\n",
6627 "foo.cpp",
6628 |metric| {
6629 assert_eq!(metric.abc.conditions_sum(), 2);
6630 insta::assert_json_snapshot!(metric.abc);
6631 },
6632 );
6633 }
6634
6635 #[test]
6636 fn javascript_empty_unit_zero() {
6637 // No code → A=B=C=0. Wires up the trait and exercises the
6638 // per-language compute reachability.
6639 check_metrics::<JavascriptParser>("", "empty.js", |metric| {
6640 assert_eq!(metric.abc.assignments_sum(), 0);
6641 assert_eq!(metric.abc.branches_sum(), 0);
6642 assert_eq!(metric.abc.conditions_sum(), 0);
6643 insta::assert_json_snapshot!(metric.abc);
6644 });
6645 }
6646
6647 #[test]
6648 fn javascript_plain_and_compound_assignments_count() {
6649 // `let` / `var` declarations behave like TypeScript: the `Var`
6650 // sentinel is pushed but only `const` suppresses the
6651 // initializer `=`. So `let x = 0` does count as A=+1; only
6652 // `const PI = 3.14` would be elided. Plain `x = 5`, `x += 2`,
6653 // `x = 7` all count → A = 4 total here.
6654 check_metrics::<JavascriptParser>(
6655 "function f() { let x = 0; x = 5; x += 2; x = 7; }",
6656 "foo.js",
6657 |metric| {
6658 assert_eq!(metric.abc.assignments_sum(), 4);
6659 assert_eq!(metric.abc.branches_sum(), 0);
6660 assert_eq!(metric.abc.conditions_sum(), 0);
6661 insta::assert_json_snapshot!(metric.abc);
6662 },
6663 );
6664 }
6665
6666 #[test]
6667 fn javascript_const_initializer_not_assignment() {
6668 // `const PI = 3.14` must NOT count as an assignment — the
6669 // `Const` sentinel suppresses the initializer `=`. `let x = 1`
6670 // and `var y = 2` still count (matches the TS impl: only
6671 // `const` suppresses).
6672 check_metrics::<JavascriptParser>(
6673 "function f() { const PI = 3.14; let x = 1; var y = 2; x = 9; }",
6674 "foo.js",
6675 |metric| {
6676 // `const PI` suppressed; `let x = 1`, `var y = 2`,
6677 // `x = 9` all count → A = 3.
6678 assert_eq!(metric.abc.assignments_sum(), 3);
6679 insta::assert_json_snapshot!(metric.abc);
6680 },
6681 );
6682 }
6683
6684 #[test]
6685 fn javascript_increment_and_decrement_count_as_assignment() {
6686 // `x++` (post) and `--x` (pre) both update an lvalue and so
6687 // count as assignments. Combined with the `let x = 0`
6688 // initializer (which counts under the JS/TS sentinel rule —
6689 // only `const` suppresses), A = 3.
6690 check_metrics::<JavascriptParser>(
6691 "function f() { let x = 0; x++; --x; }",
6692 "foo.js",
6693 |metric| {
6694 assert_eq!(metric.abc.assignments_sum(), 3);
6695 insta::assert_json_snapshot!(metric.abc);
6696 },
6697 );
6698 }
6699
6700 #[test]
6701 fn javascript_calls_are_branches() {
6702 // `g(1)` is a `call_expression` → B = 1. `new Foo(2)` is a
6703 // `new_expression` → B = 1. Total B = 2.
6704 check_metrics::<JavascriptParser>(
6705 "function f() { g(1); new Foo(2); }",
6706 "foo.js",
6707 |metric| {
6708 assert_eq!(metric.abc.branches_sum(), 2);
6709 assert_eq!(metric.abc.conditions_sum(), 0);
6710 insta::assert_json_snapshot!(metric.abc);
6711 },
6712 );
6713 }
6714
6715 #[test]
6716 fn javascript_comparisons_count_conditions() {
6717 // `==`, `===`, `!=`, `!==`, `<`, `>`, `<=`, `>=` each count
6718 // once. The `&&` / `||` short-circuit operators are NOT
6719 // counted as conditions in this impl (matches the TS
6720 // precedent — short-circuit ops are folded into the
6721 // surrounding `if` / control-flow arm, not separately).
6722 // Total C = 8.
6723 check_metrics::<JavascriptParser>(
6724 "function f(a, b) { return a == b && a === b && a != b && a !== b && a < b && a > b && a <= b && a >= b; }",
6725 "foo.js",
6726 |metric| {
6727 assert_eq!(metric.abc.conditions_sum(), 8);
6728 insta::assert_json_snapshot!(metric.abc);
6729 },
6730 );
6731 }
6732
6733 #[test]
6734 fn javascript_number_truthy_condition_counts() {
6735 // Regression for #772: JS treats every non-zero number as
6736 // truthy, so `while (5)` and `x && 5` should each count their
6737 // numeric literal as a Fitzpatrick unary condition. Pre-fix
6738 // `javascript_bool_terminal_kinds!()` listed `True` / `False`
6739 // but omitted `Number`, so the walker dropped every numeric-
6740 // truthy operand (mirrors the Lua `Number` fix).
6741 check_metrics::<JavascriptParser>(
6742 "function f(x) { while (5) {} return x && 5; }",
6743 "foo.js",
6744 |metric| {
6745 // `while (5)` → Number literal (+1). `x && 5` → both
6746 // operands count: identifier `x` (+1), Number `5` (+1).
6747 // Total: 3.
6748 assert_eq!(metric.abc.conditions_sum(), 3);
6749 insta::assert_json_snapshot!(metric.abc);
6750 },
6751 );
6752 }
6753
6754 #[test]
6755 fn typescript_number_truthy_condition_counts() {
6756 // Regression for #772: TS shares the JS truthy semantics. The
6757 // numeric *literal* `5` (kind `Number`) counts; the type-keyword
6758 // `number` (kind `Number2`, the `predefined_type`) must not —
6759 // see `typescript_bool_terminal_kinds!`.
6760 check_metrics::<TypescriptParser>(
6761 "function f(x: number) { while (5) {} return x && 5; }",
6762 "foo.ts",
6763 |metric| {
6764 // `while (5)` → +1; `x && 5` → `x` (+1) + `5` (+1).
6765 // Total: 3. The `: number` annotation contributes 0.
6766 assert_eq!(metric.abc.conditions_sum(), 3);
6767 insta::assert_json_snapshot!(metric.abc);
6768 },
6769 );
6770 }
6771
6772 #[test]
6773 fn javascript_nullish_coalescing_counts_condition() {
6774 // `a ?? b` is one nullish-coalescing operator → C = 1.
6775 check_metrics::<JavascriptParser>(
6776 "function f(a, b) { return a ?? b; }",
6777 "foo.js",
6778 |metric| {
6779 assert_eq!(metric.abc.conditions_sum(), 1);
6780 insta::assert_json_snapshot!(metric.abc);
6781 },
6782 );
6783 }
6784
6785 #[test]
6786 fn javascript_else_ternary_case_default_try_catch() {
6787 // `else`, `?` (ternary), `case`, `try`, `catch` all count.
6788 // `default` is the unconditional fallthrough → +0 (#469).
6789 // With the comparisons:
6790 // - `a > 0` → 1
6791 // - `else` opens an else_clause → 1
6792 // - `?` ternary → 1
6793 // - `case 1` → 1
6794 // - `default` → 0 (fallthrough, #469)
6795 // - `try` + `catch` → 2
6796 // Total C = 6.
6797 check_metrics::<JavascriptParser>(
6798 "function f(a) { if (a > 0) {} else {} let x = a ? 1 : 2; switch (x) { case 1: break; default: break; } try { } catch (e) { } }",
6799 "foo.js",
6800 |metric| {
6801 assert_eq!(metric.abc.conditions_sum(), 6);
6802 insta::assert_json_snapshot!(metric.abc);
6803 },
6804 );
6805 }
6806
6807 #[test]
6808 fn javascript_instanceof_counts_condition() {
6809 // `x instanceof Foo` is a binary expression whose operator is
6810 // the `instanceof` keyword token → C = 1.
6811 check_metrics::<JavascriptParser>(
6812 "function f(x) { return x instanceof Foo; }",
6813 "foo.js",
6814 |metric| {
6815 assert_eq!(metric.abc.conditions_sum(), 1);
6816 insta::assert_json_snapshot!(metric.abc);
6817 },
6818 );
6819 }
6820
6821 #[test]
6822 fn javascript_complex_function_abc() {
6823 // Mixed-shape regression. Verified by hand:
6824 // - assignments: `let x = 0` (Var sentinel does not suppress)
6825 // + `x = 5`, `x += 2`, `x++`, `x = (a>b)?a:b`, `x = b`,
6826 // `let p = ...` (Var sentinel) → A = 7.
6827 // - branches: `f(a, b)` self-call + `new Bar()` → B = 2.
6828 // - conditions: `a == b`, `a > 0` → 2 inside the if header
6829 // (`&&` is not counted directly). `else` (1) + `a > b`,
6830 // `?` → 2 in the ternary. `a < b` → 1 in the else-if.
6831 // `!x` → 1 from the Fitzpatrick Rule 9 walker on `||`
6832 // (issue #403): the wrapped Identifier counts once.
6833 // `case 1` → 1 in the switch; `default` → 0 (fallthrough,
6834 // #469). Total C = 8.
6835 check_metrics::<JavascriptParser>(
6836 "function f(a, b) {\n\
6837 let x = 0;\n\
6838 x = 5;\n\
6839 x += 2;\n\
6840 x++;\n\
6841 if (a == b && a > 0) {\n\
6842 x = (a > b) ? a : b;\n\
6843 } else if (a < b || !x) {\n\
6844 x = b;\n\
6845 }\n\
6846 switch (x) {\n\
6847 case 1: break;\n\
6848 default: break;\n\
6849 }\n\
6850 let p = new Bar();\n\
6851 return f(a, b);\n\
6852 }\n",
6853 "foo.js",
6854 |metric| {
6855 assert_eq!(metric.abc.assignments_sum(), 7);
6856 assert_eq!(metric.abc.branches_sum(), 2);
6857 assert_eq!(metric.abc.conditions_sum(), 8);
6858 insta::assert_json_snapshot!(metric.abc);
6859 },
6860 );
6861 }
6862
6863 #[test]
6864 fn mozjs_complex_function_abc() {
6865 // Mozjs shares JavaScript's expression / statement vocabulary;
6866 // the `js_abc_compute!` macro expands identical token-level
6867 // rules for both. This test pins parity against the JS impl.
6868 check_metrics::<MozjsParser>(
6869 "function f(a, b) {\n\
6870 let x = 0;\n\
6871 x = 5;\n\
6872 x += 2;\n\
6873 x++;\n\
6874 if (a == b && a > 0) {\n\
6875 x = (a > b) ? a : b;\n\
6876 } else if (a < b || !x) {\n\
6877 x = b;\n\
6878 }\n\
6879 switch (x) {\n\
6880 case 1: break;\n\
6881 default: break;\n\
6882 }\n\
6883 let p = new Bar();\n\
6884 return f(a, b);\n\
6885 }\n",
6886 "foo.js",
6887 |metric| {
6888 assert_eq!(metric.abc.assignments_sum(), 7);
6889 assert_eq!(metric.abc.branches_sum(), 2);
6890 assert_eq!(metric.abc.conditions_sum(), 8);
6891 insta::assert_json_snapshot!(metric.abc);
6892 },
6893 );
6894 }
6895
6896 // ----- JS / TS / Tsx / Mozjs Phase-2B condition slots -----
6897
6898 #[test]
6899 fn javascript_await_expression_condition_counts() {
6900 // Regression for findings.md round-2 #2 (JS):
6901 // `if (await ready()) {}` parses with `await_expression` as
6902 // the condition node inside the `parenthesized_expression`.
6903 // `javascript_inspect_container` unwraps the paren but the
6904 // await child was not in the terminal-bool set, so the
6905 // walker broke without counting. Mirrors C# (lesson 19).
6906 check_metrics::<JavascriptParser>(
6907 "async function ready() { return true; }\n\
6908 async function f() { if (await ready()) { } }\n",
6909 "foo.js",
6910 |metric| {
6911 assert_eq!(metric.abc.branches_sum(), 1);
6912 assert_eq!(metric.abc.conditions_sum(), 1);
6913 insta::assert_json_snapshot!(metric.abc);
6914 },
6915 );
6916 }
6917
6918 #[test]
6919 fn javascript_member_expression_condition_counts() {
6920 // Regression for findings.md #3 (JS-family): tree-sitter-
6921 // javascript emits `member_expression` under three kind_ids
6922 // (191 primary, 208, 228 — `MemberExpression2/3`) depending
6923 // on the production rule path. The verifier in this audit
6924 // confirmed runtime kind for `o.x` is 208. Pre-fix the
6925 // shared `js_family_bool_terminal_kinds!()` macro listed
6926 // only the primary, so every `if (o.x) {}` / `o.x && o.y`
6927 // condition silently reported zero. The per-language macro
6928 // now includes all three aliases (lesson #2).
6929 check_metrics::<JavascriptParser>(
6930 "function f(o) {\n\
6931 \x20 if (o.x) {} // +1c\n\
6932 \x20 return o.x && o.y; // +2c (walker on &&)\n\
6933 }\n",
6934 "foo.js",
6935 |metric| {
6936 assert_eq!(metric.abc.conditions_sum(), 3);
6937 insta::assert_json_snapshot!(metric.abc);
6938 },
6939 );
6940 }
6941
6942 #[test]
6943 fn javascript_if_boolean_literal_condition() {
6944 check_metrics::<JavascriptParser>(
6945 "function f() {\n\
6946 \x20 if (true) {} // +1c\n\
6947 \x20 if (!false) {} // +1c\n\
6948 \x20 while (true) {} // +1c\n\
6949 \x20 do {} while (false); // +1c\n\
6950 }\n",
6951 "foo.js",
6952 |metric| {
6953 assert_eq!(metric.abc.conditions_sum(), 4);
6954 insta::assert_json_snapshot!(metric.abc);
6955 },
6956 );
6957 }
6958
6959 #[test]
6960 fn javascript_methods_arguments_with_conditions() {
6961 check_metrics::<JavascriptParser>(
6962 "function f(a, b) {\n\
6963 \x20 m(a, b); // +1b\n\
6964 \x20 m(!a, !b); // +1b +2c\n\
6965 }\n",
6966 "foo.js",
6967 |metric| {
6968 assert_eq!(metric.abc.branches_sum(), 2);
6969 assert_eq!(metric.abc.conditions_sum(), 2);
6970 insta::assert_json_snapshot!(metric.abc);
6971 },
6972 );
6973 }
6974
6975 #[test]
6976 fn javascript_return_with_conditions() {
6977 check_metrics::<JavascriptParser>(
6978 "function m1(z) { return !(z >= 0); }\n\
6979 function m2(x) { return (((!x))); }\n\
6980 function m3(x, y) { return x && y; }\n",
6981 "foo.js",
6982 |metric| {
6983 // m1: 1 (`>=`). m2: 1 (walker unwraps to `x`).
6984 // m3: 2 (`&&` walker counts both terminals).
6985 assert_eq!(metric.abc.conditions_sum(), 4);
6986 insta::assert_json_snapshot!(metric.abc);
6987 },
6988 );
6989 }
6990
6991 #[test]
6992 fn typescript_if_boolean_literal_condition() {
6993 check_metrics::<TypescriptParser>(
6994 "function f() {\n\
6995 \x20 if (true) {}\n\
6996 \x20 if (!false) {}\n\
6997 \x20 while (true) {}\n\
6998 \x20 do {} while (false);\n\
6999 }\n",
7000 "foo.ts",
7001 |metric| {
7002 assert_eq!(metric.abc.conditions_sum(), 4);
7003 insta::assert_json_snapshot!(metric.abc);
7004 },
7005 );
7006 }
7007
7008 #[test]
7009 fn typescript_methods_arguments_with_conditions() {
7010 check_metrics::<TypescriptParser>(
7011 "function f(a: boolean, b: boolean) {\n\
7012 \x20 m(a, b);\n\
7013 \x20 m(!a, !b);\n\
7014 }\n",
7015 "foo.ts",
7016 |metric| {
7017 assert_eq!(metric.abc.branches_sum(), 2);
7018 assert_eq!(metric.abc.conditions_sum(), 2);
7019 insta::assert_json_snapshot!(metric.abc);
7020 },
7021 );
7022 }
7023
7024 #[test]
7025 fn typescript_return_with_conditions() {
7026 check_metrics::<TypescriptParser>(
7027 "function m1(z: number): boolean { return !(z >= 0); }\n\
7028 function m2(x: boolean): boolean { return (((!x))); }\n\
7029 function m3(x: boolean, y: boolean): boolean { return x && y; }\n",
7030 "foo.ts",
7031 |metric| {
7032 assert_eq!(metric.abc.conditions_sum(), 4);
7033 insta::assert_json_snapshot!(metric.abc);
7034 },
7035 );
7036 }
7037
7038 #[test]
7039 fn tsx_if_boolean_literal_condition() {
7040 check_metrics::<TsxParser>(
7041 "function f() {\n\
7042 \x20 if (true) {}\n\
7043 \x20 if (!false) {}\n\
7044 \x20 while (true) {}\n\
7045 \x20 do {} while (false);\n\
7046 }\n",
7047 "foo.tsx",
7048 |metric| {
7049 assert_eq!(metric.abc.conditions_sum(), 4);
7050 insta::assert_json_snapshot!(metric.abc);
7051 },
7052 );
7053 }
7054
7055 #[test]
7056 fn tsx_methods_arguments_with_conditions() {
7057 check_metrics::<TsxParser>(
7058 "function f(a: boolean, b: boolean) {\n\
7059 \x20 m(a, b);\n\
7060 \x20 m(!a, !b);\n\
7061 }\n",
7062 "foo.tsx",
7063 |metric| {
7064 assert_eq!(metric.abc.branches_sum(), 2);
7065 assert_eq!(metric.abc.conditions_sum(), 2);
7066 insta::assert_json_snapshot!(metric.abc);
7067 },
7068 );
7069 }
7070
7071 #[test]
7072 fn tsx_return_with_conditions() {
7073 check_metrics::<TsxParser>(
7074 "function m1(z: number): boolean { return !(z >= 0); }\n\
7075 function m2(x: boolean): boolean { return (((!x))); }\n\
7076 function m3(x: boolean, y: boolean): boolean { return x && y; }\n",
7077 "foo.tsx",
7078 |metric| {
7079 assert_eq!(metric.abc.conditions_sum(), 4);
7080 insta::assert_json_snapshot!(metric.abc);
7081 },
7082 );
7083 }
7084
7085 #[test]
7086 fn mozjs_if_boolean_literal_condition() {
7087 check_metrics::<MozjsParser>(
7088 "function f() {\n\
7089 \x20 if (true) {}\n\
7090 \x20 if (!false) {}\n\
7091 \x20 while (true) {}\n\
7092 \x20 do {} while (false);\n\
7093 }\n",
7094 "foo.js",
7095 |metric| {
7096 assert_eq!(metric.abc.conditions_sum(), 4);
7097 insta::assert_json_snapshot!(metric.abc);
7098 },
7099 );
7100 }
7101
7102 #[test]
7103 fn mozjs_methods_arguments_with_conditions() {
7104 check_metrics::<MozjsParser>(
7105 "function f(a, b) {\n\
7106 \x20 m(a, b);\n\
7107 \x20 m(!a, !b);\n\
7108 }\n",
7109 "foo.js",
7110 |metric| {
7111 assert_eq!(metric.abc.branches_sum(), 2);
7112 assert_eq!(metric.abc.conditions_sum(), 2);
7113 insta::assert_json_snapshot!(metric.abc);
7114 },
7115 );
7116 }
7117
7118 #[test]
7119 fn mozjs_return_with_conditions() {
7120 check_metrics::<MozjsParser>(
7121 "function m1(z) { return !(z >= 0); }\n\
7122 function m2(x) { return (((!x))); }\n\
7123 function m3(x, y) { return x && y; }\n",
7124 "foo.js",
7125 |metric| {
7126 assert_eq!(metric.abc.conditions_sum(), 4);
7127 insta::assert_json_snapshot!(metric.abc);
7128 },
7129 );
7130 }
7131
7132 // ----- JS / TS / Tsx / Mozjs unary-conditional walker -----
7133
7134 #[test]
7135 fn javascript_if_multiple_conditions() {
7136 check_metrics::<JavascriptParser>(
7137 "function f(a, b, c, d) {\n\
7138 \x20 if (a || b || c || d) {} // +4c\n\
7139 \x20 if (a && b && c) {} // +3c\n\
7140 \x20 if (!a && !b) {} // +2c\n\
7141 }\n",
7142 "foo.js",
7143 |metric| {
7144 assert_eq!(metric.abc.conditions_sum(), 9);
7145 insta::assert_json_snapshot!(metric.abc);
7146 },
7147 );
7148 }
7149
7150 #[test]
7151 fn javascript_while_and_do_while_conditions() {
7152 check_metrics::<JavascriptParser>(
7153 "function f(a, b) {\n\
7154 \x20 while (a || b) {} // +2c\n\
7155 \x20 do {} while (a && !b); // +2c\n\
7156 }\n",
7157 "foo.js",
7158 |metric| {
7159 assert_eq!(metric.abc.conditions_sum(), 4);
7160 insta::assert_json_snapshot!(metric.abc);
7161 },
7162 );
7163 }
7164
7165 #[test]
7166 fn javascript_short_circuit_with_boolean_literal_operand() {
7167 check_metrics::<JavascriptParser>(
7168 "function f(a) { return a && true; }\n",
7169 "foo.js",
7170 |metric| {
7171 assert_eq!(metric.abc.conditions_sum(), 2);
7172 insta::assert_json_snapshot!(metric.abc);
7173 },
7174 );
7175 }
7176
7177 #[test]
7178 fn typescript_if_multiple_conditions() {
7179 check_metrics::<TypescriptParser>(
7180 "function f(a: boolean, b: boolean, c: boolean, d: boolean) {\n\
7181 \x20 if (a || b || c || d) {} // +4c\n\
7182 \x20 if (a && b && c) {} // +3c\n\
7183 \x20 if (!a && !b) {} // +2c\n\
7184 }\n",
7185 "foo.ts",
7186 |metric| {
7187 assert_eq!(metric.abc.conditions_sum(), 9);
7188 insta::assert_json_snapshot!(metric.abc);
7189 },
7190 );
7191 }
7192
7193 #[test]
7194 fn typescript_while_and_do_while_conditions() {
7195 check_metrics::<TypescriptParser>(
7196 "function f(a: boolean, b: boolean) {\n\
7197 \x20 while (a || b) {} // +2c\n\
7198 \x20 do {} while (a && !b); // +2c\n\
7199 }\n",
7200 "foo.ts",
7201 |metric| {
7202 assert_eq!(metric.abc.conditions_sum(), 4);
7203 insta::assert_json_snapshot!(metric.abc);
7204 },
7205 );
7206 }
7207
7208 #[test]
7209 fn typescript_short_circuit_with_boolean_literal_operand() {
7210 check_metrics::<TypescriptParser>(
7211 "function f(a: boolean): boolean { return a && true; }\n",
7212 "foo.ts",
7213 |metric| {
7214 assert_eq!(metric.abc.conditions_sum(), 2);
7215 insta::assert_json_snapshot!(metric.abc);
7216 },
7217 );
7218 }
7219
7220 #[test]
7221 fn tsx_if_multiple_conditions() {
7222 check_metrics::<TsxParser>(
7223 "function f(a: boolean, b: boolean, c: boolean, d: boolean) {\n\
7224 \x20 if (a || b || c || d) {} // +4c\n\
7225 \x20 if (a && b && c) {} // +3c\n\
7226 \x20 if (!a && !b) {} // +2c\n\
7227 }\n",
7228 "foo.tsx",
7229 |metric| {
7230 assert_eq!(metric.abc.conditions_sum(), 9);
7231 insta::assert_json_snapshot!(metric.abc);
7232 },
7233 );
7234 }
7235
7236 #[test]
7237 fn tsx_while_and_do_while_conditions() {
7238 check_metrics::<TsxParser>(
7239 "function f(a: boolean, b: boolean) {\n\
7240 \x20 while (a || b) {} // +2c\n\
7241 \x20 do {} while (a && !b); // +2c\n\
7242 }\n",
7243 "foo.tsx",
7244 |metric| {
7245 assert_eq!(metric.abc.conditions_sum(), 4);
7246 insta::assert_json_snapshot!(metric.abc);
7247 },
7248 );
7249 }
7250
7251 #[test]
7252 fn tsx_short_circuit_with_boolean_literal_operand() {
7253 check_metrics::<TsxParser>(
7254 "function f(a: boolean): boolean { return a && true; }\n",
7255 "foo.tsx",
7256 |metric| {
7257 assert_eq!(metric.abc.conditions_sum(), 2);
7258 insta::assert_json_snapshot!(metric.abc);
7259 },
7260 );
7261 }
7262
7263 #[test]
7264 fn mozjs_if_multiple_conditions() {
7265 check_metrics::<MozjsParser>(
7266 "function f(a, b, c, d) {\n\
7267 \x20 if (a || b || c || d) {} // +4c\n\
7268 \x20 if (a && b && c) {} // +3c\n\
7269 \x20 if (!a && !b) {} // +2c\n\
7270 }\n",
7271 "foo.js",
7272 |metric| {
7273 assert_eq!(metric.abc.conditions_sum(), 9);
7274 insta::assert_json_snapshot!(metric.abc);
7275 },
7276 );
7277 }
7278
7279 #[test]
7280 fn mozjs_while_and_do_while_conditions() {
7281 check_metrics::<MozjsParser>(
7282 "function f(a, b) {\n\
7283 \x20 while (a || b) {} // +2c\n\
7284 \x20 do {} while (a && !b); // +2c\n\
7285 }\n",
7286 "foo.js",
7287 |metric| {
7288 assert_eq!(metric.abc.conditions_sum(), 4);
7289 insta::assert_json_snapshot!(metric.abc);
7290 },
7291 );
7292 }
7293
7294 #[test]
7295 fn mozjs_short_circuit_with_boolean_literal_operand() {
7296 check_metrics::<MozjsParser>(
7297 "function f(a) { return a && true; }\n",
7298 "foo.js",
7299 |metric| {
7300 assert_eq!(metric.abc.conditions_sum(), 2);
7301 insta::assert_json_snapshot!(metric.abc);
7302 },
7303 );
7304 }
7305
7306 // ---------- Perl ABC tests ----------
7307
7308 #[test]
7309 fn perl_empty_unit_zero() {
7310 // Empty source produces zero ABC magnitude — pins the trait
7311 // wiring without exercising any compute branch.
7312 check_metrics::<PerlParser>("", "empty.pl", |metric| {
7313 assert_eq!(metric.abc.assignments_sum(), 0);
7314 assert_eq!(metric.abc.branches_sum(), 0);
7315 assert_eq!(metric.abc.conditions_sum(), 0);
7316 insta::assert_json_snapshot!(metric.abc);
7317 });
7318 }
7319
7320 #[test]
7321 fn perl_plain_and_compound_assignments_count() {
7322 // `my $x = 0` parses as a `binary_expression` with an `=`
7323 // token, so the initialiser counts (Perl has no equivalent of
7324 // the JS `const` initialiser-suppression rule). Each
7325 // assignment operator token contributes one assignment:
7326 // `=`, `=`, `+=`, `.=`, `**=` → A = 5. Two of those `=` come
7327 // from the `my $x = 0` initialiser and the later `$x = 5`
7328 // reassignment.
7329 check_metrics::<PerlParser>(
7330 "sub f { my $x = 0; $x = 5; $x += 2; $x .= \"a\"; $x **= 3; }",
7331 "foo.pl",
7332 |metric| {
7333 assert_eq!(metric.abc.assignments_sum(), 5);
7334 assert_eq!(metric.abc.branches_sum(), 0);
7335 assert_eq!(metric.abc.conditions_sum(), 0);
7336 insta::assert_json_snapshot!(metric.abc);
7337 },
7338 );
7339 }
7340
7341 #[test]
7342 fn perl_calls_are_branches() {
7343 // `foo()` parses as `call_expression_with_args_with_brackets`
7344 // wrapping an inner `call_expression_with_bareword(foo)`;
7345 // `bar 1, 2` wraps `bar` likewise under spaced-args; `shift`
7346 // appears as a standalone bareword. The bareword-inside-
7347 // wrapper case must NOT double-count — only the outer wrapper
7348 // contributes a branch. So B = 3 (foo, bar, shift), not 5.
7349 check_metrics::<PerlParser>(
7350 "sub f { foo(); bar 1, 2; my $a = shift; }",
7351 "foo.pl",
7352 |metric| {
7353 // shift's `my $a = shift` initialiser contributes one
7354 // assignment via the `=` token.
7355 assert_eq!(metric.abc.assignments_sum(), 1);
7356 assert_eq!(metric.abc.branches_sum(), 3);
7357 assert_eq!(metric.abc.conditions_sum(), 0);
7358 insta::assert_json_snapshot!(metric.abc);
7359 },
7360 );
7361 }
7362
7363 #[test]
7364 fn perl_method_invocation_counts_as_branch() {
7365 // `$obj->method(...)` parses as `method_invocation`. Any
7366 // arrow-dispatch counts as one branch regardless of how the
7367 // arguments are passed.
7368 check_metrics::<PerlParser>(
7369 "sub f { my $obj = shift; $obj->run($x); $obj->ping; }",
7370 "foo.pl",
7371 |metric| {
7372 // `my $obj = shift` → A=1, B=1 (shift bareword).
7373 // `$obj->run($x)` and `$obj->ping` → 2 more branches.
7374 assert_eq!(metric.abc.assignments_sum(), 1);
7375 assert_eq!(metric.abc.branches_sum(), 3);
7376 assert_eq!(metric.abc.conditions_sum(), 0);
7377 insta::assert_json_snapshot!(metric.abc);
7378 },
7379 );
7380 }
7381
7382 #[test]
7383 fn perl_numeric_and_string_comparisons_count_conditions() {
7384 // Numeric ops `==`, `!=`, `<`, `>`, `<=`, `>=`, `<=>` and
7385 // string ops `eq`, `ne`, `lt`, `gt`, `le`, `ge`, `cmp` each
7386 // fire once per token. The sample below uses one of each →
7387 // C = 14. No assignments, no branches.
7388 check_metrics::<PerlParser>(
7389 "sub f {\n\
7390 my $r;\n\
7391 $r = $a == $b;\n\
7392 $r = $a != $b;\n\
7393 $r = $a < $b;\n\
7394 $r = $a > $b;\n\
7395 $r = $a <= $b;\n\
7396 $r = $a >= $b;\n\
7397 $r = $a <=> $b;\n\
7398 $r = $a eq $b;\n\
7399 $r = $a ne $b;\n\
7400 $r = $a lt $b;\n\
7401 $r = $a gt $b;\n\
7402 $r = $a le $b;\n\
7403 $r = $a ge $b;\n\
7404 $r = $a cmp $b;\n\
7405 }",
7406 "foo.pl",
7407 |metric| {
7408 // 15 `=` tokens: one declaration `my $r` (no `=`),
7409 // then 14 `$r = …` plus there's no `=` in `my $r;`.
7410 // Actually: `my $r;` has no `=`; the 14 `$r = …` are
7411 // 14 `=` tokens. So A=14, C=14.
7412 assert_eq!(metric.abc.assignments_sum(), 14);
7413 assert_eq!(metric.abc.branches_sum(), 0);
7414 assert_eq!(metric.abc.conditions_sum(), 14);
7415 insta::assert_json_snapshot!(metric.abc);
7416 },
7417 );
7418 }
7419
7420 #[test]
7421 fn perl_short_circuit_not_counted_directly_ternary_counts() {
7422 // `&&`, `||`, `//`, low-precedence `and`, `or`, `xor` are
7423 // NOT counted as conditions on their own (Fitzpatrick Rule
7424 // 5; #395) — instead each operand is counted as a unary
7425 // conditional by the walker (Rule 9; #403). At the pinned
7426 // tree-sitter-perl grammar version, only the four
7427 // punctuation forms plus one keyword form parse under a
7428 // `binary_expression` parent that triggers the walker; the
7429 // other two keyword forms parse under a distinct grammar
7430 // node and contribute zero. Net: 4 walker-firing lines × 2
7431 // scalar-variable operands + 1 ternary `?` = 9. The exact
7432 // mix of "which two keyword forms are silent" is grammar-
7433 // version-dependent; a future grammar bump that normalises
7434 // the keyword forms' parent kind will shift this count to
7435 // 13. See follow-up note above the test name.
7436 check_metrics::<PerlParser>(
7437 "sub f {\n\
7438 my $r;\n\
7439 $r = $a && $b;\n\
7440 $r = $a || $b;\n\
7441 $r = $a // $b;\n\
7442 $r = $a and $b;\n\
7443 $r = $a or $b;\n\
7444 $r = $a xor $b;\n\
7445 $r = $a ? 1 : 2;\n\
7446 }",
7447 "foo.pl",
7448 |metric| {
7449 // 7 `=` tokens (one per reassignment line).
7450 assert_eq!(metric.abc.assignments_sum(), 7);
7451 assert_eq!(metric.abc.branches_sum(), 0);
7452 // 4 walker-triggered lines × 2 operands + 1 ternary
7453 // `?` = 9. The two remaining low-precedence keyword
7454 // forms (one of `and`/`or`/`xor`) fall under a
7455 // non-binary_expression parent in this grammar
7456 // version and contribute zero via the walker.
7457 assert_eq!(metric.abc.conditions_sum(), 9);
7458 insta::assert_json_snapshot!(metric.abc);
7459 },
7460 );
7461 }
7462
7463 #[test]
7464 fn perl_elsif_and_else_count_conditions() {
7465 // `if (… == …) { … } elsif (… < …) { … } else { … }` →
7466 // 2 comparison tokens (`==`, `<`), plus `elsif_clause` and
7467 // `else_clause` each + 1 → C = 4. Branches: 0 (only
7468 // assignments). Assignments: just the `=` initialisers /
7469 // reassignments — there are 4 here (`$x` init plus three
7470 // `$x = …` reassigns).
7471 check_metrics::<PerlParser>(
7472 "sub f {\n\
7473 my $x = 0;\n\
7474 if ($a == $b) {\n\
7475 $x = 1;\n\
7476 } elsif ($a < $b) {\n\
7477 $x = 2;\n\
7478 } else {\n\
7479 $x = 3;\n\
7480 }\n\
7481 }",
7482 "foo.pl",
7483 |metric| {
7484 assert_eq!(metric.abc.assignments_sum(), 4);
7485 assert_eq!(metric.abc.branches_sum(), 0);
7486 assert_eq!(metric.abc.conditions_sum(), 4);
7487 insta::assert_json_snapshot!(metric.abc);
7488 },
7489 );
7490 }
7491
7492 #[test]
7493 fn perl_regex_match_operators_count_conditions() {
7494 // `=~` and `!~` are pattern-match operators; we count both
7495 // as conditions because they evaluate the regex match in a
7496 // boolean context.
7497 check_metrics::<PerlParser>(
7498 "sub f { my $s = shift; my $m = $s =~ /foo/; my $n = $s !~ /bar/; }",
7499 "foo.pl",
7500 |metric| {
7501 // 3 `=` tokens, 0 branches except `shift` bareword.
7502 assert_eq!(metric.abc.assignments_sum(), 3);
7503 assert_eq!(metric.abc.branches_sum(), 1);
7504 assert_eq!(metric.abc.conditions_sum(), 2);
7505 insta::assert_json_snapshot!(metric.abc);
7506 },
7507 );
7508 }
7509
7510 #[test]
7511 fn perl_complex_function_abc() {
7512 // Mixed program exercising every category. Computed
7513 // expected:
7514 // Assignments: `my $i = 0` (1), `$i++` is a unary
7515 // increment — Perl's grammar emits `PLUSPLUS` not an `=`
7516 // operator, so it does NOT count under the operator-
7517 // token rule. The for-loop's `$i++` is similarly
7518 // uncounted.
7519 // Total A: 1 from `my $i = 0`, 1 from `$total += $i`
7520 // (the `+=` token) → A = 2.
7521 // Branches: `do_work($i)` → 1; `print "done\n"` is a
7522 // call_expression_with_spaced_args → 1; `return $total`
7523 // uses the `return` keyword not a call → 0. B = 2.
7524 // Conditions: `$i < 10` (`<`) → 1; `$i % 2 == 0` (`==`) →
7525 // 1; `else_clause` → 1. C = 3.
7526 check_metrics::<PerlParser>(
7527 "sub run {\n\
7528 my $total = 0;\n\
7529 for (my $i = 0; $i < 10; $i++) {\n\
7530 if ($i % 2 == 0) {\n\
7531 do_work($i);\n\
7532 } else {\n\
7533 $total += $i;\n\
7534 }\n\
7535 }\n\
7536 print \"done\\n\";\n\
7537 return $total;\n\
7538 }",
7539 "foo.pl",
7540 |metric| {
7541 // `my $total = 0` is one `=`; `my $i = 0` is another
7542 // `=`; `$total += $i` is one `+=`. Total = 3.
7543 assert_eq!(metric.abc.assignments_sum(), 3);
7544 assert_eq!(metric.abc.branches_sum(), 2);
7545 assert_eq!(metric.abc.conditions_sum(), 3);
7546 insta::assert_json_snapshot!(metric.abc);
7547 },
7548 );
7549 }
7550
7551 #[test]
7552 fn perl_if_multiple_conditions() {
7553 // Fitzpatrick Rule 9 walker (issue #403): each operand of a
7554 // `&&` / `||` / `//` / `and` / `or` / `xor` chain is one
7555 // condition. ScalarVariable operands ($a, $b, …) qualify as
7556 // terminal-bool kinds for the walker.
7557 check_metrics::<PerlParser>(
7558 "sub f {\n\
7559 my ($a, $b, $c, $d) = @_;\n\
7560 if ($a || $b || $c || $d) { return 1; } # +4c\n\
7561 if ($a && $b && $c) { return 2; } # +3c\n\
7562 if (!$a && !$b) { return 3; } # +2c\n\
7563 return 0;\n\
7564 }",
7565 "foo.pl",
7566 |metric| {
7567 assert_eq!(metric.abc.conditions_sum(), 9);
7568 insta::assert_json_snapshot!(metric.abc);
7569 },
7570 );
7571 }
7572
7573 #[test]
7574 fn perl_while_and_until_conditions() {
7575 // Perl has no `do { ... } while(cond);` shape in this grammar
7576 // — `while` and `until` are the loop forms with a condition
7577 // slot. The walker fires on each `&&` / `||` token inside
7578 // those headers.
7579 check_metrics::<PerlParser>(
7580 "sub f {\n\
7581 my ($a, $b) = @_;\n\
7582 while ($a || $b) { last; } # +2c\n\
7583 until ($a && !$b) { last; } # +2c\n\
7584 }",
7585 "foo.pl",
7586 |metric| {
7587 assert_eq!(metric.abc.conditions_sum(), 4);
7588 insta::assert_json_snapshot!(metric.abc);
7589 },
7590 );
7591 }
7592
7593 #[test]
7594 fn perl_short_circuit_counts_scalar_variable_operands() {
7595 // `$a && $b` reports 2 conditions — one walker count per
7596 // `ScalarVariable` operand. Renamed from the cross-language
7597 // `_with_boolean_literal_operand` convention because Perl has
7598 // no readily-grammar-exposed boolean literal in an `&&`
7599 // operand slot at the pinned grammar version (the `Boolean`
7600 // kind only fires on the `boolean` pragma's named constants,
7601 // not bareword `1` / `0`). Two scalar variables are the
7602 // grammar-stable terminal-set witness for Perl.
7603 check_metrics::<PerlParser>(
7604 "sub f { my ($a) = @_; return $a && $b; }\n",
7605 "foo.pl",
7606 |metric| {
7607 assert_eq!(metric.abc.conditions_sum(), 2);
7608 insta::assert_json_snapshot!(metric.abc);
7609 },
7610 );
7611 }
7612
7613 #[test]
7614 fn perl_array_in_binary_operand_descends_to_scalar_context_value() {
7615 // Regression test for the code-review findings on the
7616 // Phase-2B Perl walker:
7617 // - Pre-fix-A: `perl_inspect_container` descended `Array`
7618 // via `node.child(1)` — the FIRST element — wrongly
7619 // attributing `$x` for `($x, $y)` (semantically `$y`
7620 // is the scalar-context value).
7621 // - Fix-A (the `array_is_paren` guard, 5db8078): dropped
7622 // Array-as-paren entirely in `BinaryExpression` operand
7623 // contexts to avoid the wrong attribution — but
7624 // regressed `$a || ($x)` (single paren-grouped operand)
7625 // to C=1 instead of 2.
7626 // - Fix-B (this change): keeps Array-as-paren unconditional
7627 // but descends via the LAST named child. `$a || ($x)`
7628 // reaches `$x` (count both operands → 2);
7629 // `$a || ($x, $y)` reaches `$y` (count `$a` + `$y` →
7630 // still 2, matching Fitzpatrick Rule 7 "one per
7631 // operand"); `if ($a)` still reaches `$a` (single-
7632 // element grouping → 1).
7633 check_metrics::<PerlParser>(
7634 "sub f { my ($a, $x, $y) = @_;\n\
7635 \x20 my $r = $a || ($x, $y); # +2c: $a + last-named $y\n\
7636 \x20 my $s = $a || ($x); # +2c: $a + only-named $x\n\
7637 \x20 $r + $s;\n\
7638 }\n",
7639 "foo.pl",
7640 |metric| {
7641 // 2 + 2 = 4 unary conditions from the two `||`s.
7642 assert_eq!(metric.abc.conditions_sum(), 4);
7643 insta::assert_json_snapshot!(metric.abc);
7644 },
7645 );
7646 }
7647
7648 #[test]
7649 fn perl_if_scalar_variable_condition() {
7650 // Renamed from the cross-language
7651 // `_if_boolean_literal_condition` convention because
7652 // Perl has no readily-grammar-exposed boolean literal in
7653 // an `if (cond)` slot at the pinned grammar version:
7654 // tree-sitter-perl's `Boolean` kind only fires for the
7655 // `boolean` pragma's named constants (not bareword `1` /
7656 // `0`, which surface as `Integer` / not in the
7657 // terminal-bool set). A scalar-variable condition is the
7658 // grammar-stable witness — `if ($a)` reaches
7659 // `scalar_variable` via the `Array` paren unwrap.
7660 check_metrics::<PerlParser>(
7661 "sub f { my ($a) = @_; if ($a) { return 1; } }\n",
7662 "foo.pl",
7663 |metric| {
7664 assert_eq!(metric.abc.conditions_sum(), 1);
7665 insta::assert_json_snapshot!(metric.abc);
7666 },
7667 );
7668 }
7669
7670 #[test]
7671 fn perl_methods_arguments_with_conditions() {
7672 // `call(!$a, !$b)` — argument list walker counts each
7673 // unary-conditional argument once. Cannot use `m(...)` as
7674 // the function name — tree-sitter-perl parses `m(...)` as
7675 // the regex-match operator, not a function call.
7676 check_metrics::<PerlParser>(
7677 "sub f { my ($a, $b) = @_; call($a, $b); call(!$a, !$b); }\n",
7678 "foo.pl",
7679 |metric| {
7680 // Two calls × 1 branch each = 2 branches.
7681 // `call(!$a, !$b)` contributes 2 walker conditions
7682 // (one per `!`-wrapped scalar-variable argument);
7683 // `call($a, $b)` contributes 0 (bare-args don't
7684 // count via the Arguments walker — list_kind !=
7685 // BinaryExpression).
7686 assert_eq!(metric.abc.branches_sum(), 2);
7687 assert_eq!(metric.abc.conditions_sum(), 2);
7688 insta::assert_json_snapshot!(metric.abc);
7689 },
7690 );
7691 }
7692
7693 #[test]
7694 fn perl_return_with_conditions() {
7695 // `return !$a` reports 1 condition via the walker (unary
7696 // unwrap to scalar-variable terminal). `return $a` reports
7697 // 0 (no paren / unary wrap, has_boolean_content stays
7698 // false from ReturnExpression parent).
7699 check_metrics::<PerlParser>(
7700 "sub m1 { my ($z) = @_; return !($z); }\n\
7701 sub m2 { my ($x) = @_; return (((!$x))); }\n\
7702 sub m3 { my ($x, $y) = @_; return $x && $y; }\n",
7703 "foo.pl",
7704 |metric| {
7705 // m1: !($z) → walker on `!` unwraps paren to $z (1).
7706 // m2: (((!$x))) → walker unwraps three parens + one
7707 // unary to $x (1).
7708 // m3: $x && $y → walker on `&&` counts both (2).
7709 // Sum: 4.
7710 assert_eq!(metric.abc.conditions_sum(), 4);
7711 insta::assert_json_snapshot!(metric.abc);
7712 },
7713 );
7714 }
7715
7716 // ---------- Lua ABC tests ----------
7717
7718 #[test]
7719 fn lua_empty_unit_zero() {
7720 check_metrics::<LuaParser>("", "empty.lua", |metric| {
7721 assert_eq!(metric.abc.assignments_sum(), 0);
7722 assert_eq!(metric.abc.branches_sum(), 0);
7723 assert_eq!(metric.abc.conditions_sum(), 0);
7724 insta::assert_json_snapshot!(metric.abc);
7725 });
7726 }
7727
7728 #[test]
7729 fn lua_assignments_count_locals_and_plain() {
7730 // `local x = 0` wraps an `assignment_statement` under a
7731 // `variable_declaration`; the inner wrapper still counts.
7732 // Multi-target assignment `a, b = 1, 2` is a single
7733 // `assignment_statement` and contributes 1, NOT 2 — the
7734 // wrapper is the unit of counting (matches the Python rule:
7735 // one `Assignment` node, one assignment).
7736 check_metrics::<LuaParser>(
7737 "function f()\n\
7738 local x = 0\n\
7739 x = 1\n\
7740 local a, b = 1, 2\n\
7741 a, b = b, a\n\
7742 end",
7743 "foo.lua",
7744 |metric| {
7745 assert_eq!(metric.abc.assignments_sum(), 4);
7746 assert_eq!(metric.abc.branches_sum(), 0);
7747 assert_eq!(metric.abc.conditions_sum(), 0);
7748 insta::assert_json_snapshot!(metric.abc);
7749 },
7750 );
7751 }
7752
7753 #[test]
7754 fn lua_calls_are_branches() {
7755 // `print(x)`, `obj.m(x)`, `obj:m(x)`, `f(g(1))` — every
7756 // call form is a `function_call` node. The nested
7757 // `f(g(1))` counts as 2 branches (one per dispatch).
7758 check_metrics::<LuaParser>(
7759 "function r(x)\n\
7760 print(x)\n\
7761 obj.m(x)\n\
7762 obj:m(x)\n\
7763 return f(g(1))\n\
7764 end",
7765 "foo.lua",
7766 |metric| {
7767 assert_eq!(metric.abc.assignments_sum(), 0);
7768 assert_eq!(metric.abc.branches_sum(), 5);
7769 assert_eq!(metric.abc.conditions_sum(), 0);
7770 insta::assert_json_snapshot!(metric.abc);
7771 },
7772 );
7773 }
7774
7775 #[test]
7776 fn lua_comparisons_count_logical_ops_do_not() {
7777 // Each comparison token contributes one condition; `and` /
7778 // `or` are NOT counted on their own (Fitzpatrick Rule 5;
7779 // #395) — instead each operand is counted as a unary
7780 // conditional by the walker (Rule 9; #403). The two
7781 // `a and b` / `a or b` lines add 2 walker conditions each.
7782 check_metrics::<LuaParser>(
7783 "function f(a, b)\n\
7784 local r\n\
7785 r = a == b\n\
7786 r = a ~= b\n\
7787 r = a < b\n\
7788 r = a > b\n\
7789 r = a <= b\n\
7790 r = a >= b\n\
7791 r = a and b\n\
7792 r = a or b\n\
7793 end",
7794 "foo.lua",
7795 |metric| {
7796 // 8 `r = …` reassignments, plus `local r` (no `=`).
7797 assert_eq!(metric.abc.assignments_sum(), 8);
7798 assert_eq!(metric.abc.branches_sum(), 0);
7799 // 6 comparisons (+6) + 2 logical lines × 2 walker
7800 // operands (+4) = 10.
7801 assert_eq!(metric.abc.conditions_sum(), 10);
7802 insta::assert_json_snapshot!(metric.abc);
7803 },
7804 );
7805 }
7806
7807 #[test]
7808 fn lua_elseif_and_else_count_conditions() {
7809 // Each elseif / else arm of the if contributes one
7810 // condition, mirroring the Python rule.
7811 check_metrics::<LuaParser>(
7812 "function f(x)\n\
7813 if x > 0 then\n\
7814 return 1\n\
7815 elseif x < 0 then\n\
7816 return -1\n\
7817 else\n\
7818 return 0\n\
7819 end\n\
7820 end",
7821 "foo.lua",
7822 |metric| {
7823 // Comparisons: `>`, `<` → 2; elseif_statement → 1;
7824 // else_statement → 1. C = 4. No branches (no calls).
7825 assert_eq!(metric.abc.assignments_sum(), 0);
7826 assert_eq!(metric.abc.branches_sum(), 0);
7827 assert_eq!(metric.abc.conditions_sum(), 4);
7828 insta::assert_json_snapshot!(metric.abc);
7829 },
7830 );
7831 }
7832
7833 #[test]
7834 fn lua_complex_function_abc() {
7835 // Combines every category to pin the metric.
7836 check_metrics::<LuaParser>(
7837 "function run(n)\n\
7838 local total = 0\n\
7839 for i = 1, n do\n\
7840 if i % 2 == 0 then\n\
7841 do_work(i)\n\
7842 else\n\
7843 total = total + i\n\
7844 end\n\
7845 end\n\
7846 print(\"done\")\n\
7847 return total\n\
7848 end",
7849 "foo.lua",
7850 |metric| {
7851 // Assignments: `local total = 0` (1), `total = total + i` (1) → 2.
7852 // Branches: `do_work(i)` (1), `print(\"done\")` (1) → 2.
7853 // Conditions: `==` (1), `else_statement` (1) → 2.
7854 assert_eq!(metric.abc.assignments_sum(), 2);
7855 assert_eq!(metric.abc.branches_sum(), 2);
7856 assert_eq!(metric.abc.conditions_sum(), 2);
7857 insta::assert_json_snapshot!(metric.abc);
7858 },
7859 );
7860 }
7861
7862 #[test]
7863 fn lua_if_multiple_conditions() {
7864 // Fitzpatrick Rule 9 walker (issue #403). Lua's `and` / `or`
7865 // are keyword tokens inside a `binary_expression`.
7866 check_metrics::<LuaParser>(
7867 "function f(a, b, c, d)\n\
7868 if a or b or c or d then return 1 end -- +4c\n\
7869 if a and b and c then return 2 end -- +3c\n\
7870 if not a and not b then return 3 end -- +2c\n\
7871 return 0\n\
7872 end",
7873 "foo.lua",
7874 |metric| {
7875 assert_eq!(metric.abc.conditions_sum(), 9);
7876 insta::assert_json_snapshot!(metric.abc);
7877 },
7878 );
7879 }
7880
7881 #[test]
7882 fn lua_while_conditions() {
7883 // Lua has no `do { ... } while(cond);` — `while cond do …
7884 // end` and `repeat … until cond` are the loop forms.
7885 check_metrics::<LuaParser>(
7886 "function f(a, b)\n\
7887 while a or b do break end -- +2c\n\
7888 repeat break until a and not b -- +2c\n\
7889 end",
7890 "foo.lua",
7891 |metric| {
7892 assert_eq!(metric.abc.conditions_sum(), 4);
7893 insta::assert_json_snapshot!(metric.abc);
7894 },
7895 );
7896 }
7897
7898 #[test]
7899 fn lua_short_circuit_with_boolean_literal_operand() {
7900 // `a and true` reports 2 conditions: one Identifier, one
7901 // True keyword literal.
7902 check_metrics::<LuaParser>("function f(a) return a and true end", "foo.lua", |metric| {
7903 assert_eq!(metric.abc.conditions_sum(), 2);
7904 insta::assert_json_snapshot!(metric.abc);
7905 });
7906 }
7907
7908 #[test]
7909 fn lua_number_truthy_condition_counts() {
7910 // Regression for findings.md #2: Lua treats every non-nil,
7911 // non-false value as truthy, so `if 1 then ... end` and
7912 // `return a and 2` should each count their numeric literal
7913 // as a Fitzpatrick Rule 6 / 7 unary condition. Pre-fix,
7914 // `lua_bool_terminal_kinds!()` listed `True` / `False` /
7915 // `Nil` but omitted `Number`, so the walker dropped every
7916 // numeric-truthy operand. The walker comment at the top of
7917 // `lua_inspect_container` already promised numbers were
7918 // terminal-bool kinds; this commit closes the gap.
7919 check_metrics::<LuaParser>(
7920 "function f(a)\n\
7921 \x20 if 1 then return 1 end\n\
7922 \x20 return a and 2\n\
7923 end",
7924 "foo.lua",
7925 |metric| {
7926 // `if 1 then` → walker counts the Number literal (+1).
7927 // `a and 2` → `and` walker counts both operands:
7928 // identifier `a` (+1), Number `2` (+1).
7929 // Total: 3.
7930 assert_eq!(metric.abc.conditions_sum(), 3);
7931 insta::assert_json_snapshot!(metric.abc);
7932 },
7933 );
7934 }
7935
7936 #[test]
7937 fn lua_if_boolean_literal_condition() {
7938 check_metrics::<LuaParser>(
7939 "function f()\n\
7940 if true then end -- +1c\n\
7941 if not false then end -- +1c\n\
7942 while true do break end -- +1c\n\
7943 repeat break until false -- +1c\n\
7944 end",
7945 "foo.lua",
7946 |metric| {
7947 assert_eq!(metric.abc.conditions_sum(), 4);
7948 insta::assert_json_snapshot!(metric.abc);
7949 },
7950 );
7951 }
7952
7953 #[test]
7954 fn lua_methods_arguments_with_conditions() {
7955 // `m(not a, not b)` — argument list walker counts each
7956 // unary-conditional argument once. Bare-identifier args
7957 // (`m(a, b)`) do not count (list_kind != BinaryExpression).
7958 check_metrics::<LuaParser>(
7959 "function f(a, b) m(a, b); m(not a, not b) end",
7960 "foo.lua",
7961 |metric| {
7962 assert_eq!(metric.abc.branches_sum(), 2);
7963 assert_eq!(metric.abc.conditions_sum(), 2);
7964 insta::assert_json_snapshot!(metric.abc);
7965 },
7966 );
7967 }
7968
7969 #[test]
7970 fn lua_return_with_conditions() {
7971 // `return not (z >= 0)` → walker on `not` unwraps the paren
7972 // chain and reaches the inner BinaryExpression; the inner
7973 // `>=` comparison is the actual Fitzpatrick condition.
7974 check_metrics::<LuaParser>(
7975 "function m1(z) return not (z >= 0) end\n\
7976 function m2(x) return (((not x))) end\n\
7977 function m3(x, y) return x and y end",
7978 "foo.lua",
7979 |metric| {
7980 // m1: `>=` (1). `not` wraps a paren'd
7981 // BinaryExpression — Lua's lua_inspect_container
7982 // reaches the inner BinaryExpression and stops,
7983 // no walker count. +1.
7984 // m2: ReturnStatement → iterate expression_list →
7985 // inspect_container on the outermost paren →
7986 // unwraps to `x` in has_boolean_content-true
7987 // (seeded by the `not`). +1.
7988 // m3: x and y → `and` walker counts both → +2.
7989 // Sum: 4.
7990 assert_eq!(metric.abc.conditions_sum(), 4);
7991 insta::assert_json_snapshot!(metric.abc);
7992 },
7993 );
7994 }
7995
7996 // ---------- Tcl ABC tests ----------
7997
7998 #[test]
7999 fn tcl_empty_unit_zero() {
8000 check_metrics::<TclParser>("", "empty.tcl", |metric| {
8001 assert_eq!(metric.abc.assignments_sum(), 0);
8002 assert_eq!(metric.abc.branches_sum(), 0);
8003 assert_eq!(metric.abc.conditions_sum(), 0);
8004 insta::assert_json_snapshot!(metric.abc);
8005 });
8006 }
8007
8008 #[test]
8009 fn tcl_set_command_counts_assignment() {
8010 // `set` has its own grammar production; each invocation is
8011 // one assignment.
8012 check_metrics::<TclParser>(
8013 "proc f {} {\n\
8014 set x 1\n\
8015 set y 2\n\
8016 set x [expr {$x + $y}]\n\
8017 }",
8018 "foo.tcl",
8019 |metric| {
8020 // 3 `set` invocations → A=3. The inner `expr` is a
8021 // sub-command (`command_substitution` + `expr_cmd`),
8022 // not a `command` node, so it doesn't add a branch.
8023 assert_eq!(metric.abc.assignments_sum(), 3);
8024 assert_eq!(metric.abc.branches_sum(), 0);
8025 assert_eq!(metric.abc.conditions_sum(), 0);
8026 insta::assert_json_snapshot!(metric.abc);
8027 },
8028 );
8029 }
8030
8031 #[test]
8032 fn tcl_incr_append_lappend_count_assignment() {
8033 // Variable-mutation commands (`incr`, `append`, `lappend`)
8034 // are recognised by name and count as assignments, not
8035 // branches.
8036 check_metrics::<TclParser>(
8037 "proc f {} {\n\
8038 set x 0\n\
8039 incr x\n\
8040 append s \"hi\"\n\
8041 lappend lst 1\n\
8042 }",
8043 "foo.tcl",
8044 |metric| {
8045 // `set` (1) + `incr` (1) + `append` (1) + `lappend`
8046 // (1) → A=4. No branches, no conditions.
8047 assert_eq!(metric.abc.assignments_sum(), 4);
8048 assert_eq!(metric.abc.branches_sum(), 0);
8049 assert_eq!(metric.abc.conditions_sum(), 0);
8050 insta::assert_json_snapshot!(metric.abc);
8051 },
8052 );
8053 }
8054
8055 #[test]
8056 fn tcl_generic_commands_are_branches() {
8057 // Anything that isn't `set` or a known mutator command
8058 // counts as a branch — including builtins like `puts` and
8059 // `return`.
8060 check_metrics::<TclParser>(
8061 "proc f {} {\n\
8062 puts \"hello\"\n\
8063 do_work 1 2\n\
8064 return 0\n\
8065 }",
8066 "foo.tcl",
8067 |metric| {
8068 // 3 commands, all branches.
8069 assert_eq!(metric.abc.assignments_sum(), 0);
8070 assert_eq!(metric.abc.branches_sum(), 3);
8071 assert_eq!(metric.abc.conditions_sum(), 0);
8072 insta::assert_json_snapshot!(metric.abc);
8073 },
8074 );
8075 }
8076
8077 #[test]
8078 fn tcl_comparisons_count_logical_ops_do_not() {
8079 // `expr` predicates expose comparison / logical tokens at
8080 // the leaf level. Each comparison token contributes one
8081 // condition; `&&` and `||` are NOT counted on their own
8082 // (Fitzpatrick Rule 5; #395) — instead each operand is
8083 // counted as a unary conditional by the walker (Rule 9;
8084 // #403). The two logical lines add 2 walker conditions
8085 // each (variable-substitution operands).
8086 check_metrics::<TclParser>(
8087 "proc f {a b} {\n\
8088 set r [expr {$a == $b}]\n\
8089 set r [expr {$a != $b}]\n\
8090 set r [expr {$a < $b}]\n\
8091 set r [expr {$a > $b}]\n\
8092 set r [expr {$a <= $b}]\n\
8093 set r [expr {$a >= $b}]\n\
8094 set r [expr {$a eq $b}]\n\
8095 set r [expr {$a ne $b}]\n\
8096 set r [expr {$a && $b}]\n\
8097 set r [expr {$a || $b}]\n\
8098 }",
8099 "foo.tcl",
8100 |metric| {
8101 // 10 `set` assignments.
8102 assert_eq!(metric.abc.assignments_sum(), 10);
8103 assert_eq!(metric.abc.branches_sum(), 0);
8104 // 8 comparisons (+8) + 2 logical lines × 2 walker
8105 // operands (+4) = 12.
8106 assert_eq!(metric.abc.conditions_sum(), 12);
8107 insta::assert_json_snapshot!(metric.abc);
8108 },
8109 );
8110 }
8111
8112 #[test]
8113 fn tcl_ternary_counts_condition() {
8114 // `$a ? $b : $c` inside an `expr` is one `ternary_expr`
8115 // node → 1 condition.
8116 check_metrics::<TclParser>(
8117 "proc f {a b c} {\n\
8118 set r [expr {$a ? $b : $c}]\n\
8119 }",
8120 "foo.tcl",
8121 |metric| {
8122 assert_eq!(metric.abc.assignments_sum(), 1);
8123 assert_eq!(metric.abc.branches_sum(), 0);
8124 assert_eq!(metric.abc.conditions_sum(), 1);
8125 insta::assert_json_snapshot!(metric.abc);
8126 },
8127 );
8128 }
8129
8130 #[test]
8131 fn tcl_elseif_and_else_count_conditions() {
8132 // `if` / `elseif` / `else` clause productions each
8133 // contribute one condition. The leaf comparison inside the
8134 // predicate is counted independently.
8135 check_metrics::<TclParser>(
8136 "proc f {x} {\n\
8137 if {$x > 0} {\n\
8138 return 1\n\
8139 } elseif {$x < 0} {\n\
8140 return -1\n\
8141 } else {\n\
8142 return 0\n\
8143 }\n\
8144 }",
8145 "foo.tcl",
8146 |metric| {
8147 // Branches: three `return` commands → 3.
8148 // Conditions: `>` (1), `<` (1), `elseif` (1), `else`
8149 // (1) → 4.
8150 assert_eq!(metric.abc.assignments_sum(), 0);
8151 assert_eq!(metric.abc.branches_sum(), 3);
8152 assert_eq!(metric.abc.conditions_sum(), 4);
8153 insta::assert_json_snapshot!(metric.abc);
8154 },
8155 );
8156 }
8157
8158 #[test]
8159 fn tcl_if_multiple_conditions() {
8160 // Fitzpatrick Rule 9 walker (issue #403). Tcl's `expr` slot
8161 // exposes `&&` / `||` operands as variable substitutions
8162 // (`$a`, `$b`, …) inside a `binop_expr`.
8163 check_metrics::<TclParser>(
8164 "proc f {a b c d} {\n\
8165 if {[expr {$a || $b || $c || $d}]} { return 1 } \n\
8166 if {[expr {$a && $b && $c}]} { return 2 } \n\
8167 return 0\n\
8168 }",
8169 "foo.tcl",
8170 |metric| {
8171 // The two `expr` predicates feed the walker: 4 + 3 = 7.
8172 assert_eq!(metric.abc.conditions_sum(), 7);
8173 insta::assert_json_snapshot!(metric.abc);
8174 },
8175 );
8176 }
8177
8178 #[test]
8179 fn tcl_while_conditions() {
8180 // Tcl has no `do { ... } while(cond);` — `while {…} {…}` is
8181 // the standard loop. The walker fires on `&&` / `||` tokens
8182 // inside the `expr` predicate.
8183 check_metrics::<TclParser>(
8184 "proc f {a b} {\n\
8185 while {[expr {$a || $b}]} { break } \n\
8186 while {[expr {$a && $b}]} { break } \n\
8187 }",
8188 "foo.tcl",
8189 |metric| {
8190 assert_eq!(metric.abc.conditions_sum(), 4);
8191 insta::assert_json_snapshot!(metric.abc);
8192 },
8193 );
8194 }
8195
8196 #[test]
8197 fn tcl_short_circuit_with_boolean_literal_operand() {
8198 // `$a && 1` reports 2 conditions: a VariableSubstitution
8199 // operand plus a Number-literal operand. Confirms `Number`
8200 // is in the walker terminal set. `true` / `false` Tcl
8201 // keywords are not literal tokens in tree-sitter-tcl —
8202 // they're emitted as the operator-context word, which is
8203 // captured separately by the `Tcl::Boolean` kind for
8204 // dedicated `expr {true}` predicates but not as a `&&`
8205 // operand at this iteration; using a numeric literal keeps
8206 // the assertion grammar-stable.
8207 check_metrics::<TclParser>(
8208 "proc f {a} { return [expr {$a && 1}] }\n",
8209 "foo.tcl",
8210 |metric| {
8211 assert_eq!(metric.abc.conditions_sum(), 2);
8212 insta::assert_json_snapshot!(metric.abc);
8213 },
8214 );
8215 }
8216
8217 #[test]
8218 fn tcl_complex_function_abc() {
8219 // Mixed program covering every category. Tcl's grammar
8220 // re-parses braced content that looks command-shaped as a
8221 // nested `command` node, which inflates the branch count
8222 // relative to a naive read of the source — see breakdown.
8223 check_metrics::<TclParser>(
8224 "proc run {n} {\n\
8225 set total 0\n\
8226 for {set i 0} {$i < $n} {incr i} {\n\
8227 if {$i % 2 == 0} {\n\
8228 do_work $i\n\
8229 } else {\n\
8230 incr total $i\n\
8231 }\n\
8232 }\n\
8233 puts \"done\"\n\
8234 return $total\n\
8235 }",
8236 "foo.tcl",
8237 |metric| {
8238 // Assignments: `set total 0` (1), `set i 0` (1),
8239 // `incr i` (1), `incr total $i` (1) → A = 4.
8240 // Branches: the outer `for …` is one `command` node;
8241 // the `{$i < $n}` predicate ALSO re-parses as a
8242 // `command` node (tree-sitter-tcl treats braced
8243 // predicates as nested commands at the pinned
8244 // grammar version); plus `do_work $i`, `puts
8245 // "done"`, and `return $total`. The for-loop body's
8246 // `incr` and `incr total $i` are assignment commands
8247 // and don't add branches. Total B = 5.
8248 // Conditions: `==` (1) and `else` (1) → C = 2. The
8249 // `<` inside `{$i < $n}` is NOT `Tcl::LT`: because
8250 // that predicate re-parses as a `command`, the `<`
8251 // is emitted as `simple_word`. Only `<` inside a
8252 // real `expr` production becomes `Tcl::LT`.
8253 assert_eq!(metric.abc.assignments_sum(), 4);
8254 assert_eq!(metric.abc.branches_sum(), 5);
8255 assert_eq!(metric.abc.conditions_sum(), 2);
8256 insta::assert_json_snapshot!(metric.abc);
8257 },
8258 );
8259 }
8260
8261 /// The dedicated `set name value` production counts as one assignment.
8262 #[test]
8263 fn irules_abc_set_assignment() {
8264 check_metrics::<IrulesParser>("when X {\n set x 1\n}\n", "foo.irule", |metric| {
8265 assert_eq!(metric.abc.assignments_sum(), 1);
8266 assert_eq!(metric.abc.branches_sum(), 0);
8267 assert_eq!(metric.abc.conditions_sum(), 0);
8268 });
8269 }
8270
8271 /// Mutator commands (`incr` / `append` / `lappend`) count as
8272 /// assignments, not branches — iRules has no assignment operators, so
8273 /// mutation is always a command invocation.
8274 #[test]
8275 fn irules_abc_mutator_commands() {
8276 check_metrics::<IrulesParser>(
8277 "when X {\n incr x\n append s \"y\"\n lappend l 1\n}\n",
8278 "foo.irule",
8279 |metric| {
8280 assert_eq!(metric.abc.assignments_sum(), 3);
8281 assert_eq!(metric.abc.branches_sum(), 0);
8282 },
8283 );
8284 }
8285
8286 /// Generic (non-mutator) commands count as branches.
8287 #[test]
8288 fn irules_abc_branch_commands() {
8289 check_metrics::<IrulesParser>(
8290 "when X {\n log local0. hi\n pool p1\n}\n",
8291 "foo.irule",
8292 |metric| {
8293 assert_eq!(metric.abc.assignments_sum(), 0);
8294 assert_eq!(metric.abc.branches_sum(), 2);
8295 assert_eq!(metric.abc.conditions_sum(), 0);
8296 },
8297 );
8298 }
8299
8300 /// A numeric comparison (`==`) is one condition; the `log` inside the
8301 /// `if` body is one branch.
8302 #[test]
8303 fn irules_abc_comparison_condition() {
8304 check_metrics::<IrulesParser>(
8305 "when X {\n if { $a == 1 } { log local0. hi }\n}\n",
8306 "foo.irule",
8307 |metric| {
8308 assert_eq!(metric.abc.branches_sum(), 1);
8309 assert_eq!(metric.abc.conditions_sum(), 1);
8310 },
8311 );
8312 }
8313
8314 /// A word-form string comparator (`contains`) is a condition just like
8315 /// `==` — iRules-specific (Tcl has only `eq`/`ne`/`in`/`ni`). If
8316 /// `contains` were dropped from the condition set this would report 0.
8317 #[test]
8318 fn irules_abc_string_op_condition() {
8319 check_metrics::<IrulesParser>(
8320 "when X {\n if { $a contains \"x\" } { log local0. hi }\n}\n",
8321 "foo.irule",
8322 |metric| {
8323 assert_eq!(metric.abc.branches_sum(), 1);
8324 assert_eq!(metric.abc.conditions_sum(), 1);
8325 },
8326 );
8327 }
8328
8329 /// Each `elseif` / `else` clause is one condition; the three `set`s are
8330 /// assignments. The leading `if` is not itself a condition.
8331 #[test]
8332 fn irules_abc_elseif_else_conditions() {
8333 check_metrics::<IrulesParser>(
8334 "when X {\n if { $a } { set r 1 } elseif { $b } { set r 2 } else { set r 3 }\n}\n",
8335 "foo.irule",
8336 |metric| {
8337 assert_eq!(metric.abc.assignments_sum(), 3);
8338 assert_eq!(metric.abc.conditions_sum(), 2);
8339 },
8340 );
8341 }
8342
8343 /// A ternary contributes its own condition plus the `>` comparison in
8344 /// its test: conditions 2; the `set` is one assignment.
8345 #[test]
8346 fn irules_abc_ternary_condition() {
8347 check_metrics::<IrulesParser>(
8348 "when X {\n set y [expr { $a > 0 ? 1 : 0 }]\n}\n",
8349 "foo.irule",
8350 |metric| {
8351 assert_eq!(metric.abc.assignments_sum(), 1);
8352 assert_eq!(metric.abc.conditions_sum(), 2);
8353 },
8354 );
8355 }
8356
8357 /// Fitzpatrick Rule 9: the short-circuit `&&` is not itself a condition,
8358 /// but each negated bare operand (`!$a`, `!$b`) in the chain is. Guards
8359 /// the `irules_count_unary_conditions` / `irules_inspect_container`
8360 /// walker — conditions 2.
8361 #[test]
8362 fn irules_abc_negated_operands_in_chain() {
8363 check_metrics::<IrulesParser>(
8364 "when X {\n if { !$a && !$b } { log local0. hi }\n}\n",
8365 "foo.irule",
8366 |metric| {
8367 assert_eq!(metric.abc.branches_sum(), 1);
8368 assert_eq!(metric.abc.conditions_sum(), 2);
8369 },
8370 );
8371 }
8372
8373 /// A bare-truthy `if {$a}` condition carries no comparison, ternary,
8374 /// or short-circuit operator, so the unary-conditional walker is
8375 /// never invoked and conditions stay 0 — iRules does not wire the
8376 /// broader Phase 2B slot routing that counts bare-truthy operands.
8377 /// The `log` command is the single branch. Pins the metrics-book
8378 /// claim in the ABC per-language deviation table.
8379 #[test]
8380 fn irules_abc_bare_truthy_reports_zero() {
8381 check_metrics::<IrulesParser>(
8382 "when X {\n if { $a } { log local0. hi }\n}\n",
8383 "foo.irule",
8384 |metric| {
8385 assert_eq!(metric.abc.branches_sum(), 1);
8386 assert_eq!(metric.abc.conditions_sum(), 0);
8387 },
8388 );
8389 }
8390}