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