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/// # Cross-language empty-`for`-condition policy
114///
115/// `for (;;)` — and every other spelling that omits the test slot
116/// (`for (init; ; update)`, Go's bare `for {}`) — counts **zero**
117/// conditions. Nothing in Fitzpatrick's condition rules attributes a
118/// count to a `for` keyword: they count conditional operators and
119/// unary conditions that are *present*, and an omitted test is not a
120/// decision. Most languages get this for free: `*_walk_for_statement`
121/// asks `for_statement` for its `condition` field and an empty header
122/// has none. Two do not, and neither needs a special case either —
123/// the JS family fills the slot with an `empty_statement`, which is
124/// not a boolean terminal and not a paren / `!` wrapper, so it falls
125/// through; and Go, whose `for_statement` exposes no `condition` field
126/// at all, locates the header slot structurally and finds only the
127/// body. Before #1276 Java and Groovy alone disagreed, counting the
128/// `;` or `)` that landed in a positional child slot as a
129/// vacuously-true condition.
130///
131/// See issue #395 for the Phase-1 cross-language policy
132/// alignment, #403 for the Phase-2 unary-conditional walker
133/// fan-out, #404 for the Phase-3 book documentation, #557
134/// for the Kotlin / Ruby / Elixir walker wiring, and #1276 for the
135/// `for`-header condition slot.
136#[derive(Debug, Clone, PartialEq)]
137#[non_exhaustive]
138pub struct Stats {
139 pub(super) assignments: f64,
140 assignments_sum: f64,
141 assignments_min: f64,
142 assignments_max: f64,
143 pub(super) branches: f64,
144 branches_sum: f64,
145 branches_min: f64,
146 branches_max: f64,
147 pub(super) conditions: f64,
148 conditions_sum: f64,
149 conditions_min: f64,
150 conditions_max: f64,
151 space_count: usize,
152}
153
154impl Default for Stats {
155 fn default() -> Self {
156 Self {
157 assignments: 0.,
158 assignments_sum: 0.,
159 assignments_min: f64::MAX,
160 assignments_max: 0.,
161 branches: 0.,
162 branches_sum: 0.,
163 branches_min: f64::MAX,
164 branches_max: 0.,
165 conditions: 0.,
166 conditions_sum: 0.,
167 conditions_min: f64::MAX,
168 conditions_max: 0.,
169 space_count: 1,
170 }
171 }
172}
173
174impl fmt::Display for Stats {
175 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
176 write!(
177 f,
178 "assignments: {}, branches: {}, conditions: {}, magnitude: {}, \
179 assignments_average: {}, branches_average: {}, conditions_average: {}, \
180 assignments_min: {}, assignments_max: {}, \
181 branches_min: {}, branches_max: {}, \
182 conditions_min: {}, conditions_max: {}",
183 self.assignments_sum(),
184 self.branches_sum(),
185 self.conditions_sum(),
186 self.magnitude_sum(),
187 self.assignments_average(),
188 self.branches_average(),
189 self.conditions_average(),
190 self.assignments_min(),
191 self.assignments_max(),
192 self.branches_min(),
193 self.branches_max(),
194 self.conditions_min(),
195 self.conditions_max()
196 )
197 }
198}
199
200impl Stats {
201 /// Merges a second `Abc` metric into the first one.
202 pub fn merge(&mut self, other: &Stats) {
203 // Calculates minimum and maximum values
204 self.assignments_min = self.assignments_min.min(other.assignments_min);
205 self.assignments_max = self.assignments_max.max(other.assignments_max);
206 self.branches_min = self.branches_min.min(other.branches_min);
207 self.branches_max = self.branches_max.max(other.branches_max);
208 self.conditions_min = self.conditions_min.min(other.conditions_min);
209 self.conditions_max = self.conditions_max.max(other.conditions_max);
210
211 self.assignments_sum += other.assignments_sum;
212 self.branches_sum += other.branches_sum;
213 self.conditions_sum += other.conditions_sum;
214
215 self.space_count += other.space_count;
216 }
217
218 /// Returns the `Abc` assignments metric value.
219 #[must_use]
220 pub fn assignments(&self) -> u64 {
221 self.assignments as u64
222 }
223
224 /// Returns the `Abc` assignments sum metric value.
225 #[must_use]
226 pub fn assignments_sum(&self) -> u64 {
227 self.assignments_sum as u64
228 }
229
230 /// Returns the `Abc` assignments average value.
231 ///
232 /// This value is computed dividing the `Abc`
233 /// assignments value for the number of spaces.
234 #[must_use]
235 pub fn assignments_average(&self) -> f64 {
236 crate::metrics::average(self.assignments_sum() as f64, self.space_count)
237 }
238
239 /// Returns the `Abc` assignments minimum value.
240 ///
241 /// Collapses the `f64::MAX` sentinel that `Stats::default()` plants
242 /// into `assignments_min` to `0`, so a never-observed space
243 /// serializes to a meaningful number rather than `1.7976931e308`.
244 #[allow(clippy::float_cmp)]
245 #[must_use]
246 pub fn assignments_min(&self) -> u64 {
247 if self.assignments_min == f64::MAX {
248 0
249 } else {
250 self.assignments_min as u64
251 }
252 }
253
254 /// Returns the `Abc` assignments maximum value.
255 #[must_use]
256 pub fn assignments_max(&self) -> u64 {
257 self.assignments_max as u64
258 }
259
260 /// Returns the `Abc` branches metric value.
261 #[must_use]
262 pub fn branches(&self) -> u64 {
263 self.branches as u64
264 }
265
266 /// Returns the `Abc` branches sum metric value.
267 #[must_use]
268 pub fn branches_sum(&self) -> u64 {
269 self.branches_sum as u64
270 }
271
272 /// Returns the `Abc` branches average value.
273 ///
274 /// This value is computed dividing the `Abc`
275 /// branches value for the number of spaces.
276 #[must_use]
277 pub fn branches_average(&self) -> f64 {
278 crate::metrics::average(self.branches_sum() as f64, self.space_count)
279 }
280
281 /// Returns the `Abc` branches minimum value.
282 ///
283 /// Same `f64::MAX` sentinel collapse as `assignments_min`.
284 #[allow(clippy::float_cmp)]
285 #[must_use]
286 pub fn branches_min(&self) -> u64 {
287 if self.branches_min == f64::MAX {
288 0
289 } else {
290 self.branches_min as u64
291 }
292 }
293
294 /// Returns the `Abc` branches maximum value.
295 #[must_use]
296 pub fn branches_max(&self) -> u64 {
297 self.branches_max as u64
298 }
299
300 /// Returns the `Abc` conditions metric value.
301 #[must_use]
302 pub fn conditions(&self) -> u64 {
303 self.conditions as u64
304 }
305
306 /// Returns the `Abc` conditions sum metric value.
307 #[must_use]
308 pub fn conditions_sum(&self) -> u64 {
309 self.conditions_sum as u64
310 }
311
312 /// Returns the `Abc` conditions average value.
313 ///
314 /// This value is computed dividing the `Abc`
315 /// conditions value for the number of spaces.
316 #[must_use]
317 pub fn conditions_average(&self) -> f64 {
318 crate::metrics::average(self.conditions_sum() as f64, self.space_count)
319 }
320
321 /// Returns the `Abc` conditions minimum value.
322 ///
323 /// Same `f64::MAX` sentinel collapse as `assignments_min`.
324 #[allow(clippy::float_cmp)]
325 #[must_use]
326 pub fn conditions_min(&self) -> u64 {
327 if self.conditions_min == f64::MAX {
328 0
329 } else {
330 self.conditions_min as u64
331 }
332 }
333
334 /// Returns the `Abc` conditions maximum value.
335 #[must_use]
336 pub fn conditions_max(&self) -> u64 {
337 self.conditions_max as u64
338 }
339
340 /// Returns the `Abc` magnitude metric value.
341 #[must_use]
342 pub fn magnitude(&self) -> f64 {
343 (self.assignments.powi(2) + self.branches.powi(2) + self.conditions.powi(2)).sqrt()
344 }
345
346 /// Returns the `Abc` magnitude sum metric value.
347 #[must_use]
348 pub fn magnitude_sum(&self) -> f64 {
349 (self.assignments_sum.powi(2) + self.branches_sum.powi(2) + self.conditions_sum.powi(2))
350 .sqrt()
351 }
352
353 #[inline]
354 pub(crate) fn compute_sum(&mut self) {
355 self.assignments_sum += self.assignments;
356 self.branches_sum += self.branches;
357 self.conditions_sum += self.conditions;
358 }
359
360 #[inline]
361 pub(crate) fn compute_minmax(&mut self) {
362 self.assignments_min = self.assignments_min.min(self.assignments);
363 self.assignments_max = self.assignments_max.max(self.assignments);
364 self.branches_min = self.branches_min.min(self.branches);
365 self.branches_max = self.branches_max.max(self.branches);
366 self.conditions_min = self.conditions_min.min(self.conditions);
367 self.conditions_max = self.conditions_max.max(self.conditions);
368 self.compute_sum();
369 }
370}
371
372#[doc(hidden)]
373/// Per-language computation of the ABC metric.
374pub(crate) trait Abc
375where
376 Self: Checker,
377{
378 /// Walk `node` and update `stats` with this metric for the language
379 /// implementing the trait.
380 ///
381 /// `code` is the source bytes underlying the parsed tree. Most
382 /// languages ignore it: assignments, branches, and conditions all
383 /// surface as distinct grammar productions and a `kind_id()` match
384 /// is enough. Elixir is the exception — `case` / `cond` / `if` /
385 /// `with` / guard `when` arms surface as `Call` nodes whose keyword
386 /// target lives only in the source text. Matching the `Cyclomatic`
387 /// / `Halstead` / `Exit` / `Cognitive` pattern keeps the signature
388 /// uniform.
389 ///
390 /// `ancestors` is the chain the walker descended through. Nearly
391 /// every language classifies some token by what encloses it: `<`
392 /// and `>` are comparisons only under a binary expression (a type
393 /// -argument list otherwise), and `&&` / `||` reach their operands
394 /// through the enclosing chain node. Reaching that parent with
395 /// [`Node::parent`] costs `O(depth)` per node (#1096). The
396 /// condition-slot walkers take *their* parent as an argument
397 /// instead, because the caller descended from it.
398 fn compute<'a>(
399 node: &Node<'a>,
400 code: &'a [u8],
401 ancestors: Ancestors<'a, '_>,
402 stats: &mut Stats,
403 );
404}
405
406// Shared Phase-2B helper (issue #403): walk every named child of an
407// expression-list-style wrapper (Go's `expression_list`, Lua's
408// `expression_list`) and route each through a language-specific
409// classifier. Used for `return value1, value2, ...` arms where the
410// values live one level below the return statement under a list
411// wrapper. The classifier receives only named children so that
412// `,` / `;` / `(` / `)` tokens never reach it, plus `list` itself as
413// the child's parent — the container classifiers seed their
414// boolean-context flag from the parent kind, and reaching it with
415// `Node::parent` would cost `O(depth)` per node (#1096).
416pub(super) fn for_each_named_child(
417 list: &Node,
418 conditions: &mut f64,
419 f: fn(&Node, &Node, &mut f64),
420) {
421 let mut cursor = list.cursor();
422 if cursor.goto_first_child() {
423 loop {
424 let child = cursor.node();
425 if child.is_named() {
426 f(&child, list, conditions);
427 }
428 if !cursor.goto_next_sibling() {
429 break;
430 }
431 }
432 }
433}
434
435// Default no-op `Abc` impls. Audited in #188; the matrix below
436// records the rationale for every entry so the no-op default is a
437// deliberate choice, not scaffolding leftover.
438//
439// Real defaults (the language has no construct ABC measures, so the
440// metric is genuinely 0):
441// - PreprocCode, CcommentCode: no executable code (comments /
442// preprocessor lines only).
443implement_metric_trait!(Abc, PreprocCode, CcommentCode);
444
445#[cfg(test)]
446#[allow(
447 clippy::float_cmp,
448 clippy::cast_precision_loss,
449 clippy::cast_possible_truncation,
450 clippy::cast_sign_loss,
451 clippy::similar_names,
452 clippy::doc_markdown,
453 clippy::needless_raw_string_hashes,
454 clippy::too_many_lines
455)]
456mod tests {
457 use crate::test_support::{
458 ast_has_kind_id, check_func_space_only_shim, check_metrics_only_shim, metrics_verbatim,
459 };
460 use crate::traits::ParserTrait;
461
462 use super::*;
463
464 check_metrics_only_shim!(check_metrics, Abc);
465 // Every `check_func_space` caller in this module is an ABC-versus-
466 // cyclomatic parity test (`abc.conditions() == cyclomatic() - 1` on
467 // the same space), so the func-space shim carries Cyclomatic too.
468 check_func_space_only_shim!(check_func_space, Abc, Cyclomatic);
469
470 /// `abc.conditions_sum()` of `src` under a walk restricted to ABC
471 /// and the metrics it resolves, for the cross-language parity tests
472 /// that compare one construct across sibling grammars.
473 /// `check_metrics` expands to a plain `fn` and so cannot close over
474 /// a reference value; this can. Restricted rather than
475 /// `MetricsOptions::default()` per `metrics_verbatim`'s own doc
476 /// (#1127).
477 fn abc_conditions(lang: LANG, src: &str) -> u64 {
478 metrics_verbatim(
479 lang,
480 src.as_bytes(),
481 MetricsOptions::default().with_only(&[crate::Metric::Abc]),
482 )
483 .abc
484 .conditions_sum()
485 }
486
487 // Recurse to the innermost function space and assert the invariant
488 // named above there. `conditions()` is that one space's own count,
489 // so it pairs with `cyclomatic()` and never `cyclomatic_sum()`,
490 // which folds in a base of 1 per nested space.
491 fn assert_deepest_conditions_match_cyclomatic(space: &crate::FuncSpace, expected: u64) {
492 let mut deepest = space;
493 while let Some(child) = deepest.spaces.last() {
494 deepest = child;
495 }
496 let decisions = deepest.metrics.cyclomatic.cyclomatic() - 1;
497 assert_eq!(decisions, expected);
498 assert_eq!(deepest.metrics.abc.conditions(), decisions);
499 }
500
501 /// Regression for #227: a `Stats::default()` that never sees an
502 /// observation must not leak the `f64::MAX` sentinel for
503 /// `assignments_min`, `branches_min`, or `conditions_min`. All
504 /// three getters collapse the sentinel to `0.0` so JSON never
505 /// emits `1.7976931e308`.
506 #[test]
507 fn abc_empty_file_min_is_zero() {
508 let stats = Stats::default();
509 assert_eq!(stats.assignments_min(), 0);
510 assert_eq!(stats.branches_min(), 0);
511 assert_eq!(stats.conditions_min(), 0);
512 }
513
514 // The `EQ` arm of `java_count_token_assignment`: a plain `=` counts
515 // unless `java_eq_initializes_final_binding` finds it initialising a
516 // `final` binding, whether the declaration is a local or a field.
517 #[test]
518 fn java_eq_arm_counts_outside_final_declarations() {
519 check_metrics::<JavaParser>(
520 "class A { void m() { int x = 0; x = 1; x = 2; x = 3; } }",
521 "foo.java",
522 |metric| {
523 // `int x = 0;` is not `final`, so it counts like each
524 // `x = N;` that follows.
525 assert_eq!(metric.abc.assignments_sum(), 4);
526 },
527 );
528 }
529
530 #[test]
531 fn java_eq_arm_skips_final_initializers() {
532 check_metrics::<JavaParser>(
533 "class A {
534 final int X = 1;
535 @Deprecated private final int Y = 2;
536 void m() { final int Z = 3; }
537 }",
538 "foo.java",
539 |metric| {
540 // All three `=` tokens are `final` initializers — the
541 // second behind an annotation and an access modifier in
542 // the same `modifiers` node — so assignments are 0
543 // across all spaces.
544 assert_eq!(metric.abc.assignments_sum(), 0);
545 },
546 );
547 }
548
549 #[test]
550 fn java_final_initializer_does_not_suppress_the_assignments_inside_it() {
551 // The sentinel stack the structural predicate replaced stayed
552 // live from `final` to the next `;`, so every `=` nested in the
553 // initializer — a lambda body's, an array initializer's — was
554 // suppressed with the declarator's own. Java lambdas open no
555 // space, so nothing else separated them. Each row names the
556 // pre-fix value.
557 let cases = [
558 ("void m() { final Runnable r = () -> { x = 1; }; }", 1, 0),
559 (
560 "void m() { final Runnable r = () -> { x = 1; y = 2; }; }",
561 2,
562 1,
563 ),
564 ("void m() { final Runnable r = () -> x = 1; }", 1, 0),
565 ("void m() { final int[] a = { x = 1 }; }", 1, 0),
566 ("private final Runnable f = () -> { x = 1; };", 1, 0),
567 // The `;` that closed the leak is the reference: the same
568 // body after a `final` local counts as it always did.
569 ("void m() { final int q = 1; x = 1; }", 1, 1),
570 ];
571 let mut ran = 0;
572 for (body, expected, before) in cases {
573 let src = format!("class K {{ int x, y; {body} }}\n");
574 let assignments = metrics_verbatim(
575 LANG::Java,
576 src.as_bytes(),
577 MetricsOptions::default().with_only(&[crate::Metric::Abc]),
578 )
579 .abc
580 .assignments_sum();
581 assert_eq!(assignments, expected, "`{body}` (pre-fix {before})");
582 ran += 1;
583 }
584 assert_eq!(ran, cases.len());
585 assert!(cases.iter().any(|&(_, now, before)| now != before));
586 }
587
588 // Constant declarations are not counted as assignments
589 #[test]
590 fn java_constant_declarations() {
591 check_metrics::<JavaParser>(
592 "class A {
593 private final int X1 = 0, Y1 = 0;
594 public final float PI = 3.14f;
595 final static String HELLO = \"Hello,\";
596 protected String world = \" world!\"; // +1a
597 public float e = 2.718f; // +1a
598 private int x2 = 1, y2 = 2; // +2a
599
600 void m() {
601 final int Z1 = 0, Z2 = 0, Z3 = 0;
602 final float T = 0.0f;
603 int z1 = 1, z2 = 2, z3 = 3; // +3a
604 float t = 60.0f; // +1a
605 }
606 }",
607 "foo.java",
608 |metric| {
609 // magnitude: sqrt(64 + 0 + 0) = sqrt(64)
610 // space count: 3 (1 unit, 1 class and 1 method)
611 insta::assert_json_snapshot!(
612 metric.abc,
613 @r#"
614 {
615 "assignments": 8,
616 "branches": 0,
617 "conditions": 0,
618 "magnitude": 8.0,
619 "value": 0.0,
620 "assignments_average": 2.6666666666666665,
621 "branches_average": 0.0,
622 "conditions_average": 0.0,
623 "assignments_min": 0,
624 "assignments_max": 4,
625 "branches_min": 0,
626 "branches_max": 0,
627 "conditions_min": 0,
628 "conditions_max": 0
629 }
630 "#
631 );
632 },
633 );
634 }
635
636 // "In computer science, conditionals (that is, conditional statements, conditional expressions
637 // and conditional constructs,) are programming language commands for handling decisions."
638 // Source: https://en.wikipedia.org/wiki/Conditional_(computer_programming)
639 // According to this definition, boolean expressions that are evaluated to make a decision are considered as conditions
640 // Variables, method invocations and true or false values used inside
641 // variable declarations and assignment expressions are not counted as conditions
642 #[test]
643 fn java_declarations_with_conditions() {
644 check_metrics::<JavaParser>(
645 "
646 boolean a = (1 > 2); // +1a +1c
647 boolean b = 3 > 4; // +1a +1c
648 boolean c = (1 > 2) && 3 > 4; // +1a +2c
649 boolean d = b && (x > 5) || c; // +1a +3c
650 boolean e = !d; // +1a +1c
651 boolean f = ((!false)); // +1a +1c
652 boolean g = !(!(true)); // +1a +1c
653 boolean h = true; // +1a
654 boolean i = (false); // +1a
655 boolean j = (((((true))))); // +1a
656 boolean k = (((((m()))))); // +1a +1b
657 boolean l = (((((!m()))))); // +1a +1b +1c
658 boolean m = (!(!((m())))); // +1a +1b +1c
659 List<String> n = null; // +1a (< and > used for generic types are not counted as conditions)
660 ",
661 "foo.java",
662 |metric| {
663 // magnitude: sqrt(196 + 9 + 144) = sqrt(349)
664 // space count: 1 (1 unit)
665 insta::assert_json_snapshot!(
666 metric.abc,
667 @r#"
668 {
669 "assignments": 14,
670 "branches": 3,
671 "conditions": 12,
672 "magnitude": 18.681541692269406,
673 "value": 18.681541692269406,
674 "assignments_average": 14.0,
675 "branches_average": 3.0,
676 "conditions_average": 12.0,
677 "assignments_min": 14,
678 "assignments_max": 14,
679 "branches_min": 3,
680 "branches_max": 3,
681 "conditions_min": 12,
682 "conditions_max": 12
683 }
684 "#
685 );
686 },
687 );
688 }
689
690 // Conditions can be found in assignment expressions
691 #[test]
692 fn java_assignments_with_conditions() {
693 check_metrics::<JavaParser>(
694 "
695 a = 2 < 1; // +1a +1c
696 b = (4 >= 3) && 2 <= 1; // +1a +2c
697 c = a || (x != 10) && b; // +1a +3c
698 d = !false; // +1a +1c
699 e = (!false); // +1a +1c
700 f = !(false); // +1a +1c
701 g = (!(((true)))); // +1a +1c
702 h = ((true)); // +1a
703 i = !m(); // +1a +1b +1c
704 j = !((m())); // +1a +1b +1c
705 k = (!(m())); // +1a +1b +1c
706 l = ((!(m()))); // +1a +1b +1c
707 m = !B.<Integer>m(2); // +1a +1b +1c
708 n = !((B.<Integer>m(4))); // +1a +1b +1c
709 ",
710 "foo.java",
711 |metric| {
712 // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
713 // space count: 1 (1 unit)
714 insta::assert_json_snapshot!(
715 metric.abc,
716 @r#"
717 {
718 "assignments": 14,
719 "branches": 6,
720 "conditions": 16,
721 "magnitude": 22.090722034374522,
722 "value": 22.090722034374522,
723 "assignments_average": 14.0,
724 "branches_average": 6.0,
725 "conditions_average": 16.0,
726 "assignments_min": 14,
727 "assignments_max": 14,
728 "branches_min": 6,
729 "branches_max": 6,
730 "conditions_min": 16,
731 "conditions_max": 16
732 }
733 "#
734 );
735 },
736 );
737 }
738
739 // Conditions can be found in method arguments
740 #[test]
741 fn java_methods_arguments_with_conditions() {
742 check_metrics::<JavaParser>(
743 "
744 m1(a); // +1b
745 m2(a, b); // +1b
746 m3(true, (false), (((true)))); // +1b
747 m3(m1(false), m1(true), m1(false)); // +4b
748 m1(!a); // +1b +1c
749 m2((((a))), (!b)); // +1b +1c
750 m3(!(a), b, !!!c); // +1b +2c
751 m3(a, !b, m2(!a, !m2(!b, !m1(!c)))); // +4b +6c
752 ",
753 "foo.java",
754 |metric| {
755 // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
756 // space count: 1 (1 unit)
757 insta::assert_json_snapshot!(
758 metric.abc,
759 @r#"
760 {
761 "assignments": 0,
762 "branches": 14,
763 "conditions": 10,
764 "magnitude": 17.204650534085253,
765 "value": 17.204650534085253,
766 "assignments_average": 0.0,
767 "branches_average": 14.0,
768 "conditions_average": 10.0,
769 "assignments_min": 0,
770 "assignments_max": 0,
771 "branches_min": 14,
772 "branches_max": 14,
773 "conditions_min": 10,
774 "conditions_max": 10
775 }
776 "#
777 );
778 },
779 );
780 }
781
782 // "A unary conditional expression is an implicit condition that uses no relational operators."
783 // Source: Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report.
784 // https://www.softwarerenovation.com/Articles.aspx (page 5)
785 #[test]
786 fn java_if_single_conditions() {
787 check_metrics::<JavaParser>(
788 "
789 if ( a < 0 ) {} // +1c
790 if ( ((a != 0)) ) {} // +1c
791 if ( !(a > 0) ) {} // +1c
792 if ( !(((a == 0))) ) {} // +1c
793 if ( b.m1() ) {} // +1b +1c
794 if ( !b.m1() ) {} // +1b +1c
795 if ( !!b.m2() ) {} // +1b +1c
796 if ( (!(b.m1())) ) {} // +1b +1c
797 if ( (!(!b.m1())) ) {} // +1b +1c
798 if ( ((b.m2())) ) {} // +1b +1c
799 if ( ((b.m().m1())) ) {} // +2b +1c
800 if ( c ) {} // +1c
801 if ( !c ) {} // +1c
802 if ( !!!!!!!!!!c ) {} // +1c
803 if ( (((c))) ) {} // +1c
804 if ( (((!c))) ) {} // +1c
805 if ( ((!(c))) ) {} // +1c
806 if ( true ) {} // +1c
807 if ( !true ) {} // +1c
808 if ( ((false)) ) {} // +1c
809 if ( !(!(false)) ) {} // +1c
810 if ( !!!false ) {} // +1c
811 ",
812 "foo.java",
813 |metric| {
814 // magnitude: sqrt(0 + 64 + 484) = sqrt(548)
815 // space count: 1 (1 unit)
816 insta::assert_json_snapshot!(
817 metric.abc,
818 @r#"
819 {
820 "assignments": 0,
821 "branches": 8,
822 "conditions": 22,
823 "magnitude": 23.40939982143925,
824 "value": 23.40939982143925,
825 "assignments_average": 0.0,
826 "branches_average": 8.0,
827 "conditions_average": 22.0,
828 "assignments_min": 0,
829 "assignments_max": 0,
830 "branches_min": 8,
831 "branches_max": 8,
832 "conditions_min": 22,
833 "conditions_max": 22
834 }
835 "#
836 );
837 },
838 );
839 }
840
841 #[test]
842 fn java_if_multiple_conditions() {
843 check_metrics::<JavaParser>(
844 "
845 if ( a || b || c || d ) {} // +4c
846 if ( a || b && c && d ) {} // +4c
847 if ( x < y && a == b ) {} // +2c
848 if ( ((z < (x + y))) ) {} // +1c
849 if ( a || ((((b))) && c) ) {} // +3c
850 if ( a && ((((a == b))) && c) ) {} // +3c
851 if ( a || ((((a == b))) || ((c))) ) {} // +3c
852 if ( x < y && B.m() ) {} // +1b +2c
853 if ( x < y && !(((B.m()))) ) {} // +1b +2c
854 if ( !(x < y) && !B.m() ) {} // +1b +2c
855 if ( !!!(!!!(a)) && B.m() || // +1b +2c
856 !B.m() && (((x > 4))) ) {} // +1b +2c
857 ",
858 "foo.java",
859 |metric| {
860 // magnitude: sqrt(0 + 25 + 900) = sqrt(925)
861 // space count: 1 (1 unit)
862 insta::assert_json_snapshot!(
863 metric.abc,
864 @r#"
865 {
866 "assignments": 0,
867 "branches": 5,
868 "conditions": 30,
869 "magnitude": 30.4138126514911,
870 "value": 30.4138126514911,
871 "assignments_average": 0.0,
872 "branches_average": 5.0,
873 "conditions_average": 30.0,
874 "assignments_min": 0,
875 "assignments_max": 0,
876 "branches_min": 5,
877 "branches_max": 5,
878 "conditions_min": 30,
879 "conditions_max": 30
880 }
881 "#
882 );
883 },
884 );
885 }
886
887 #[test]
888 fn java_while_and_do_while_conditions() {
889 check_metrics::<JavaParser>(
890 "
891 while ( (!(!(!(a)))) ) {} // +1c
892 while ( b || 1 > 2 ) {} // +2c
893 while ( x.m() && (((c))) ) {} // +1b +2c
894 do {} while ( !!!(((!!!a))) ); // +1c
895 do {} while ( a || (b && c) ); // +3c
896 do {} while ( !x.m() && 1 > 2 || !true ); // +1b +3c
897 ",
898 "foo.java",
899 |metric| {
900 // magnitude: sqrt(0 + 4 + 144) = sqrt(148)
901 // space count: 1 (1 unit)
902 insta::assert_json_snapshot!(
903 metric.abc,
904 @r#"
905 {
906 "assignments": 0,
907 "branches": 2,
908 "conditions": 12,
909 "magnitude": 12.165525060596439,
910 "value": 12.165525060596439,
911 "assignments_average": 0.0,
912 "branches_average": 2.0,
913 "conditions_average": 12.0,
914 "assignments_min": 0,
915 "assignments_max": 0,
916 "branches_min": 2,
917 "branches_max": 2,
918 "conditions_min": 12,
919 "conditions_max": 12
920 }
921 "#
922 );
923 },
924 );
925 }
926
927 // GMetrics, a Groovy source code analyzer, provides the following definition of unary conditional expression:
928 // "These are cases where a single variable/field/value is treated as a boolean value.
929 // Examples include `if (x)` and `return !ready`."
930 // According to this definition, unary conditional expressions are counted also in function return values.
931 // Source: https://dx42.github.io/gmetrics/metrics/AbcMetric.html
932 // Examples: https://github.com/dx42/gmetrics/blob/master/src/test/groovy/org/gmetrics/metric/abc/AbcMetric_MethodTest.groovy
933 #[test]
934 fn java_return_with_conditions() {
935 check_metrics::<JavaParser>(
936 "class A {
937 boolean m1() {
938 return !(z >= 0); // +1c
939 }
940 boolean m2() {
941 return (((!x))); // +1c
942 }
943 boolean m3() {
944 return x && y; // +2c
945 }
946 boolean m4() {
947 return y || (z < 0); // +2c
948 }
949 boolean m5() {
950 return x || y ? // +3c (two unary conditions and one ?)
951 true : false;
952 }
953 }",
954 "foo.java",
955 |metric| {
956 // magnitude: sqrt(0 + 0 + 81) = sqrt(81)
957 // space count: 7 (1 unit, 1 class and 5 methods)
958 insta::assert_json_snapshot!(
959 metric.abc,
960 @r#"
961 {
962 "assignments": 0,
963 "branches": 0,
964 "conditions": 9,
965 "magnitude": 9.0,
966 "value": 0.0,
967 "assignments_average": 0.0,
968 "branches_average": 0.0,
969 "conditions_average": 1.2857142857142858,
970 "assignments_min": 0,
971 "assignments_max": 0,
972 "branches_min": 0,
973 "branches_max": 0,
974 "conditions_min": 0,
975 "conditions_max": 3
976 }
977 "#
978 );
979 },
980 );
981 }
982
983 // Variables, method invocations, and true or false values
984 // inside return statements are not counted as conditions
985 #[test]
986 fn java_return_without_conditions() {
987 check_metrics::<JavaParser>(
988 "class A {
989 boolean m1() {
990 return x;
991 }
992 boolean m2() {
993 return (x);
994 }
995 boolean m3() {
996 return y.m(); // +1b
997 }
998 boolean m4() {
999 return false;
1000 }
1001 void m5() {
1002 return;
1003 }
1004 }",
1005 "foo.java",
1006 |metric| {
1007 // magnitude: sqrt(0 + 1 + 0) = sqrt(1)
1008 // space count: 7 (1 unit, 1 class and 5 methods)
1009 insta::assert_json_snapshot!(
1010 metric.abc,
1011 @r#"
1012 {
1013 "assignments": 0,
1014 "branches": 1,
1015 "conditions": 0,
1016 "magnitude": 1.0,
1017 "value": 0.0,
1018 "assignments_average": 0.0,
1019 "branches_average": 0.14285714285714285,
1020 "conditions_average": 0.0,
1021 "assignments_min": 0,
1022 "assignments_max": 0,
1023 "branches_min": 0,
1024 "branches_max": 1,
1025 "conditions_min": 0,
1026 "conditions_max": 0
1027 }
1028 "#
1029 );
1030 },
1031 );
1032 }
1033
1034 // Variables, method invocations, and true or false values
1035 // in lambda expression return values are not counted as conditions
1036 #[test]
1037 fn java_lambda_expressions_return_with_conditions() {
1038 check_metrics::<JavaParser>(
1039 "
1040 Predicate<Boolean> p1 = a -> a; // +1a
1041 Predicate<Boolean> p2 = b -> true; // +1a
1042 Predicate<Boolean> p3 = c -> m(); // +1a
1043 Predicate<Integer> p4 = d -> d > 10; // +1a +1c
1044 Predicate<Boolean> p5 = (e) -> !e; // +1a +1c
1045 Predicate<Boolean> p6 = (f) -> !((!f)); // +1a +1c
1046 Predicate<Boolean> p7 = (g) -> !g && true; // +1a +2c
1047 BiPredicate<Boolean, Boolean> bp1 = (h, i) -> !h && !i; // +1a +2c
1048 BiPredicate<Boolean, Boolean> bp2 = (j, k) -> {
1049 return j || k; // +1a +2c
1050 };
1051 ",
1052 "foo.java",
1053 |metric| {
1054 // magnitude: sqrt(81 + 1 + 81) = sqrt(163)
1055 // space count: 1 (1 unit)
1056 insta::assert_json_snapshot!(
1057 metric.abc,
1058 @r#"
1059 {
1060 "assignments": 9,
1061 "branches": 1,
1062 "conditions": 9,
1063 "magnitude": 12.767145334803704,
1064 "value": 12.767145334803704,
1065 "assignments_average": 9.0,
1066 "branches_average": 1.0,
1067 "conditions_average": 9.0,
1068 "assignments_min": 9,
1069 "assignments_max": 9,
1070 "branches_min": 1,
1071 "branches_max": 1,
1072 "conditions_min": 9,
1073 "conditions_max": 9
1074 }
1075 "#
1076 );
1077 },
1078 );
1079 }
1080
1081 #[test]
1082 fn java_for_with_variable_declaration() {
1083 check_metrics::<JavaParser>(
1084 "
1085 for ( int i1 = 0; !(!(!(!a))); i1++ ) {} // +2a +1c
1086 for ( int i2 = 0; !B.m(); i2++ ) {} // +2a +1b +1c
1087 for ( int i3 = 0; a || false; i3++ ) {} // +2a +2c
1088 for ( int i4 = 0; a && B.m() ? true : false; i4++ ) {} // +2a +1b +3c
1089 for ( int i5 = 0; true; i5++ ) {} // +2a +1c
1090 ",
1091 "foo.java",
1092 |metric| {
1093 // magnitude: sqrt(100 + 4 + 64) = sqrt(168)
1094 // space count: 1 (1 unit)
1095 insta::assert_json_snapshot!(
1096 metric.abc,
1097 @r#"
1098 {
1099 "assignments": 10,
1100 "branches": 2,
1101 "conditions": 8,
1102 "magnitude": 12.96148139681572,
1103 "value": 12.96148139681572,
1104 "assignments_average": 10.0,
1105 "branches_average": 2.0,
1106 "conditions_average": 8.0,
1107 "assignments_min": 10,
1108 "assignments_max": 10,
1109 "branches_min": 2,
1110 "branches_max": 2,
1111 "conditions_min": 8,
1112 "conditions_max": 8
1113 }
1114 "#
1115 );
1116 },
1117 );
1118 }
1119
1120 #[test]
1121 fn java_for_without_variable_declaration() {
1122 check_metrics::<JavaParser>(
1123 "class A{
1124 void m1() {
1125 for (i = 0; x < y; i++) {} // +2a +1c
1126 for (i = 0; ((x < y)); i++) {} // +2a +1c
1127 for (i = 0; !(!(x < y)); i++) {} // +2a +1c
1128 for (i = 0; true; i++) {} // +2a +1c
1129 }
1130 void m2() {
1131 for ( ; true; ) {} // +1c
1132 }
1133 void m3() {
1134 for ( ; ; ) {} // +0c — no condition to count (#1276)
1135 }
1136 }",
1137 "foo.java",
1138 |metric| {
1139 // magnitude: sqrt(64 + 0 + 25) = sqrt(89)
1140 // space count: 5 (1 unit, 1 class and 3 methods)
1141 insta::assert_json_snapshot!(
1142 metric.abc,
1143 @r#"
1144 {
1145 "assignments": 8,
1146 "branches": 0,
1147 "conditions": 5,
1148 "magnitude": 9.433981132056603,
1149 "value": 0.0,
1150 "assignments_average": 1.6,
1151 "branches_average": 0.0,
1152 "conditions_average": 1.0,
1153 "assignments_min": 0,
1154 "assignments_max": 8,
1155 "branches_min": 0,
1156 "branches_max": 0,
1157 "conditions_min": 0,
1158 "conditions_max": 4
1159 }
1160 "#
1161 );
1162 },
1163 );
1164 }
1165
1166 // Issue #1276 changed Java's answer here. `java_walk_for_statement`
1167 // used to read child(3), fall through to child(4) when that was the
1168 // `;` an expression initializer leaves behind, and count a `;` or
1169 // `)` landing there as a vacuously-true condition — so `for (;;)`
1170 // scored one. Nothing in Fitzpatrick's C dimension attributes a
1171 // condition to a `for` keyword; it counts conditional operators and
1172 // unary conditions that are present. Java and Groovy were the only
1173 // two impls disagreeing, and the field-addressed walker now reports
1174 // zero the way the C family, the JS family, PHP, C# and Go all do.
1175 #[test]
1176 fn java_empty_for_condition_counts_nothing() {
1177 check_metrics::<JavaParser>(
1178 "class A { void m() { for (;;) { break; } } }",
1179 "foo.java",
1180 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
1181 );
1182 // The other empty spelling, and the one the old cascade
1183 // actually mis-scored: with an **expression** initializer the
1184 // children are `for ( init ; ; update )`, so child(3) was the
1185 // separating `;` and child(4) the empty condition's `;`, which
1186 // the vacuous-true arm counted. The `for (int i = 0; ; i++)`
1187 // spelling scored 0 both before and after — Java's
1188 // `local_variable_declaration` swallows its own `;`, putting the
1189 // update expression at child(4), where it matched no arm — so it
1190 // would be a vacuous regression test and is deliberately not
1191 // used here.
1192 check_metrics::<JavaParser>(
1193 "class A { void m() { int i; for (i = 0; ; i++) { break; } } }",
1194 "foo.java",
1195 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
1196 );
1197 }
1198
1199 // The second defect the positional cascade carried: tree-sitter
1200 // counts comments among a node's children, so a comment anywhere in
1201 // the header shifted every index and the condition went unread —
1202 // the same failure #1181 removed from `java_walk_ternary`. Reading
1203 // the `condition` field cannot shift.
1204 #[test]
1205 fn java_for_condition_survives_a_header_comment() {
1206 check_metrics::<JavaParser>(
1207 "class A { void m(boolean a) { for (; /* n */ a; ) { break; } } }",
1208 "foo.java",
1209 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
1210 );
1211 // Unchanged control: the same loop without the comment. Both
1212 // spellings must agree, which is the property the positional
1213 // form broke.
1214 check_metrics::<JavaParser>(
1215 "class A { void m(boolean a) { for (; a; ) { break; } } }",
1216 "foo.java",
1217 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
1218 );
1219 }
1220
1221 // Variables, method invocations, and true or false values
1222 // in ternary expression return values are not counted as conditions
1223 #[test]
1224 fn java_ternary_conditions() {
1225 check_metrics::<JavaParser>(
1226 "
1227 a = true; // +1a
1228 b = a ? true : false; // +1a +2c
1229 c = ((((a)))) ? !false : !b; // +1a +4c
1230 d = !this.m() ? !!a : (false); // +1a +1b +3c
1231 e = !(a) && b ? ((c)) : !d; // +1a +4c
1232 if ( this.m() ? a : !this.m() ) {} // +2b +3c
1233 if ( x > 0 ? !(false) : this.m() ) {} // +1b +3c
1234 if ( x > 0 && x != 3 ? !(a) : (!(b)) ) {} // +5c
1235 ",
1236 "foo.java",
1237 |metric| {
1238 // magnitude: sqrt(25 + 16 + 576) = sqrt(617)
1239 // space count: 1 (1 unit)
1240 insta::assert_json_snapshot!(
1241 metric.abc,
1242 @r#"
1243 {
1244 "assignments": 5,
1245 "branches": 4,
1246 "conditions": 24,
1247 "magnitude": 24.839484696748443,
1248 "value": 24.839484696748443,
1249 "assignments_average": 5.0,
1250 "branches_average": 4.0,
1251 "conditions_average": 24.0,
1252 "assignments_min": 5,
1253 "assignments_max": 5,
1254 "branches_min": 4,
1255 "branches_max": 4,
1256 "conditions_min": 24,
1257 "conditions_max": 24
1258 }
1259 "#
1260 );
1261 },
1262 );
1263 }
1264
1265 #[test]
1266 fn bash_assignments_only() {
1267 check_metrics::<BashParser>(
1268 "f() {
1269 a=1
1270 b=2
1271 c+=3
1272 }",
1273 "foo.sh",
1274 |metric| {
1275 insta::assert_json_snapshot!(
1276 metric.abc,
1277 @r#"
1278 {
1279 "assignments": 3,
1280 "branches": 0,
1281 "conditions": 0,
1282 "magnitude": 3.0,
1283 "value": 0.0,
1284 "assignments_average": 1.5,
1285 "branches_average": 0.0,
1286 "conditions_average": 0.0,
1287 "assignments_min": 0,
1288 "assignments_max": 3,
1289 "branches_min": 0,
1290 "branches_max": 0,
1291 "conditions_min": 0,
1292 "conditions_max": 0
1293 }
1294 "#
1295 );
1296 },
1297 );
1298 }
1299
1300 #[test]
1301 fn bash_commands_only() {
1302 check_metrics::<BashParser>(
1303 "f() {
1304 echo a
1305 ls
1306 }",
1307 "foo.sh",
1308 |metric| {
1309 insta::assert_json_snapshot!(
1310 metric.abc,
1311 @r#"
1312 {
1313 "assignments": 0,
1314 "branches": 2,
1315 "conditions": 0,
1316 "magnitude": 2.0,
1317 "value": 0.0,
1318 "assignments_average": 0.0,
1319 "branches_average": 1.0,
1320 "conditions_average": 0.0,
1321 "assignments_min": 0,
1322 "assignments_max": 0,
1323 "branches_min": 0,
1324 "branches_max": 2,
1325 "conditions_min": 0,
1326 "conditions_max": 0
1327 }
1328 "#
1329 );
1330 },
1331 );
1332 }
1333
1334 #[test]
1335 fn bash_control_flow_counts_conditions() {
1336 // Regression for #696: Bash control-flow branches are ABC
1337 // conditions (a Bash predicate is a command, so the branch keyword
1338 // is the only condition signal). Each mirrors a cyclomatic decision.
1339 //
1340 // expected: 4 conditions — `if` (1) + `elif` (1) + `while` (1) +
1341 // the non-wildcard case arm `a)` (1). The bare-`*)` wildcard arm is
1342 // the Bash analogue of `default:` and is excluded, exactly as the
1343 // cyclomatic standard count excludes it. No comparison / test
1344 // operators appear, so every condition here is control-flow.
1345 check_metrics::<BashParser>(
1346 "f() {
1347 if cmd; then
1348 echo a
1349 elif other; then
1350 echo b
1351 fi
1352 while running; do
1353 echo c
1354 done
1355 case \"$x\" in
1356 a) echo d ;;
1357 *) echo e ;;
1358 esac
1359 }",
1360 "foo.sh",
1361 |metric| {
1362 assert_eq!(metric.abc.conditions_sum(), 4);
1363 },
1364 );
1365 }
1366
1367 #[test]
1368 fn bash_conditions_mix() {
1369 // Exercises every condition path: `==` and `!=` inside `[[ ]]`,
1370 // arithmetic `<` inside `(( ))`, and the prefix `-z` test operator
1371 // inside `[ ]`. Each `if` body's `echo` contributes a branch.
1372 //
1373 // expected: 8 conditions — each of the four `if`s contributes one
1374 // for the control-flow branch (#696) plus one for its comparison /
1375 // test operator (`==`, `!=`, `<`, `-z`). 4 branches (one `echo`
1376 // each). magnitude = sqrt(4² + 8²) = sqrt(80).
1377 check_metrics::<BashParser>(
1378 "f() {
1379 if [[ \"$a\" == \"$b\" ]]; then
1380 echo eq
1381 fi
1382 if [[ \"$x\" != \"$y\" ]]; then
1383 echo ne
1384 fi
1385 if (( $a < $b )); then
1386 echo lt
1387 fi
1388 if [ -z \"$x\" ]; then
1389 echo empty
1390 fi
1391 }",
1392 "foo.sh",
1393 |metric| {
1394 assert_eq!(metric.abc.conditions_sum(), 8);
1395 assert_eq!(metric.abc.branches_sum(), 4);
1396 insta::assert_json_snapshot!(
1397 metric.abc,
1398 @r#"
1399 {
1400 "assignments": 0,
1401 "branches": 4,
1402 "conditions": 8,
1403 "magnitude": 8.94427190999916,
1404 "value": 0.0,
1405 "assignments_average": 0.0,
1406 "branches_average": 2.0,
1407 "conditions_average": 4.0,
1408 "assignments_min": 0,
1409 "assignments_max": 0,
1410 "branches_min": 0,
1411 "branches_max": 4,
1412 "conditions_min": 0,
1413 "conditions_max": 8
1414 }
1415 "#
1416 );
1417 },
1418 );
1419 }
1420
1421 #[test]
1422 fn bash_redirection_is_not_a_condition() {
1423 // `>` and `<` spell an I/O redirection as well as a comparison, and
1424 // the grammar parents the redirection under `file_redirect` rather
1425 // than `binary_expression`. Ungated, every redirect in a script
1426 // scored a condition: this fixture measured 2 before the parent
1427 // gate — the Bash instance of #1280's positive-parent polarity.
1428 // expected: 0 conditions — no test, no `if`, no comparison.
1429 check_metrics::<BashParser>(
1430 "f() {\n echo hi > out.txt\n read x < in.txt\n}\n",
1431 "foo.sh",
1432 |metric| {
1433 assert_eq!(metric.abc.conditions_sum(), 0);
1434 // The two `echo` / `read` commands still count as branches,
1435 // so the zero above is the gate firing rather than the walk
1436 // skipping the function body.
1437 assert_eq!(metric.abc.branches_sum(), 2);
1438 },
1439 );
1440 }
1441
1442 #[test]
1443 fn bash_comparison_inside_an_arithmetic_or_test_context_is_a_condition() {
1444 // The positive control for the gate above: the same tokens under a
1445 // `binary_expression` are real comparisons in both the `[[ … ]]`
1446 // test form and the `(( … ))` arithmetic form, and still count.
1447 // expected: 4 — one `if` control-flow condition and one `>` per
1448 // function.
1449 check_metrics::<BashParser>(
1450 "f() {\n if [[ $a > $b ]]; then :; fi\n}\ng() {\n if (( a > b )); then :; fi\n}\n",
1451 "foo.sh",
1452 |metric| {
1453 assert_eq!(metric.abc.conditions_sum(), 4);
1454 },
1455 );
1456 }
1457
1458 #[test]
1459 fn bash_arithmetic_ternary_is_a_condition() {
1460 // The ABC half of #1268. Cyclomatic and cognitive both count Bash's
1461 // only ternary form; ABC did not, so the identical construct scored
1462 // 1 here against the C family's 2.
1463 // expected: 2 — the `>` comparison and the ternary itself, matching
1464 // `int m = a > b ? a : b;` in C.
1465 check_metrics::<BashParser>(
1466 "f() {\n local m=$(( a > b ? a : b ))\n}\n",
1467 "foo.sh",
1468 |metric| {
1469 assert_eq!(metric.abc.conditions_sum(), 2);
1470 },
1471 );
1472 }
1473
1474 #[test]
1475 fn bash_magnitude() {
1476 // Combined assignments + branches + conditions. The single `if`
1477 // contributes two conditions (the control-flow branch, #696, plus
1478 // the `==` operator), so magnitude = sqrt(2² + 1² + 2²) = sqrt(9).
1479 check_metrics::<BashParser>(
1480 "f() {
1481 a=1
1482 b=2
1483 if [[ \"$a\" == \"$b\" ]]; then
1484 echo eq
1485 fi
1486 }",
1487 "foo.sh",
1488 |metric| {
1489 assert_eq!(metric.abc.conditions_sum(), 2);
1490 insta::assert_json_snapshot!(
1491 metric.abc,
1492 @r#"
1493 {
1494 "assignments": 2,
1495 "branches": 1,
1496 "conditions": 2,
1497 "magnitude": 3.0,
1498 "value": 0.0,
1499 "assignments_average": 1.0,
1500 "branches_average": 0.5,
1501 "conditions_average": 1.0,
1502 "assignments_min": 0,
1503 "assignments_max": 2,
1504 "branches_min": 0,
1505 "branches_max": 1,
1506 "conditions_min": 0,
1507 "conditions_max": 2
1508 }
1509 "#
1510 );
1511 },
1512 );
1513 }
1514
1515 #[test]
1516 fn java_malformed_parenthesized_no_panic() {
1517 check_metrics::<JavaParser>("class A { void m() { if (( }) }", "foo.java", |metric| {
1518 // tree-sitter emits ERROR nodes for this malformed source, so no
1519 // IfStatement, branch, or condition is recognised — all counts are 0.
1520 // Primary goal: the unwrap-free path does not panic.
1521 assert_eq!(metric.abc.assignments(), 0);
1522 assert_eq!(metric.abc.branches(), 0);
1523 assert_eq!(metric.abc.conditions(), 0);
1524 assert_eq!(metric.abc.magnitude(), 0.0);
1525 });
1526 }
1527
1528 #[test]
1529 fn java_bool_returning_terminal_kinds_count() {
1530 // Companion to `csharp_bool_returning_terminal_kinds_count`
1531 // (issue #372 / lesson #19). Java's grammar wraps every
1532 // if/while/do condition in `parenthesized_expression`, so
1533 // the gap lived in `java_inspect_container`'s terminal-arm
1534 // recognizer: `FieldAccess` (`cfg.flag`), `CastExpression`
1535 // (`(boolean)v`), `ArrayAccess` (`flags[0]`), and
1536 // `InstanceofExpression` (`x instanceof Foo`) were never
1537 // counted. Java has no `await` or `is_pattern` analogues,
1538 // so the C# fix's five-kind set collapses to four here.
1539 //
1540 // expected: 4 conditions (one per `if`), 0 assignments,
1541 // 0 branches (no invocations).
1542 check_metrics::<JavaParser>(
1543 "class Cfg { boolean flag; }
1544 class A {
1545 void m(Object v, boolean[] flags, Cfg cfg) {
1546 if (cfg.flag) { }
1547 if ((boolean) v) { }
1548 if (v instanceof Cfg) { }
1549 if (flags[0]) { }
1550 }
1551 }",
1552 "foo.java",
1553 |metric| {
1554 assert_eq!(metric.abc.conditions_sum(), 4);
1555 assert_eq!(metric.abc.assignments_sum(), 0);
1556 assert_eq!(metric.abc.branches_sum(), 0);
1557 },
1558 );
1559 }
1560
1561 // Issue #1274: a generic *declaration* is type syntax, not a
1562 // decision. `java_count_token_condition` denied only
1563 // `type_arguments`, so `class Gen<T>` and `<U> U ident(U x)` — both
1564 // `type_parameters` — each scored two conditions. The fix counts
1565 // `<` / `>` only under `binary_expression`, the polarity C / C++ /
1566 // Rust / Go already use; a `grammar.json` sweep proves the token is
1567 // emitted from exactly three productions, so the two shapes agree on
1568 // every well-formed input.
1569 //
1570 // One fixture per production the arm must ignore: the class-level
1571 // `type_parameters` (`Gen<T …>`), its `extends` bound's nested
1572 // `type_arguments` (`Comparable<T>`), the method-level
1573 // `type_parameters` (`<U>`), and a nested generic
1574 // (`Map<String, List<T>>`) whose two closing `>` lex as separate
1575 // tokens under separate `type_arguments`. Pre-fix this file scored
1576 // 4 conditions — two per `type_parameters` bracket pair; expected 0,
1577 // the file contains no conditional construct at all.
1578 #[test]
1579 fn java_generic_declarations_are_not_conditions() {
1580 check_metrics::<JavaParser>(
1581 "class Gen<T extends Comparable<T>> {
1582 <U> U ident(U x) { return x; }
1583 List<T> pick(Map<String, List<T>> m) { return m.get(\"k\"); }
1584 }",
1585 "foo.java",
1586 |metric| {
1587 assert_eq!(metric.abc.conditions_sum(), 0);
1588 // Non-vacuity guard: 0 is also what an unparsed file
1589 // scores, so pin a value only a walked body can produce
1590 // — the `m.get("k")` invocation.
1591 assert_eq!(metric.abc.branches_sum(), 1);
1592 },
1593 );
1594 }
1595
1596 // Second half of #1274, same premise and same match: a wildcard type
1597 // argument's `?` is type syntax, not a ternary. tree-sitter-java emits
1598 // a bare `?` from exactly two productions — `ternary_expression` and
1599 // `wildcard` — so the `QMARK` arm carries the same allowlist gate the
1600 // `<` / `>` arm does.
1601 //
1602 // The body carries a real ternary over a real comparison so the
1603 // expected total is 2, not 0. Asserting 0 on a wildcard-only fixture
1604 // would have been vacuous — an unparsable file scores 0 too, so the
1605 // test could not tell "the wildcard was ignored" from "the fixture
1606 // stopped being recognised". Each failure mode now lands on its own
1607 // number: 4 if the wildcard `?` counts again (the pre-fix value), 3
1608 // if the gate is aimed at `Wildcard` instead, 1 if it swallows the
1609 // genuine ternary, 0 if parsing breaks. Two wildcards against one
1610 // ternary is deliberate — with one of each, aiming the gate at the
1611 // wrong one of the two productions still totals 2 and the test
1612 // cannot see the difference.
1613 #[test]
1614 fn java_generic_wildcard_is_not_a_condition() {
1615 check_metrics::<JavaParser>(
1616 "class A {
1617 int m(List<? extends Number> xs, Map<String, ? extends Number> ys,
1618 int a, int b) { return a < b ? 1 : 2; }
1619 }",
1620 "foo.java",
1621 |metric| {
1622 assert_eq!(metric.abc.conditions_sum(), 2);
1623 },
1624 );
1625 }
1626
1627 // The other half of #1274: narrowing the `<` / `>` arm must not
1628 // swallow real comparisons. `List<String>` in the signature keeps a
1629 // generic in scope so the fixture proves both directions at once,
1630 // and the assertion is the grammar-dispatch §8 pin — on the
1631 // method's own space the ABC condition count must equal the
1632 // cyclomatic decision count (`cyclomatic()` minus the per-space
1633 // base of 1). Both are 2, one per `if`.
1634 #[test]
1635 fn java_comparison_operators_still_count_alongside_generics() {
1636 check_func_space::<JavaParser, _>(
1637 "class A {
1638 int m(List<String> xs, int a, int b) {
1639 if (a < b) { return 1; }
1640 if (a > b) { return 2; }
1641 return 0;
1642 }
1643 }",
1644 "foo.java",
1645 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
1646 );
1647 }
1648
1649 #[test]
1650 fn java_constructor_delegation_is_a_branch() {
1651 // Regression for #1279: `super(…)` / `this(…)` parse as
1652 // `explicit_constructor_invocation`, not `method_invocation`, so
1653 // each scored zero branches while Groovy scored one for identical
1654 // source. Both constructors delegate, and neither body contains any
1655 // other call.
1656 // expected: 2 branches — one delegation each.
1657 check_metrics::<JavaParser>(
1658 "class Sub extends Base {
1659 Sub() { super(1); }
1660 Sub(int a) { this(); }
1661 }",
1662 "foo.java",
1663 |metric| {
1664 assert_eq!(metric.abc.branches_sum(), 2);
1665 },
1666 );
1667 }
1668
1669 #[test]
1670 fn java_constructor_delegation_does_not_double_count_arguments() {
1671 // The delegation node does not wrap a `method_invocation` for the
1672 // call itself, so `super(f())` is exactly two branches — the
1673 // delegation and the argument call — not three (#1279).
1674 // expected: 2 branches.
1675 check_metrics::<JavaParser>(
1676 "class Sub extends Base {
1677 Sub() { super(f()); }
1678 }",
1679 "foo.java",
1680 |metric| {
1681 assert_eq!(metric.abc.branches_sum(), 2);
1682 },
1683 );
1684 }
1685
1686 #[test]
1687 fn csharp_constructor_initializer_is_a_branch() {
1688 // C# spells the same delegation as a `constructor_initializer`
1689 // (`: base(…)` / `: this(…)`), which likewise scored zero (#1279).
1690 // expected: 2 branches — one initializer each, no other calls.
1691 check_metrics::<CsharpParser>(
1692 "class Sub : Base {
1693 Sub() : base(1) { }
1694 Sub(int a) : this() { }
1695 }",
1696 "foo.cs",
1697 |metric| {
1698 assert_eq!(metric.abc.branches_sum(), 2);
1699 },
1700 );
1701 }
1702
1703 #[test]
1704 fn kotlin_constructor_delegation_is_a_branch() {
1705 // Kotlin's secondary-constructor delegation is a
1706 // `constructor_delegation_call`, a distinct production from
1707 // `CallExpression`, so it scored zero for the same reason (#1279).
1708 // expected: 1 branch — the `: super(x)` delegation.
1709 check_metrics::<KotlinParser>(
1710 "class Sub : Base {
1711 constructor(x: Int) : super(x) { }
1712 }",
1713 "foo.kt",
1714 |metric| {
1715 assert_eq!(metric.abc.branches_sum(), 1);
1716 },
1717 );
1718 }
1719
1720 #[test]
1721 fn groovy_constructor_delegation_is_a_branch() {
1722 // Groovy already counted this shape before #1279; the assertion
1723 // pins the JVM-family parity the Java and Kotlin fixes restore.
1724 // expected: 1 branch — the `super(1)` delegation.
1725 check_metrics::<GroovyParser>(
1726 "class Sub extends Base {
1727 Sub() { super(1) }
1728 }",
1729 "foo.groovy",
1730 |metric| {
1731 assert_eq!(metric.abc.branches_sum(), 1);
1732 },
1733 );
1734 }
1735
1736 #[test]
1737 fn groovy_no_abc() {
1738 // Comment-only file has no executable code → all-zero ABC.
1739 check_metrics::<GroovyParser>(
1740 "// just a comment, no executable code",
1741 "foo.groovy",
1742 |metric| {
1743 assert_eq!(metric.abc.assignments_sum(), 0);
1744 assert_eq!(metric.abc.branches_sum(), 0);
1745 assert_eq!(metric.abc.conditions_sum(), 0);
1746 },
1747 );
1748 }
1749
1750 #[test]
1751 fn groovy_single_assignment() {
1752 // `int x = 1` is a local-variable declaration whose `=` counts
1753 // as one assignment (matches Java's semantics).
1754 check_metrics::<GroovyParser>("int x = 1", "foo.groovy", |metric| {
1755 assert_eq!(metric.abc.assignments_sum(), 1);
1756 assert_eq!(metric.abc.branches_sum(), 0);
1757 assert_eq!(metric.abc.conditions_sum(), 0);
1758 });
1759 }
1760
1761 #[test]
1762 fn groovy_assignments() {
1763 check_metrics::<GroovyParser>(
1764 "void f() {
1765 int a = 1
1766 int b = 2
1767 a = 3
1768 b = 4
1769 a += 1
1770 b -= 1
1771 }",
1772 "foo.groovy",
1773 |metric| {
1774 // Six `=` tokens total. The two `Final`-less local
1775 // var-decls (`int a = 1`, `int b = 2`) and the two
1776 // bare assignments (`a = 3`, `b = 4`) each contribute
1777 // one assignment via the `EQ` arm; the `+=` / `-=`
1778 // each contribute one via the compound-assign arm.
1779 assert_eq!(metric.abc.assignments_sum(), 6);
1780 },
1781 );
1782 }
1783
1784 #[test]
1785 fn groovy_branches() {
1786 check_metrics::<GroovyParser>(
1787 "void f() {
1788 doStuff()
1789 helper.invoke()
1790 new Worker()
1791 }",
1792 "foo.groovy",
1793 |metric| {
1794 // 2 method invocations + 1 object creation = 3 branches
1795 assert_eq!(metric.abc.branches_sum(), 3);
1796 },
1797 );
1798 }
1799
1800 #[test]
1801 fn groovy_conditions_in_if() {
1802 check_metrics::<GroovyParser>(
1803 "void f(int a) {
1804 if (a == 0) { println(a) }
1805 if (a >= 1) { println(a) }
1806 if (a != 2) { println(a) }
1807 }",
1808 "foo.groovy",
1809 |metric| {
1810 // Three relational ops = 3 conditions
1811 assert_eq!(metric.abc.conditions_sum(), 3);
1812 },
1813 );
1814 }
1815
1816 #[test]
1817 fn groovy_branches_with_juxt_call() {
1818 // Groovy's parens-less call form `println foo` must be counted
1819 // as a branch (`JuxtFunctionCall`).
1820 check_metrics::<GroovyParser>(
1821 "void f() {
1822 println 'hi'
1823 println 'bye'
1824 }",
1825 "foo.groovy",
1826 |metric| {
1827 // 2 juxt calls = 2 branches.
1828 assert_eq!(metric.abc.branches_sum(), 2);
1829 },
1830 );
1831 }
1832
1833 #[test]
1834 fn groovy_try_catch_conditions() {
1835 // Each `try` and `catch` keyword token contributes +1 to
1836 // conditions (mirrors Java).
1837 check_metrics::<GroovyParser>(
1838 "void f() {
1839 try {
1840 risky()
1841 } catch (Exception e) {
1842 handle(e)
1843 }
1844 }",
1845 "foo.groovy",
1846 |metric| {
1847 // try + catch = 2 conditions
1848 assert_eq!(metric.abc.conditions_sum(), 2);
1849 },
1850 );
1851 }
1852
1853 #[test]
1854 fn groovy_ternary_conditions() {
1855 check_metrics::<GroovyParser>(
1856 "void f(int x) {
1857 def y = x > 0 ? 1 : 2
1858 }",
1859 "foo.groovy",
1860 |metric| {
1861 // QMARK alone is +1 condition, plus the `>` condition = 2.
1862 assert_eq!(metric.abc.conditions_sum(), 2);
1863 },
1864 );
1865 }
1866
1867 #[test]
1868 fn groovy_constant_excluded_from_assignments() {
1869 // `final` declarations are not counted as assignments
1870 // (mirrors Java's `Final` handling).
1871 check_metrics::<GroovyParser>(
1872 "class A {
1873 final int CONST = 42
1874 int field = 0
1875 }",
1876 "foo.groovy",
1877 |metric| {
1878 // The `=` on `final int CONST = 42` is a constant
1879 // initialiser (skipped). Only `field = 0` counts.
1880 assert_eq!(metric.abc.assignments_sum(), 1);
1881 },
1882 );
1883 }
1884
1885 #[test]
1886 fn groovy_malformed_parenthesized_no_panic() {
1887 // Regression: malformed Groovy input must not panic the ABC
1888 // walker; the `spaces.rs` Unit fallback (lesson 9) covers
1889 // structural recovery. amaanq's grammar treats `def x = (((`
1890 // as a `local_variable_declaration` whose initialiser is the
1891 // first opening paren — the `=` still fires the assignment
1892 // arm.
1893 check_metrics::<GroovyParser>("def x = (((", "foo.groovy", |metric| {
1894 assert_eq!(metric.abc.assignments_sum(), 1);
1895 });
1896 }
1897
1898 #[test]
1899 fn groovy_bool_returning_terminal_kinds_count() {
1900 // Companion to `csharp_bool_returning_terminal_kinds_count`
1901 // (issue #372 / lesson #19). The dekobon Groovy grammar
1902 // shares Java's wrapping conventions for `FieldAccess` and
1903 // `InstanceofExpression`, but it splits casts into two
1904 // distinct kinds — `cast_expression` for the Groovy-idiomatic
1905 // `v as Boolean` and `parenthesized_type_cast` for the
1906 // Java-style `(boolean) v`. The grammar has no `await` or
1907 // `array_access` analogues, so the C# fix's five-kind set
1908 // collapses to four here (with the cast slot doubled).
1909 //
1910 // expected: 4 conditions (one per `if`), 0 assignments,
1911 // 0 branches (no invocations).
1912 check_metrics::<GroovyParser>(
1913 "class Cfg { boolean flag }
1914 class A {
1915 void m(Object v, Cfg cfg) {
1916 if (cfg.flag) { }
1917 if ((boolean) v) { }
1918 if (v as Boolean) { }
1919 if (v instanceof Cfg) { }
1920 }
1921 }",
1922 "foo.groovy",
1923 |metric| {
1924 assert_eq!(metric.abc.conditions_sum(), 4);
1925 assert_eq!(metric.abc.assignments_sum(), 0);
1926 assert_eq!(metric.abc.branches_sum(), 0);
1927 },
1928 );
1929 }
1930
1931 // Groovy half of #1274 — see `java_generic_declarations_are_not_conditions`.
1932 // The class-level `type_parameters` and its bound's nested
1933 // `type_arguments` mirror the Java fixture; pre-fix this file scored
1934 // 2 conditions, expected 0.
1935 #[test]
1936 fn groovy_generic_declarations_are_not_conditions() {
1937 check_metrics::<GroovyParser>(
1938 "class Gen<T extends Comparable<T>> {
1939 List<T> pick(Map<String, List<T>> m) { m.get(\"k\") }
1940 }",
1941 "foo.groovy",
1942 |metric| {
1943 assert_eq!(metric.abc.conditions_sum(), 0);
1944 // Non-vacuity guard — see the Java counterpart.
1945 assert_eq!(metric.abc.branches_sum(), 1);
1946 },
1947 );
1948 }
1949
1950 // Groovy counterpart of `java_generic_wildcard_is_not_a_condition`,
1951 // including its choice of a non-zero expected total and its
1952 // two-wildcards-one-ternary shape; the dekobon grammar emits the
1953 // same `wildcard` node. Pre-fix: 4.
1954 #[test]
1955 fn groovy_generic_wildcard_is_not_a_condition() {
1956 check_metrics::<GroovyParser>(
1957 "class A {
1958 int m(List<? extends Number> xs, Map<String, ? extends Number> ys,
1959 int a, int b) { a < b ? 1 : 2 }
1960 }",
1961 "foo.groovy",
1962 |metric| {
1963 assert_eq!(metric.abc.conditions_sum(), 2);
1964 },
1965 );
1966 }
1967
1968 // A generic *method* gets its own fixture because the dekobon
1969 // grammar gives it its own production: `def <U> U ident(U x)` emits
1970 // `method_type_parameters`, not the `type_parameters` the class form
1971 // above uses. It is the fourth and last production from which that
1972 // grammar emits a bare `<` / `>` (the others being
1973 // `binary_expression`, `type_arguments` and `type_parameters`), and
1974 // the one a denylist extended only with `TypeParameters` — the fix
1975 // this issue's own plan proposed — would still miss. Revert-verified
1976 // in both directions: under that denylist this test still fails
1977 // while `groovy_generic_declarations_are_not_conditions` passes.
1978 //
1979 // The body carries a ternary over a comparison for the same
1980 // non-vacuity reason as the wildcard tests: expected 2, pre-fix 4
1981 // (the `<U>` bracket pair), 0 if the fixture stops parsing.
1982 #[test]
1983 fn groovy_method_type_parameters_are_not_conditions() {
1984 check_metrics::<GroovyParser>(
1985 "class A {
1986 def <U> U ident(U x, int a, int b) { a < b ? x : null }
1987 }",
1988 "foo.groovy",
1989 |metric| {
1990 assert_eq!(metric.abc.conditions_sum(), 2);
1991 },
1992 );
1993 }
1994
1995 // Groovy counterpart of
1996 // `java_comparison_operators_still_count_alongside_generics`: the
1997 // narrowed arm keeps counting real comparisons, pinned against the
1998 // cyclomatic decision count on the method's own space (§8).
1999 #[test]
2000 fn groovy_elvis_counts_one_condition_per_token() {
2001 // `a ?: c` is a short-circuit decision Groovy cyclomatic already
2002 // counts and the Kotlin ABC arm counts for its identical token;
2003 // Groovy's arm listed neither `?:` nor its `elvis_expression`, so
2004 // a method whose only branching is elvis chains reported
2005 // cyclomatic > 1 with zero conditions. One per token, so the
2006 // grammar-dispatch §8 invariant holds on the chain:
2007 // `conditions() == cyclomatic() - 1`.
2008 check_func_space::<GroovyParser, _>(
2009 "class K { def f(a, c) { return a ?: c } }",
2010 "foo.groovy",
2011 |space| assert_deepest_conditions_match_cyclomatic(&space, 1),
2012 );
2013 check_func_space::<GroovyParser, _>(
2014 "class K { def g(a, b, c) { return a ?: b ?: c } }",
2015 "foo.groovy",
2016 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
2017 );
2018 }
2019
2020 #[test]
2021 fn groovy_comparison_operators_still_count_alongside_generics() {
2022 check_func_space::<GroovyParser, _>(
2023 "class A {
2024 int m(List<String> xs, int a, int b) {
2025 if (a < b) { return 1 }
2026 if (a > b) { return 2 }
2027 return 0
2028 }
2029 }",
2030 "foo.groovy",
2031 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
2032 );
2033 }
2034
2035 #[test]
2036 fn groovy_if_multiple_conditions() {
2037 // Mirrors `java_if_multiple_conditions`: `&&` / `||` chains
2038 // and parenthesised unary forms each contribute one
2039 // condition per primitive comparison; the inspect-container
2040 // pass picks up the unary `!a` / `!b` arguments inside the
2041 // `BinaryExpression` and counts them too.
2042 check_metrics::<GroovyParser>(
2043 "void f(boolean a, boolean b, boolean c) {
2044 if (a || b || c) { println(a) }
2045 if (a && b && c) { println(a) }
2046 if (!a && !b) { println(a) }
2047 }",
2048 "foo.groovy",
2049 |metric| {
2050 // Conditions counted via the AMPAMP/PIPEPIPE arms
2051 // (one count per identifier in the chain — three
2052 // for `||`, three for `&&`, two for the unary chain)
2053 // = 8.
2054 assert_eq!(metric.abc.conditions_sum(), 8);
2055 // Three `println a` juxt calls — each is a branch.
2056 assert_eq!(metric.abc.branches_sum(), 3);
2057 },
2058 );
2059 }
2060
2061 #[test]
2062 fn groovy_while_and_do_while_conditions() {
2063 // Covers the WhileStatement and DoStatement arms in
2064 // `impl Abc for GroovyCode`. Each `while` / `do-while` has
2065 // its condition inspected through `groovy_inspect_container`.
2066 check_metrics::<GroovyParser>(
2067 "void f(boolean a, boolean b) {
2068 while (a) {
2069 a = false
2070 }
2071 do {
2072 b = !b
2073 } while (b)
2074 }",
2075 "foo.groovy",
2076 |metric| {
2077 // `while(a)` + `while(b)` each contribute one condition;
2078 // the unary `!b` on the do body's right-hand side adds
2079 // one more via the assignment-arm inspection = 3.
2080 assert_eq!(metric.abc.conditions_sum(), 3);
2081 // Two assignments to existing variables (`a = false`,
2082 // `b = !b`).
2083 assert_eq!(metric.abc.assignments_sum(), 2);
2084 },
2085 );
2086 }
2087
2088 #[test]
2089 fn groovy_if_while_boolean_literal_condition() {
2090 // Regression for the Groovy half of #371-class bugs: the
2091 // dekobon tree-sitter-groovy grammar wraps a bare
2092 // `true` / `false` literal used as the condition of
2093 // `if` / `while` / `do` / `?:` in a `boolean_literal` node
2094 // (`Groovy::BooleanLiteral`, kind_id 270), not the leaf
2095 // `True` / `False` keyword tokens. `groovy_count_condition`
2096 // must therefore match `BooleanLiteral` (the wrapper).
2097 // Without that, every literal-condition statement silently
2098 // scored 0 conditions. Mirror of
2099 // `csharp_if_while_boolean_literal_condition`.
2100 check_metrics::<GroovyParser>(
2101 "void m() {
2102 if (true) { println 'a' }
2103 if (false) { println 'b' }
2104 while (true) { break }
2105 int t = true ? 1 : 0
2106 }",
2107 "foo.groovy",
2108 |metric| {
2109 // Four literal-condition statements contribute 4
2110 // `BooleanLiteral` conditions (if / if / while /
2111 // ternary), plus the ternary's `?` token adds one
2112 // more via `groovy_count_token_condition` → 5
2113 // total. The `println` calls contribute 2 branches
2114 // (the `while` body's `break` is not a branch).
2115 // The `int t = …` initializer contributes 1
2116 // assignment.
2117 assert_eq!(metric.abc.conditions_sum(), 5);
2118 assert_eq!(metric.abc.branches_sum(), 2);
2119 assert_eq!(metric.abc.assignments_sum(), 1);
2120 },
2121 );
2122 }
2123
2124 #[test]
2125 fn groovy_return_unary_boolean_literal() {
2126 // Companion to `groovy_if_while_boolean_literal_condition`:
2127 // a `!true` / `!false` operand inside a `return` statement
2128 // routes through `groovy_inspect_container` (via
2129 // `groovy_inspect_child(node, 1)` on the ReturnStatement).
2130 // The `!` operator establishes boolean context, then the
2131 // innermost-operand check matches `BooleanLiteral` — that
2132 // helper's `BooleanLiteral` arm must be present or the
2133 // count silently drops. Mutation-verified: removing
2134 // `BooleanLiteral` from `groovy_inspect_container` leaves
2135 // every other Groovy test passing.
2136 check_metrics::<GroovyParser>(
2137 "boolean f() {
2138 return !true
2139 }
2140 boolean g() {
2141 return !false
2142 }",
2143 "foo.groovy",
2144 |metric| {
2145 // Each `return !X` walks into
2146 // `groovy_inspect_container` with a UnaryExpression
2147 // wrapping a `BANG` + BooleanLiteral. The `!` arm
2148 // seeds `has_boolean_content = true` (ReturnStatement
2149 // is not a known-boolean parent), then the
2150 // BooleanLiteral operand contributes one condition.
2151 // Two `return !X` → 2 conditions, no branches, no
2152 // assignments.
2153 assert_eq!(metric.abc.conditions_sum(), 2);
2154 assert_eq!(metric.abc.branches_sum(), 0);
2155 assert_eq!(metric.abc.assignments_sum(), 0);
2156 },
2157 );
2158 }
2159
2160 #[test]
2161 fn groovy_short_circuit_with_boolean_literal_operand() {
2162 // Companion to `groovy_if_while_boolean_literal_condition`:
2163 // a bare `true` / `false` operand of `&&` / `||` lands in
2164 // `groovy_count_unary_conditions`, which iterates the
2165 // parent BinaryExpression's children. That helper must
2166 // match the `BooleanLiteral` wrapper just like
2167 // `groovy_count_condition` does — otherwise the operand
2168 // silently scores zero. Mutation-verified: removing
2169 // `BooleanLiteral` from the `groovy_count_unary_conditions`
2170 // arm leaves every other Groovy test passing.
2171 check_metrics::<GroovyParser>(
2172 "void m(boolean x) {
2173 if (x && true) { println 'a' }
2174 if (false || x) { println 'b' }
2175 }",
2176 "foo.groovy",
2177 |metric| {
2178 // `&&` and `||` themselves are NOT in
2179 // `groovy_count_token_condition`'s match list —
2180 // they route through
2181 // `groovy_walk_for_conditions::AMPAMP|PIPEPIPE`,
2182 // which calls `groovy_count_unary_conditions` on
2183 // the parent BinaryExpression. Each invocation
2184 // counts every child that matches the terminal-
2185 // operand kinds and whose parent is a
2186 // BinaryExpression. For `x && true`: Identifier x
2187 // (+1) + BooleanLiteral true (+1) = 2. For
2188 // `false || x`: BooleanLiteral false (+1) +
2189 // Identifier x (+1) = 2. Total 4.
2190 assert_eq!(metric.abc.conditions_sum(), 4);
2191 assert_eq!(metric.abc.branches_sum(), 2);
2192 assert_eq!(metric.abc.assignments_sum(), 0);
2193 },
2194 );
2195 }
2196
2197 #[test]
2198 fn groovy_methods_arguments_with_conditions() {
2199 // Mirror of `java_methods_arguments_with_conditions`: a
2200 // unary `!x` inside an argument list must count both the
2201 // method invocation as a branch AND the unary as a
2202 // condition. The `ArgumentList | ArgumentList2` arm in
2203 // `impl Abc for GroovyCode` is what exercises this.
2204 check_metrics::<GroovyParser>(
2205 "void f(boolean a, boolean b, boolean c) {
2206 m1(a)
2207 m1(!a)
2208 m2(!a, !b)
2209 }",
2210 "foo.groovy",
2211 |metric| {
2212 // 3 method invocations (m1, m1, m2) — each fires the
2213 // branches arm.
2214 assert_eq!(metric.abc.branches_sum(), 3);
2215 // Three `!` unaries — `m1(!a)` and the two args of
2216 // `m2(!a, !b)` — each contribute one condition via
2217 // the ArgumentList inspection.
2218 assert_eq!(metric.abc.conditions_sum(), 3);
2219 },
2220 );
2221 }
2222
2223 #[test]
2224 fn groovy_return_with_conditions() {
2225 // Mirror of `java_return_with_conditions`: a parenthesised
2226 // or unary expression inside `return` flows through the
2227 // `ReturnStatement` arm to `groovy_inspect_container`.
2228 check_metrics::<GroovyParser>(
2229 "boolean f(boolean a) {
2230 return (a)
2231 }
2232 boolean g(boolean a) {
2233 return !a
2234 }",
2235 "foo.groovy",
2236 |metric| {
2237 // Only one of the two return forms surfaces a
2238 // condition: `return !a` hits the UnaryExpression
2239 // path and adds one; `return (a)` reaches
2240 // `groovy_inspect_container` but the inner
2241 // identifier `a` is not in a boolean-context-firing
2242 // parent, so no condition is added.
2243 assert_eq!(metric.abc.conditions_sum(), 1);
2244 },
2245 );
2246 }
2247
2248 #[test]
2249 fn groovy_for_with_variable_declaration() {
2250 // Classical `for (int i = 0; cond; i++)` form. The init
2251 // slot's `int i = 0` is suppressed from assignments by the
2252 // `LocalVariableDeclaration` push/pop dance; the `i++` in
2253 // the update slot contributes one assignment via the
2254 // `PLUSPLUS` arm. The condition `i < 10` flows through the
2255 // `ForStatement` arm.
2256 check_metrics::<GroovyParser>(
2257 "void f() {
2258 for (int i = 0; i < 10; i++) {
2259 println(i)
2260 }
2261 }",
2262 "foo.groovy",
2263 |metric| {
2264 // `int i = 0` fires the EQ arm + `i++` fires the
2265 // PLUSPLUS arm = 2 assignments.
2266 assert_eq!(metric.abc.assignments_sum(), 2);
2267 // `i < 10` is one condition (the LT arm).
2268 assert_eq!(metric.abc.conditions_sum(), 1);
2269 },
2270 );
2271 }
2272
2273 /// The existing `for` test uses `i < 10`, which the `LT` token arm
2274 /// counts on its own — `groovy_walk_for_statement` never
2275 /// contributes there, so it stayed uncovered. A bare-identifier
2276 /// condition has no comparison token, so the count can only come
2277 /// from the walker.
2278 #[test]
2279 fn groovy_for_with_bare_identifier_condition() {
2280 check_metrics::<GroovyParser>(
2281 "void f(boolean go) {
2282 for (int i = 0; go; i++) {
2283 println(i)
2284 }
2285 }",
2286 "foo.groovy",
2287 |metric| {
2288 // `go` is the whole condition and counts once.
2289 assert_eq!(metric.abc.conditions_sum(), 1);
2290 // `int i = 0` (EQ) + `i++` (PLUSPLUS) = 2, as in
2291 // `groovy_for_with_variable_declaration`.
2292 assert_eq!(metric.abc.assignments_sum(), 2);
2293 },
2294 );
2295 }
2296
2297 /// The same slot with the initialiser hoisted out of the header.
2298 /// Under the pre-#1276 positional cascade this was a distinct code
2299 /// path — the condition moved from child(4) to child(3) — and the
2300 /// pair is kept as a shape guard now that the walker reads the
2301 /// `condition` field and cannot see the difference.
2302 #[test]
2303 fn groovy_for_with_empty_initializer_counts_the_condition() {
2304 check_metrics::<GroovyParser>(
2305 "void f(boolean go) {
2306 int i = 0
2307 for (; go; i++) {
2308 println(i)
2309 }
2310 }",
2311 "foo.groovy",
2312 |metric| {
2313 assert_eq!(metric.abc.conditions_sum(), 1);
2314 // `int i = 0` (EQ) + `i++` (PLUSPLUS), as above — the
2315 // initialiser just moved out of the loop header.
2316 assert_eq!(metric.abc.assignments_sum(), 2);
2317 },
2318 );
2319 }
2320
2321 /// Issue #1276 changed Groovy's answer here, exactly as it changed
2322 /// Java's: the positional cascade counted a `;` / `)` landing at
2323 /// child(4) as a vacuously-true condition, so `for (;;)` scored
2324 /// one. An omitted test is not a decision, and every other impl
2325 /// scores it zero. See `java_empty_for_condition_counts_nothing`.
2326 #[test]
2327 fn groovy_empty_for_condition_counts_nothing() {
2328 check_metrics::<GroovyParser>("void f() { for (;;) { break } }", "foo.groovy", |metric| {
2329 assert_eq!(metric.abc.conditions_sum(), 0);
2330 });
2331 // The second spelling that moved. Unlike Java's, Groovy's
2332 // `local_variable_declaration` does not swallow its `;`, so the
2333 // old cascade found `;` at child(3) and `;` at child(4) here
2334 // too, and counted one.
2335 check_metrics::<GroovyParser>(
2336 "void f() { for (int i = 0; ; i++) { break } }",
2337 "foo.groovy",
2338 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
2339 );
2340 }
2341
2342 /// The cascade's other defect, shared with Java: a comment in the
2343 /// header shifted every child index, so the condition went unread.
2344 /// Reading the `condition` field cannot shift.
2345 #[test]
2346 fn groovy_for_condition_survives_a_header_comment() {
2347 check_metrics::<GroovyParser>(
2348 "void f(boolean go) { for (; /* n */ go; ) { break } }",
2349 "foo.groovy",
2350 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
2351 );
2352 // Unchanged control: the same loop without the comment.
2353 check_metrics::<GroovyParser>(
2354 "void f(boolean go) { for (; go; ) { break } }",
2355 "foo.groovy",
2356 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
2357 );
2358 }
2359
2360 /// C#'s `csharp_walk_for_statement` reads the loop condition off
2361 /// the named `condition` field and routes a parenthesised or
2362 /// `!`-prefixed one through `csharp_inspect_container`. Every other
2363 /// C# `for` test uses a comparison (`i < n`), which the `LT` token
2364 /// arm counts without entering the walker.
2365 #[test]
2366 fn csharp_for_with_negated_condition() {
2367 check_metrics::<CsharpParser>(
2368 "class A {
2369 void M(bool done) {
2370 for (int i = 0; !done; i++) { System.Console.WriteLine(i); }
2371 }
2372 }",
2373 "foo.cs",
2374 |metric| {
2375 // `!done` unwraps to the `done` terminal: one condition,
2376 // and no comparison token to double-count it.
2377 assert_eq!(metric.abc.conditions_sum(), 1);
2378 // `int i = 0` + `i++`.
2379 assert_eq!(metric.abc.assignments_sum(), 2);
2380 assert_eq!(metric.abc.branches_sum(), 1);
2381 },
2382 );
2383 }
2384
2385 #[test]
2386 fn groovy_eq_arm_counts_outside_final_declarations() {
2387 // Bare reassignment of an already-declared variable: the `=`
2388 // belongs to no `final` declaration, so it counts. Mirrors
2389 // `java_eq_arm_counts_outside_final_declarations`.
2390 check_metrics::<GroovyParser>(
2391 "void f(int x) {
2392 x = 42
2393 }",
2394 "foo.groovy",
2395 |metric| {
2396 assert_eq!(metric.abc.assignments_sum(), 1);
2397 assert_eq!(metric.abc.branches_sum(), 0);
2398 assert_eq!(metric.abc.conditions_sum(), 0);
2399 },
2400 );
2401 }
2402
2403 #[test]
2404 fn groovy_final_field_initializer_does_not_suppress_the_closure_body() {
2405 // The Groovy spelling of
2406 // `java_final_initializer_does_not_suppress_the_assignments_inside_it`:
2407 // a `final` field's closure body opens no space, and the sentinel
2408 // stack suppressed its `x = 1` with the declarator's own `=`.
2409 check_metrics::<GroovyParser>(
2410 "class K {
2411 int x
2412 final Closure c = { x = 1 }
2413 Closure d = { x = 2 }
2414 final int q = 3
2415 }",
2416 "foo.groovy",
2417 |metric| {
2418 // `x = 1`, `d = {…}`, `x = 2`; the two `final`
2419 // initializers are suppressed. Pre-fix: 2.
2420 assert_eq!(metric.abc.assignments_sum(), 3);
2421 },
2422 );
2423 }
2424
2425 #[test]
2426 fn groovy_final_local_is_an_error_at_the_pinned_grammar() {
2427 // `groovy_eq_initializes_final_binding` lists
2428 // `local_variable_declaration` for symmetry with Java, but at
2429 // dekobon-tree-sitter-groovy 0.2.2 a `final` local never
2430 // reaches it: the parser emits an `ERROR` node for the
2431 // declaration, with or without a terminator, so both of its `=`
2432 // tokens count. This pins the grammar limitation so a bump that
2433 // starts parsing the local shows up here rather than as a silent
2434 // change in what the predicate suppresses.
2435 let source = b"class K { int x; def m() { final int a = 0; x = 1 } }";
2436 let parser = GroovyParser::new(
2437 source.to_vec(),
2438 &std::path::PathBuf::from("foo.groovy"),
2439 None,
2440 );
2441 assert!(
2442 ast_has_kind_id(&parser, u16::MAX),
2443 "a `final` local should still parse to an ERROR node; if the grammar \
2444 now accepts it, re-derive the local half of the predicate",
2445 );
2446 check_metrics::<GroovyParser>(
2447 "class K { int x; def m() { final int a = 0; x = 1 } }",
2448 "foo.groovy",
2449 |metric| {
2450 assert_eq!(metric.abc.assignments_sum(), 2);
2451 },
2452 );
2453 }
2454
2455 #[test]
2456 fn csharp_const_initializer_shapes() {
2457 // `csharp_eq_initializes_const_binding` reads the `const`
2458 // through the `modifier` node one hop above the
2459 // `variable_declaration`, for a local and a field alike, behind
2460 // other modifiers; `readonly` is not `const` and its initializer
2461 // counts. A lambda's body opens its own space in C#, so unlike
2462 // Java there was never a nested `=` to leak — the row pins that
2463 // the structural rule keeps the count the sentinel gave.
2464 check_metrics::<CsharpParser>(
2465 "class K {
2466 int x;
2467 private const int Q = 1;
2468 public static readonly int R = 2;
2469 void M() { const int q = 3; System.Action a = () => { x = 4; }; }
2470 }",
2471 "foo.cs",
2472 |metric| {
2473 // `R = 2`, `a = () => …`, `x = 4`.
2474 assert_eq!(metric.abc.assignments_sum(), 3);
2475 },
2476 );
2477 }
2478
2479 #[test]
2480 fn csharp_constant_declarations() {
2481 check_metrics::<CsharpParser>(
2482 "class A {
2483 private const int X1 = 0, Y1 = 0;
2484 public const float PI = 3.14f;
2485 const string HELLO = \"Hello,\";
2486 protected string world = \" world!\";
2487 public float e = 2.718f;
2488 private int x2 = 1, y2 = 2;
2489 void M() {
2490 const int Z1 = 0, Z2 = 0, Z3 = 0;
2491 const float T = 0.0f;
2492 int z1 = 1, z2 = 2, z3 = 3;
2493 }
2494 }",
2495 "foo.cs",
2496 |metric| insta::assert_json_snapshot!(metric.abc),
2497 );
2498 }
2499
2500 #[test]
2501 fn csharp_declarations_with_conditions() {
2502 check_metrics::<CsharpParser>(
2503 "class A {
2504 bool a = (1 == 2);
2505 bool b = (1 < 2);
2506 bool c = !true;
2507 bool d = !false;
2508 }",
2509 "foo.cs",
2510 |metric| insta::assert_json_snapshot!(metric.abc),
2511 );
2512 }
2513
2514 #[test]
2515 fn csharp_assignments_with_conditions() {
2516 check_metrics::<CsharpParser>(
2517 "class A {
2518 void M() {
2519 int a = 0;
2520 a += 1;
2521 a -= 2;
2522 a *= 3;
2523 a /= 4;
2524 a %= 5;
2525 a++;
2526 a--;
2527 }
2528 }",
2529 "foo.cs",
2530 |metric| insta::assert_json_snapshot!(metric.abc),
2531 );
2532 }
2533
2534 #[test]
2535 fn csharp_methods_arguments_with_conditions() {
2536 check_metrics::<CsharpParser>(
2537 "class A {
2538 void M(int x, int y) {
2539 F(x == y, x < y, !x.Equals(y));
2540 }
2541 void F(bool a, bool b, bool c) {}
2542 }",
2543 "foo.cs",
2544 |metric| insta::assert_json_snapshot!(metric.abc),
2545 );
2546 }
2547
2548 #[test]
2549 fn csharp_if_single_conditions() {
2550 check_metrics::<CsharpParser>(
2551 "class A {
2552 void M(int x) {
2553 if (x > 0) { System.Console.WriteLine(\"a\"); }
2554 if (x < 0) { System.Console.WriteLine(\"b\"); }
2555 if (x == 0) { System.Console.WriteLine(\"c\"); }
2556 }
2557 }",
2558 "foo.cs",
2559 |metric| insta::assert_json_snapshot!(metric.abc),
2560 );
2561 }
2562
2563 #[test]
2564 fn csharp_if_multiple_conditions() {
2565 check_metrics::<CsharpParser>(
2566 "class A {
2567 void M(int x, int y) {
2568 if (x > 0 && y > 0) { System.Console.WriteLine(\"a\"); }
2569 if (x < 0 || y < 0) { System.Console.WriteLine(\"b\"); }
2570 }
2571 }",
2572 "foo.cs",
2573 |metric| insta::assert_json_snapshot!(metric.abc),
2574 );
2575 }
2576
2577 #[test]
2578 fn csharp_while_and_do_while_conditions() {
2579 check_metrics::<CsharpParser>(
2580 "class A {
2581 void M(int x) {
2582 while (x > 0) { x--; }
2583 do { x++; } while (x < 10);
2584 }
2585 }",
2586 "foo.cs",
2587 |metric| insta::assert_json_snapshot!(metric.abc),
2588 );
2589 }
2590
2591 #[test]
2592 fn csharp_return_with_conditions() {
2593 check_metrics::<CsharpParser>(
2594 "class A {
2595 bool M(int x) {
2596 return (x > 0);
2597 }
2598 bool N(int x) {
2599 return !(x < 0);
2600 }
2601 }",
2602 "foo.cs",
2603 |metric| insta::assert_json_snapshot!(metric.abc),
2604 );
2605 }
2606
2607 // C# `switch` *expression* arms scored zero ABC conditions before
2608 // #456 — they carry no `case` / `default` token, so the token-driven
2609 // `csharp_count_token_condition` never saw them, even though C#
2610 // cyclomatic counts each non-discard arm. Revert-verified: adding the
2611 // gated `SwitchExpressionArm` arm is what lifts this from 0 to 2. The
2612 // bare `_ =>` discard arm is excluded (the `default:` analogue),
2613 // mirroring the cyclomatic gate (lesson 11).
2614 #[test]
2615 fn csharp_switch_expression_arm_counts_condition() {
2616 check_metrics::<CsharpParser>(
2617 "class A {
2618 int M(int x) {
2619 return x switch { 1 => 10, 2 => 20, _ => 0 };
2620 }
2621 }",
2622 "foo.cs",
2623 |metric| {
2624 // arm `1 =>` (+1) + arm `2 =>` (+1) + `_ =>` discard (+0).
2625 assert_eq!(metric.abc.conditions_sum(), 2);
2626 },
2627 );
2628 }
2629
2630 // Cross-language parity (lesson 11): a C# `switch` expression and the
2631 // equivalent Java arrow-`switch` must report the same ABC condition
2632 // count on equivalent code. Both have two concrete case arms and no
2633 // fallback arm, so both must count exactly 2. `check_metrics` takes a
2634 // non-capturing `fn` pointer, so the shared expected value (2) is
2635 // asserted in each callback rather than compared across closures; the
2636 // matching constant is what enforces parity. This guards against the
2637 // C# fix drifting away from the Java arrow-case treatment.
2638 #[test]
2639 fn csharp_java_switch_arm_abc_parity() {
2640 // C# switch expression: two arms, no fallback → 2 conditions.
2641 check_metrics::<CsharpParser>(
2642 "class A {
2643 int M(int x) {
2644 return x switch { 1 => 10, 2 => 20 };
2645 }
2646 }",
2647 "foo.cs",
2648 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2649 );
2650
2651 // Equivalent Java arrow-`switch`: two case arms, no default → 2.
2652 check_metrics::<JavaParser>(
2653 "class A {
2654 int m(int x) {
2655 return switch (x) { case 1 -> 10; case 2 -> 20; };
2656 }
2657 }",
2658 "foo.java",
2659 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2660 );
2661 }
2662
2663 // Issue #469: the `default` arm of a C-family `switch` is the
2664 // unconditional fallthrough and must NOT count as an ABC condition,
2665 // mirroring cyclomatic — which counts only the `Case` arms, never
2666 // the `Default` token.
2667 //
2668 // expected: each fixture is a single function whose switch has two
2669 // concrete `case` arms plus one `default`. ABC must count exactly
2670 // the two case arms (conditions = 2), matching cyclomatic's two
2671 // case-arm decisions. Pre-fix, every language below scored 3 (the
2672 // `Default` token leaked into the condition tally) — revert-verified
2673 // against the pre-#469 condition arms. We anchor on the integer
2674 // `conditions_sum()` headline (the value the public JSON serializes;
2675 // float magnitude is bit-brittle and excluded by the snapshot
2676 // policy). The cyclomatic side is pinned separately in
2677 // `java_csharp_cpp_switch_default_cyclomatic_parity` below, where
2678 // the per-space `cyclomatic()` decision count is isolated.
2679 #[test]
2680 fn java_switch_default_not_a_condition() {
2681 // Classic statement `default:`.
2682 check_metrics::<JavaParser>(
2683 "class A {
2684 int m(int x) {
2685 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2686 }
2687 }",
2688 "foo.java",
2689 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2690 );
2691 // Arrow `default ->` — shares the same `Default` token.
2692 check_metrics::<JavaParser>(
2693 "class A {
2694 int m(int x) {
2695 return switch (x) { case 1 -> 1; case 2 -> 2; default -> 0; };
2696 }
2697 }",
2698 "foo.java",
2699 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2700 );
2701 }
2702
2703 // Over-exclusion guard (issue #469): a statement `switch` with two
2704 // `case` arms and NO `default` must still count both cases. This
2705 // pins that the fix excludes only the `Default` token, never a
2706 // `Case` arm — the count is identical before and after #469 (two
2707 // cases → 2), so it would catch a fix that over-eagerly dropped a
2708 // real case (e.g. treating the trailing case as a fallthrough).
2709 // expected: case 1 (+1) + case 2 (+1) = 2.
2710 #[test]
2711 fn java_switch_without_default_counts_all_cases() {
2712 check_metrics::<JavaParser>(
2713 "class A {
2714 int m(int x) {
2715 switch (x) { case 1: return 1; case 2: return 2; }
2716 return -1;
2717 }
2718 }",
2719 "foo.java",
2720 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2721 );
2722 }
2723
2724 #[test]
2725 fn csharp_switch_default_not_a_condition() {
2726 check_metrics::<CsharpParser>(
2727 "class A {
2728 int M(int x) {
2729 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2730 }
2731 }",
2732 "foo.cs",
2733 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2734 );
2735 }
2736
2737 #[test]
2738 fn cpp_switch_default_not_a_condition() {
2739 // C++ (and plain C, which shares this grammar) already excluded
2740 // `default`; this pins the cross-language parity invariant.
2741 check_metrics::<CppParser>(
2742 "void f(int x) {
2743 switch (x) { case 1: return; case 2: return; default: return; }
2744 }",
2745 "foo.cpp",
2746 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2747 );
2748 }
2749
2750 #[test]
2751 fn objc_abc() {
2752 // ObjC ABC reuses the C/C++ walker with two additions: a message
2753 // send `[obj msg]` is a call (B), and `@try` / `@catch` count as
2754 // conditions (C) like C++ try/catch.
2755 // A: `int total = 0`, `int i = 0`, `i++`, `total = total + …`,
2756 // `total = -1` = 5.
2757 // B: `[self valueAt:i]`, `[self risky]` = 2 message sends.
2758 // C: `i < n`, `@try`, `@catch`, `total >= 0` = 4.
2759 check_metrics::<ObjcParser>(
2760 "@implementation Foo\n\
2761 - (int)bar:(int)n {\n\
2762 int total = 0;\n\
2763 for (int i = 0; i < n; i++) {\n\
2764 total = total + [self valueAt:i];\n\
2765 }\n\
2766 @try {\n\
2767 [self risky];\n\
2768 } @catch (NSException *e) {\n\
2769 total = -1;\n\
2770 }\n\
2771 if (total >= 0) {\n\
2772 return total;\n\
2773 }\n\
2774 return 0;\n\
2775 }\n\
2776 @end\n",
2777 "foo.m",
2778 |metric| {
2779 assert_eq!(metric.abc.assignments_sum(), 5);
2780 assert_eq!(metric.abc.branches_sum(), 2);
2781 assert_eq!(metric.abc.conditions_sum(), 4);
2782 },
2783 );
2784 }
2785
2786 #[test]
2787 fn objc_abc_conditions() {
2788 // Exercises the condition-slot arms shared with C/C++: a `while`
2789 // head, a `&&` chain, a `do … while` trailing condition, and a
2790 // `return <comparison>`. ObjC routes these through the same
2791 // grammar-agnostic `cpp_inspect_*` helpers.
2792 // A: `p++`, `p--` = 2. B: no calls = 0.
2793 // C: `p != 0`, `*p > 0`, `*p < 9`, `*p == 0` = 4.
2794 check_metrics::<ObjcParser>(
2795 "@implementation Foo\n\
2796 - (int)g:(int *)p {\n\
2797 while (p != 0 && *p > 0) {\n\
2798 p++;\n\
2799 }\n\
2800 do {\n\
2801 p--;\n\
2802 } while (*p < 9);\n\
2803 return *p == 0;\n\
2804 }\n\
2805 @end\n",
2806 "foo.m",
2807 |metric| {
2808 assert_eq!(metric.abc.assignments_sum(), 2);
2809 assert_eq!(metric.abc.branches_sum(), 0);
2810 assert_eq!(metric.abc.conditions_sum(), 4);
2811 },
2812 );
2813 }
2814
2815 #[test]
2816 fn objc_abc_message_send_unary_condition() {
2817 // A negated boolean passed as a message-send argument is a unary
2818 // condition (Fitzpatrick Rule 9), the same as in a C-call argument.
2819 // Message args are direct children of `message_expression` (no
2820 // `argument_list`), so they are inspected in the `MessageExpression`
2821 // arm. Here: `[self use:!a]` (1 call + 1 unary condition) +
2822 // `cFunc(!a)` (1 call + 1 unary condition) → B=2, C=2.
2823 check_metrics::<ObjcParser>(
2824 "@implementation Foo\n\
2825 - (void)bar:(int)a {\n\
2826 [self use:!a];\n\
2827 cFunc(!a);\n\
2828 }\n\
2829 @end\n",
2830 "foo.m",
2831 |metric| {
2832 assert_eq!(metric.abc.branches_sum(), 2);
2833 assert_eq!(metric.abc.conditions_sum(), 2);
2834 },
2835 );
2836 }
2837
2838 #[test]
2839 fn objc_message_send_is_a_bool_terminal_in_condition_slots() {
2840 // `[obj ok]` is Objective-C's call, so in a condition slot it is
2841 // the unary condition `ok()` is (Fitzpatrick Rule 9). Until
2842 // `message_expression` joined `cpp_bool_terminal_kinds!` every
2843 // row here scored zero conditions where its C-call twin scored
2844 // one — and #1276's `for` slot inherited the gap. Each row is a
2845 // message send as the *whole* slot (grammar-dispatch §11: a
2846 // comparison would be counted by its own operator arm anyway),
2847 // and branches are asserted beside conditions so the arm cannot
2848 // be read as counting the call twice. The last three are the
2849 // over-count guards: a value position counts nothing, as `ok()`'s
2850 // does not.
2851 let cases = [
2852 ("if ([self ok]) {}", 1, 1),
2853 ("while (![self ok]) {}", 1, 1),
2854 ("do {} while ([self ok]);", 1, 1),
2855 ("for (; [self ok]; ) {}", 1, 1),
2856 ("x = [self ok] ? 1 : 2;", 2, 1),
2857 ("if ([self ok] && [o ok]) {}", 2, 2),
2858 ("return [self ok];", 0, 1),
2859 ("[self use:[self ok]];", 0, 2),
2860 ("[self use:![self ok]];", 1, 2),
2861 ];
2862 let mut ran = 0;
2863 for (body, conditions, branches) in cases {
2864 let src = format!("@implementation Foo\n- (int)bar {{\n {body}\n}}\n@end\n");
2865 let abc = metrics_verbatim(
2866 LANG::Objc,
2867 src.as_bytes(),
2868 MetricsOptions::default().with_only(&[crate::Metric::Abc]),
2869 )
2870 .abc;
2871 assert_eq!(abc.conditions_sum(), conditions, "`{body}` conditions");
2872 assert_eq!(abc.branches_sum(), branches, "`{body}` branches");
2873 ran += 1;
2874 }
2875 assert_eq!(ran, cases.len());
2876 // Both answers are present, so a walker stuck at 0 or at 1 cannot
2877 // pass half the table silently.
2878 assert!(cases.iter().any(|&(_, c, _)| c == 0));
2879 assert!(cases.iter().any(|&(_, c, _)| c == 2));
2880 }
2881
2882 #[test]
2883 fn objc_message_send_condition_agrees_with_c_call() {
2884 // The intra-ObjC parity C++ cannot express: a message send and a
2885 // C call in the same slot score alike. Non-degenerate by the
2886 // `assert_eq!(…, 1)` on the reference.
2887 for (send, call) in [
2888 ("if ([a ok]) {}", "if (ok()) {}"),
2889 ("for (; [a ok]; ) {}", "for (; ok(); ) {}"),
2890 ("x = [a ok] ? 1 : 2;", "x = ok() ? 1 : 2;"),
2891 ] {
2892 let wrap =
2893 |body: &str| format!("@implementation Foo\n- (int)bar {{\n {body}\n}}\n@end\n");
2894 let reference = abc_conditions(LANG::Objc, &wrap(call));
2895 assert!(reference >= 1, "`{call}` must count at least the slot");
2896 assert_eq!(
2897 abc_conditions(LANG::Objc, &wrap(send)),
2898 reference,
2899 "`{send}`"
2900 );
2901 }
2902 }
2903
2904 #[test]
2905 fn groovy_switch_default_not_a_condition() {
2906 check_metrics::<GroovyParser>(
2907 "class A {
2908 int m(int x) {
2909 switch (x) { case 1: return 1; case 2: return 2; default: return 0 }
2910 }
2911 }",
2912 "foo.groovy",
2913 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2914 );
2915 }
2916
2917 #[test]
2918 fn js_switch_default_not_a_condition() {
2919 check_metrics::<JavascriptParser>(
2920 "function f(x) {
2921 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2922 }",
2923 "foo.js",
2924 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2925 );
2926 }
2927
2928 #[test]
2929 fn ts_switch_default_not_a_condition() {
2930 check_metrics::<TypescriptParser>(
2931 "function f(x: number): number {
2932 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2933 }",
2934 "foo.ts",
2935 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2936 );
2937 }
2938
2939 // Cross-language parity (lesson 11): the equivalent statement-`switch`
2940 // with a `default` arm reports the same ABC condition count across
2941 // Java / C# / C++. All three have two concrete case arms plus a
2942 // fallthrough `default`, so all three must count exactly 2 conditions
2943 // (the `default` excluded). `check_metrics` takes a non-capturing
2944 // `fn` pointer, so the shared expected value is asserted in each
2945 // callback; the matching constant is what enforces parity.
2946 #[test]
2947 fn java_csharp_cpp_switch_default_abc_parity() {
2948 check_metrics::<JavaParser>(
2949 "class A {
2950 int m(int x) {
2951 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2952 }
2953 }",
2954 "foo.java",
2955 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2956 );
2957 check_metrics::<CsharpParser>(
2958 "class A {
2959 int M(int x) {
2960 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2961 }
2962 }",
2963 "foo.cs",
2964 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2965 );
2966 check_metrics::<CppParser>(
2967 "void f(int x) {
2968 switch (x) { case 1: return; case 2: return; default: return; }
2969 }",
2970 "foo.cpp",
2971 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
2972 );
2973 }
2974
2975 // Pins the ABC-vs-cyclomatic agreement the fix is about (lesson 11):
2976 // on the method's own function space, the cyclomatic decision count
2977 // (`cyclomatic()` minus the per-space base of 1) must equal the ABC
2978 // `conditions()` for the same switch. Both must be 2 — the two case
2979 // arms — with the `default` excluded from each. Revert-verified: pre-
2980 // #469 ABC `conditions()` was 3 here while cyclomatic stayed at 2.
2981 #[test]
2982 fn java_csharp_cpp_switch_default_cyclomatic_parity() {
2983 check_func_space::<JavaParser, _>(
2984 "class A {
2985 int m(int x) {
2986 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2987 }
2988 }",
2989 "foo.java",
2990 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
2991 );
2992 check_func_space::<CsharpParser, _>(
2993 "class A {
2994 int M(int x) {
2995 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
2996 }
2997 }",
2998 "foo.cs",
2999 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3000 );
3001 check_func_space::<CppParser, _>(
3002 "void f(int x) {
3003 switch (x) { case 1: return; case 2: return; default: return; }
3004 }",
3005 "foo.cpp",
3006 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3007 );
3008 }
3009
3010 // Issue #473: PHP `switch` `default:` (`DefaultStatement`) is the
3011 // unconditional fallthrough, not a condition. ABC `conditions()` must
3012 // equal the cyclomatic decision count (`cyclomatic() - 1`) on the
3013 // function's own space — both 2 for the two `case` arms, with the
3014 // `default` excluded. Revert-verified: re-adding `DefaultStatement` to
3015 // the PHP ABC condition arm makes `conditions()` 3 here while cyclomatic
3016 // stays at 2, failing the invariant.
3017 #[test]
3018 fn php_switch_default_not_a_condition() {
3019 check_func_space::<PhpParser, _>(
3020 "<?php
3021 function f($x) {
3022 switch ($x) {
3023 case 1: return 1;
3024 case 2: return 2;
3025 default: return 0;
3026 }
3027 }",
3028 "foo.php",
3029 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3030 );
3031 }
3032
3033 // Issue #473: PHP `match` `default =>` (`MatchDefaultExpression`) is the
3034 // unconditional fallthrough, mirroring the switch `default:` case above.
3035 // ABC `conditions()` must equal the cyclomatic decision count
3036 // (`cyclomatic() - 1`) — both 2 for the two non-default match arms.
3037 // Revert-verified: re-adding `MatchDefaultExpression` to the PHP ABC
3038 // condition arm makes `conditions()` 3 here while cyclomatic stays at 2.
3039 #[test]
3040 fn php_match_default_not_a_condition() {
3041 check_func_space::<PhpParser, _>(
3042 "<?php
3043 function g($x) {
3044 return match ($x) {
3045 1 => \"a\",
3046 2 => \"b\",
3047 default => \"z\",
3048 };
3049 }",
3050 "foo.php",
3051 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3052 );
3053 }
3054
3055 #[test]
3056 fn csharp_if_bare_identifier_condition() {
3057 check_metrics::<CsharpParser>(
3058 "class A {
3059 void M(bool x) {
3060 if (x) { System.Console.WriteLine(\"a\"); }
3061 }
3062 }",
3063 "foo.cs",
3064 |metric| {
3065 // `if (x)` contributes 1 condition (bare identifier).
3066 // `System.Console.WriteLine(...)` is the only call → 1 branch.
3067 // `*_sum()` is what the public JSON serializes as the
3068 // headline value (see `crate::wire::Abc`).
3069 assert_eq!(metric.abc.conditions_sum(), 1);
3070 assert_eq!(metric.abc.branches_sum(), 1);
3071 assert_eq!(metric.abc.assignments_sum(), 0);
3072 },
3073 );
3074 }
3075
3076 #[test]
3077 fn csharp_while_bare_identifier_condition() {
3078 check_metrics::<CsharpParser>(
3079 "class A {
3080 void M(bool x) {
3081 while (x) { x = false; }
3082 }
3083 }",
3084 "foo.cs",
3085 |metric| {
3086 // `while (x)` contributes 1 condition; `x = false` is 1 assignment.
3087 assert_eq!(metric.abc.conditions_sum(), 1);
3088 assert_eq!(metric.abc.assignments_sum(), 1);
3089 assert_eq!(metric.abc.branches_sum(), 0);
3090 },
3091 );
3092 }
3093
3094 #[test]
3095 fn csharp_do_while_bare_identifier_condition() {
3096 check_metrics::<CsharpParser>(
3097 "class A {
3098 void M(bool x) {
3099 do { x = true; } while (x);
3100 }
3101 }",
3102 "foo.cs",
3103 |metric| {
3104 // `do { ... } while (x)` contributes 1 condition;
3105 // `x = true` is 1 assignment.
3106 assert_eq!(metric.abc.conditions_sum(), 1);
3107 assert_eq!(metric.abc.assignments_sum(), 1);
3108 assert_eq!(metric.abc.branches_sum(), 0);
3109 },
3110 );
3111 }
3112
3113 #[test]
3114 fn csharp_if_unary_not_condition() {
3115 // Two cases share one test:
3116 //
3117 // if (!x) { … } — IfStatement is a known-boolean parent, so
3118 // the unary `!` arm in `csharp_inspect_container` is *one of
3119 // two* ways `has_boolean_content` gets set to true (the parent
3120 // seed sets it before the `!` does). A regression that broke
3121 // only the `is_not` branch wouldn't show up here.
3122 //
3123 // return !x; — ReturnStatement is *not* in the boolean-context
3124 // seed list (BinaryExpression | IfStatement | WhileStatement |
3125 // DoStatement | ForStatement | ConditionalExpression). So the
3126 // `!` wrapper is the *only* path that sets
3127 // `has_boolean_content = true`. Asserting the `return !x;`
3128 // case isolates the unary-unwrap logic from the parent-seed
3129 // path.
3130 check_metrics::<CsharpParser>(
3131 "class A {
3132 void M(bool x) {
3133 if (!x) { System.Console.WriteLine(\"a\"); }
3134 }
3135 bool N(bool x) {
3136 return !x;
3137 }
3138 }",
3139 "foo.cs",
3140 |metric| {
3141 // `if (!x)` contributes 1 condition (PrefixUnaryExpression
3142 // path with parent IfStatement seeding has_boolean_content).
3143 // `return !x;` contributes 1 condition (parent doesn't seed
3144 // — the unary `!` is the only path that sets the flag).
3145 // → 2 conditions total. 1 branch from WriteLine().
3146 assert_eq!(metric.abc.conditions_sum(), 2);
3147 assert_eq!(metric.abc.branches_sum(), 1);
3148 assert_eq!(metric.abc.assignments_sum(), 0);
3149 },
3150 );
3151 }
3152
3153 #[test]
3154 fn csharp_if_double_parenthesized_condition() {
3155 // Audit-tests follow-up: with only the
3156 // `csharp_prefix_unary_expr_kinds!()` arm covered by
3157 // `csharp_if_unary_not_condition`, the
3158 // `csharp_paren_expr_kinds!()` delegation arm in
3159 // `csharp_count_condition` was a pure dead-code candidate —
3160 // disabling it caused zero existing tests to fail (verified
3161 // 2026-05-26).
3162 //
3163 // `if ((x))` puts a `ParenthesizedExpression` at child(2) of
3164 // the IfStatement (child(1) is the literal `(`, child(2) is
3165 // the inner parenthesised expression, child(3) is the literal
3166 // `)`). `csharp_count_condition` must route that case to
3167 // `csharp_inspect_container`, which then sees parent =
3168 // IfStatement, seeds `has_boolean_content = true`, walks to
3169 // the inner Identifier, and counts it. A regression that
3170 // removed the paren arm would silently score 0.
3171 check_metrics::<CsharpParser>(
3172 "class A {
3173 void M(bool x) {
3174 if ((x)) { System.Console.WriteLine(\"a\"); }
3175 }
3176 }",
3177 "foo.cs",
3178 |metric| {
3179 assert_eq!(metric.abc.conditions_sum(), 1);
3180 assert_eq!(metric.abc.branches_sum(), 1);
3181 assert_eq!(metric.abc.assignments_sum(), 0);
3182 },
3183 );
3184 }
3185
3186 #[test]
3187 fn csharp_bool_returning_terminal_kinds_count() {
3188 // Regression for issue #372 (lesson #19): before the fix,
3189 // `csharp_count_condition` / `csharp_inspect_container` only
3190 // recognised invocation / identifier / boolean literal as
3191 // terminal-bool operands, so the five idiomatic boolean
3192 // expressions in the `if (...)` slots below silently scored
3193 // zero conditions:
3194 //
3195 // - `cfg.flag` — MemberAccessExpression
3196 // - `await c.Check()` — AwaitExpression
3197 // - `(bool)v` — CastExpression
3198 // - `v is not null` — IsPatternExpression
3199 // - `flags[0]` — ElementAccessExpression
3200 //
3201 // expected: 5 conditions (one per `if`), 0 assignments,
3202 // 1 branch (the single `c.Check()` invocation; the other
3203 // `if`-condition expressions are not invocations).
3204 check_metrics::<CsharpParser>(
3205 "using System.Threading.Tasks;
3206 class A {
3207 async Task M(object v, bool[] flags, Cfg cfg, C c) {
3208 if (cfg.flag) { }
3209 if (await c.Check()) { }
3210 if ((bool)v) { }
3211 if (v is not null) { }
3212 if (flags[0]) { }
3213 }
3214 }
3215 class Cfg { public bool flag; }
3216 class C { public Task<bool> Check() => null; }",
3217 "foo.cs",
3218 |metric| {
3219 assert_eq!(metric.abc.conditions_sum(), 5);
3220 assert_eq!(metric.abc.assignments_sum(), 0);
3221 assert_eq!(metric.abc.branches_sum(), 1);
3222 },
3223 );
3224 }
3225
3226 #[test]
3227 fn csharp_if_method_call_condition() {
3228 check_metrics::<CsharpParser>(
3229 "class A {
3230 void M(string s) {
3231 if (s.StartsWith(\"x\")) { System.Console.WriteLine(\"a\"); }
3232 }
3233 }",
3234 "foo.cs",
3235 |metric| {
3236 // `if (s.StartsWith("x"))` contributes 1 condition
3237 // (InvocationExpression) plus 1 branch for the call itself,
3238 // plus 1 branch for WriteLine.
3239 assert_eq!(metric.abc.conditions_sum(), 1);
3240 assert_eq!(metric.abc.branches_sum(), 2);
3241 assert_eq!(metric.abc.assignments_sum(), 0);
3242 },
3243 );
3244 }
3245
3246 #[test]
3247 fn csharp_if_while_boolean_literal_condition() {
3248 // Regression for #371: the tree-sitter-c-sharp grammar wraps a
3249 // bare `true` / `false` literal used as the condition of
3250 // `if` / `while` / `do` / `?:` in a `boolean_literal` node,
3251 // not the leaf `true` / `false` tokens. `csharp_count_condition`
3252 // must therefore match `BooleanLiteral` (the wrapper),
3253 // mirroring the existing `csharp_walk_for_statement` arm.
3254 // Without that, every literal-condition statement scored 0
3255 // conditions. The sibling `csharp_count_unary_conditions`
3256 // arm is covered separately by
3257 // `csharp_short_circuit_with_boolean_literal_operand` and
3258 // `csharp_inspect_container` is covered by
3259 // `csharp_declarations_with_conditions` (`!true` / `!false`).
3260 check_metrics::<CsharpParser>(
3261 "class A {
3262 void M() {
3263 if (true) { System.Console.WriteLine(\"a\"); }
3264 if (false) { System.Console.WriteLine(\"b\"); }
3265 while (true) { break; }
3266 do { break; } while (false);
3267 int t = true ? 1 : 0;
3268 }
3269 }",
3270 "foo.cs",
3271 |metric| {
3272 // Five literal-condition statements contribute 5
3273 // `BooleanLiteral` conditions (one per if/if/while/
3274 // do-while/ternary), plus the ternary's `?` token
3275 // adds one more via `csharp_count_token_condition`
3276 // → 6 total. The two `System.Console.WriteLine`
3277 // calls contribute 2 branches; the `int t = …`
3278 // initializer contributes 1 assignment.
3279 assert_eq!(metric.abc.conditions_sum(), 6);
3280 assert_eq!(metric.abc.branches_sum(), 2);
3281 assert_eq!(metric.abc.assignments_sum(), 1);
3282 },
3283 );
3284 }
3285
3286 #[test]
3287 fn csharp_short_circuit_with_boolean_literal_operand() {
3288 // Regression for #371 (companion to
3289 // `csharp_if_while_boolean_literal_condition`): a bare
3290 // `true` / `false` operand of `&&` / `||` lands in
3291 // `csharp_count_unary_conditions`, which iterates the parent
3292 // BinaryExpression's children. That helper must match the
3293 // `BooleanLiteral` wrapper just like `csharp_count_condition`
3294 // does — otherwise the operand silently scores zero. Mutation-
3295 // verified: removing `BooleanLiteral` from the
3296 // `csharp_count_unary_conditions` arm leaves every other test
3297 // in the suite passing, so this is the only test guarding
3298 // that helper's literal-operand path.
3299 check_metrics::<CsharpParser>(
3300 "class A {
3301 void M(bool x) {
3302 if (x && true) { System.Console.WriteLine(\"a\"); }
3303 if (false || x) { System.Console.WriteLine(\"b\"); }
3304 }
3305 }",
3306 "foo.cs",
3307 |metric| {
3308 // `&&` and `||` themselves are NOT in
3309 // `csharp_count_token_condition`'s match list — they
3310 // route through `csharp_walk_for_conditions::AMPAMP|
3311 // PIPEPIPE`, which calls
3312 // `csharp_count_unary_conditions` on the parent
3313 // BinaryExpression. Each invocation counts every
3314 // child that matches the terminal-operand kinds and
3315 // whose parent is a BinaryExpression. For
3316 // `x && true`: 1 (Identifier x) + 1 (BooleanLiteral
3317 // true) = 2. For `false || x`: 1 (BooleanLiteral
3318 // false) + 1 (Identifier x) = 2. Total 4. Without
3319 // the BooleanLiteral arm only the two Identifier
3320 // counts would land, giving 2.
3321 assert_eq!(metric.abc.conditions_sum(), 4);
3322 assert_eq!(metric.abc.branches_sum(), 2);
3323 assert_eq!(metric.abc.assignments_sum(), 0);
3324 },
3325 );
3326 }
3327
3328 #[test]
3329 fn csharp_return_without_conditions() {
3330 check_metrics::<CsharpParser>(
3331 "class A {
3332 int M() { return 42; }
3333 string N() { return \"hi\"; }
3334 }",
3335 "foo.cs",
3336 |metric| insta::assert_json_snapshot!(metric.abc),
3337 );
3338 }
3339
3340 #[test]
3341 fn csharp_lambda_expressions_return_with_conditions() {
3342 check_metrics::<CsharpParser>(
3343 "class A {
3344 public void M() {
3345 System.Func<int, bool> f = x => (x > 0);
3346 System.Func<int, bool> g = x => !(x < 0);
3347 }
3348 }",
3349 "foo.cs",
3350 |metric| insta::assert_json_snapshot!(metric.abc),
3351 );
3352 }
3353
3354 #[test]
3355 fn csharp_for_with_variable_declaration() {
3356 check_metrics::<CsharpParser>(
3357 "class A {
3358 void M() {
3359 for (int i = 0; i < 10; i++) {
3360 System.Console.WriteLine(i);
3361 }
3362 }
3363 }",
3364 "foo.cs",
3365 |metric| insta::assert_json_snapshot!(metric.abc),
3366 );
3367 }
3368
3369 #[test]
3370 fn csharp_for_without_variable_declaration() {
3371 check_metrics::<CsharpParser>(
3372 "class A {
3373 void M() {
3374 int i;
3375 for (i = 0; i < 10; i++) {
3376 System.Console.WriteLine(i);
3377 }
3378 }
3379 }",
3380 "foo.cs",
3381 |metric| insta::assert_json_snapshot!(metric.abc),
3382 );
3383 }
3384
3385 #[test]
3386 fn csharp_for_identifier_condition() {
3387 check_metrics::<CsharpParser>(
3388 "class A {
3389 void M(bool ready) {
3390 for (; ready ;) { }
3391 }
3392 }",
3393 "foo.cs",
3394 |metric| {
3395 // expected: assignments=0 (no `=` / `++` / `--`),
3396 // branches=0 (no invocation / object creation),
3397 // conditions=1 (bare-identifier for-loop condition).
3398 // Averages divide by 3 spaces (top-level + class + method).
3399 insta::assert_json_snapshot!(
3400 metric.abc,
3401 @r#"
3402 {
3403 "assignments": 0,
3404 "branches": 0,
3405 "conditions": 1,
3406 "magnitude": 1.0,
3407 "value": 0.0,
3408 "assignments_average": 0.0,
3409 "branches_average": 0.0,
3410 "conditions_average": 0.3333333333333333,
3411 "assignments_min": 0,
3412 "assignments_max": 0,
3413 "branches_min": 0,
3414 "branches_max": 0,
3415 "conditions_min": 0,
3416 "conditions_max": 1
3417 }
3418 "#
3419 );
3420 },
3421 );
3422 }
3423
3424 #[test]
3425 fn csharp_for_invocation_condition() {
3426 check_metrics::<CsharpParser>(
3427 "class A {
3428 bool Ok() { return true; }
3429 void M() {
3430 for (; Ok() ;) { }
3431 }
3432 }",
3433 "foo.cs",
3434 |metric| {
3435 // expected: assignments=0, branches=1 (the `Ok()` call),
3436 // conditions=1 (invocation as for-loop condition).
3437 // Averages divide by 4 spaces (top-level + class + two
3438 // methods).
3439 insta::assert_json_snapshot!(
3440 metric.abc,
3441 @r#"
3442 {
3443 "assignments": 0,
3444 "branches": 1,
3445 "conditions": 1,
3446 "magnitude": 1.4142135623730951,
3447 "value": 0.0,
3448 "assignments_average": 0.0,
3449 "branches_average": 0.25,
3450 "conditions_average": 0.25,
3451 "assignments_min": 0,
3452 "assignments_max": 0,
3453 "branches_min": 0,
3454 "branches_max": 1,
3455 "conditions_min": 0,
3456 "conditions_max": 1
3457 }
3458 "#
3459 );
3460 },
3461 );
3462 }
3463
3464 // Regression coverage for #279: the C# grammar wraps a literal
3465 // `true` / `false` for-loop condition in a `boolean_literal` node.
3466 // The `BooleanLiteral` arm in the `ForStatement` dispatch must
3467 // attribute one condition; without it, `for (; true ;)` would
3468 // contribute 0 (the bug fixed by this commit also affected this
3469 // shape).
3470 #[test]
3471 fn csharp_for_boolean_literal_condition() {
3472 check_metrics::<CsharpParser>(
3473 "class A {
3474 void M() {
3475 for (; true ;) { }
3476 }
3477 }",
3478 "foo.cs",
3479 |metric| {
3480 // expected: assignments=0, branches=0,
3481 // conditions=1 (the `true` literal as condition).
3482 assert_eq!(metric.abc.conditions_sum(), 1);
3483 assert_eq!(metric.abc.assignments_sum(), 0);
3484 assert_eq!(metric.abc.branches_sum(), 0);
3485 },
3486 );
3487 }
3488
3489 // Regression coverage for #279: an empty for-loop condition such as
3490 // `for (; ;) {}` must contribute 0 to conditions — there is no
3491 // condition node to count.
3492 #[test]
3493 fn csharp_for_empty_condition() {
3494 check_metrics::<CsharpParser>(
3495 "class A {
3496 void M() {
3497 for (; ;) { }
3498 }
3499 }",
3500 "foo.cs",
3501 |metric| {
3502 // expected: assignments=0, branches=0, conditions=0
3503 // (no condition expression in `for (; ;)`).
3504 insta::assert_json_snapshot!(
3505 metric.abc,
3506 @r#"
3507 {
3508 "assignments": 0,
3509 "branches": 0,
3510 "conditions": 0,
3511 "magnitude": 0.0,
3512 "value": 0.0,
3513 "assignments_average": 0.0,
3514 "branches_average": 0.0,
3515 "conditions_average": 0.0,
3516 "assignments_min": 0,
3517 "assignments_max": 0,
3518 "branches_min": 0,
3519 "branches_max": 0,
3520 "conditions_min": 0,
3521 "conditions_max": 0
3522 }
3523 "#
3524 );
3525 },
3526 );
3527 }
3528
3529 #[test]
3530 fn csharp_ternary_conditions() {
3531 check_metrics::<CsharpParser>(
3532 "class A {
3533 int Sign(int x) {
3534 return (x > 0) ? 1 : (x < 0 ? -1 : 0);
3535 }
3536 }",
3537 "foo.cs",
3538 |metric| insta::assert_json_snapshot!(metric.abc),
3539 );
3540 }
3541
3542 #[test]
3543 fn csharp_malformed_parenthesized_no_panic() {
3544 check_metrics::<CsharpParser>("class A { void M() { if (( }) }", "foo.cs", |metric| {
3545 // Don't panic on malformed source.
3546 assert_eq!(metric.abc.assignments(), 0);
3547 assert_eq!(metric.abc.branches(), 0);
3548 });
3549 }
3550
3551 #[test]
3552 fn csharp_function_pointer_type_no_double_count() {
3553 // EC1 extension — `<` and `>` are also parameter-list delimiters
3554 // for unsafe function-pointer types. `FunctionPointerType` must
3555 // be in the LT/GT exclusion list, otherwise these brackets
3556 // accumulate spurious `conditions` counts.
3557 check_metrics::<CsharpParser>(
3558 "unsafe class A {
3559 public delegate*<int, int, int> Adder;
3560 public delegate*<string, void> Logger;
3561 }",
3562 "foo.cs",
3563 |metric| {
3564 assert_eq!(
3565 metric.abc.conditions(),
3566 0,
3567 "function-pointer-type angle brackets must not count"
3568 );
3569 },
3570 );
3571 }
3572
3573 #[test]
3574 fn csharp_generic_type_args_no_double_count() {
3575 // EC1 — `<` and `>` inside TypeArgumentList must not count as
3576 // boolean conditions.
3577 check_metrics::<CsharpParser>(
3578 "class A {
3579 void M(System.Collections.Generic.Dictionary<string, System.Collections.Generic.List<int>> d) {
3580 System.Console.WriteLine(d);
3581 }
3582 }",
3583 "foo.cs",
3584 |metric| insta::assert_json_snapshot!(metric.abc),
3585 );
3586 }
3587
3588 // #1275: tree-sitter-c-sharp spells `int?`, `where T : class?`, the
3589 // ternary and `a?.b` with one and the same bare `?` token, so the
3590 // unguarded `QMARK` arm scored the two type-syntax forms as
3591 // decisions.
3592 //
3593 // The fixture carries two `nullable_type` `?`, one
3594 // `type_parameter_constraint` `?`, and one real ternary over a real
3595 // `>` comparison, so every way of getting the gate wrong lands on
3596 // its own number: 5 pre-fix, 3 if only `NullableType` is denied, 4
3597 // if only `TypeParameterConstraint` is, 1 if the gate swallows the
3598 // genuine ternary, 0 if the fixture stops parsing. Asserting 0 on a
3599 // nullable-only body would have been vacuous — an unparsable file
3600 // scores 0 too.
3601 #[test]
3602 fn csharp_nullable_type_syntax_is_not_a_condition() {
3603 check_metrics::<CsharpParser>(
3604 "class A {
3605 int M<T>(int? p, int a, int b) where T : class? {
3606 int? q = null;
3607 return a > b ? 1 : 2;
3608 }
3609 }",
3610 "foo.cs",
3611 |metric| {
3612 assert_eq!(metric.abc.conditions_sum(), 2);
3613 },
3614 );
3615 }
3616
3617 // The polarity pin for #1275's C# half. `csharp_count_token_condition`
3618 // denies the two type-syntax parents rather than allowing the two
3619 // decision-bearing ones, so that `a?.b` and `a?[0]` keep counting by
3620 // construction: both spell their operator as the same bare `?` under
3621 // `conditional_access_expression`, and C# cyclomatic counts that node
3622 // (`safe_navigation_chain_parity` pins C# at one decision per
3623 // operator alongside every other safe-navigation language).
3624 //
3625 // This test is what stops a later "make all three languages
3626 // consistent" pass from flipping C# to a `ConditionalExpression`
3627 // allowlist: that would silently drop both counts here to 0 while
3628 // every other ABC test still passed. `??` is deliberately absent
3629 // from the expected total — C# ABC does not list `QMARKQMARK` as a
3630 // condition (it does in the TS family), which is pre-existing and
3631 // out of scope for #1275.
3632 #[test]
3633 fn csharp_conditional_access_still_counts_as_a_condition() {
3634 check_metrics::<CsharpParser>(
3635 "class A {
3636 object M(string s, int[] xs) {
3637 return s?.Length ?? xs?[0];
3638 }
3639 }",
3640 "foo.cs",
3641 |metric| {
3642 assert_eq!(metric.abc.conditions_sum(), 2);
3643 },
3644 );
3645 }
3646
3647 // The other direction of #1275's C# gate, isolated: narrowing
3648 // `QMARK` must not swallow a real ternary. The fixture carries no
3649 // comparison operator at all, so the ternary is the only thing that
3650 // can put the count above the walker's own contribution — in the
3651 // test above, a surviving `>` would have masked an over-suppressed
3652 // `?` at 1 instead of 0 (grammar-dispatch §11). A nullable
3653 // parameter keeps the denied production live in the same parse, so
3654 // one fixture proves both directions: 3 pre-fix, 2 after, 1 if the
3655 // allowlist/denylist is aimed at `ConditionalExpression`.
3656 //
3657 // The 2 is the `?` token plus the ternary's condition slot, which
3658 // `csharp_walk_for_conditions` counts as a Fitzpatrick unary
3659 // condition. That is why this is not written as an
3660 // `assert_deepest_conditions_match_cyclomatic` parity pin the way
3661 // the Java `<` / `>` sibling is: C# cyclomatic scores this method 1
3662 // decision, and the divergence is the pre-existing unary-condition
3663 // rule, not the `?` gate.
3664 #[test]
3665 fn csharp_ternary_still_counts_alongside_nullable_types() {
3666 check_metrics::<CsharpParser>(
3667 "class A {
3668 int M(int? n, bool c) {
3669 return c ? 1 : 2;
3670 }
3671 }",
3672 "foo.cs",
3673 |metric| {
3674 assert_eq!(metric.abc.conditions_sum(), 2);
3675 },
3676 );
3677 }
3678
3679 #[test]
3680 fn csharp_aliased_invocation_expression_branches() {
3681 // Regression for issue #94 (lesson #2): the C# grammar emits three
3682 // aliased `kind_id`s for `invocation_expression`. Code that matches
3683 // only the unsuffixed `Csharp::InvocationExpression` undercounts ABC
3684 // branches whenever the AST emits an aliased variant. The three
3685 // method calls live in `M`, so the per-method maximum (visible at
3686 // the unit-space aggregate as `branches_max`) must be 3.
3687 check_metrics::<CsharpParser>(
3688 "class A {
3689 void M() {
3690 System.Console.WriteLine(1);
3691 System.Console.WriteLine(2);
3692 System.Console.WriteLine(3);
3693 }
3694 }",
3695 "foo.cs",
3696 |metric| {
3697 assert_eq!(metric.abc.branches_max(), 3);
3698 assert_eq!(metric.abc.conditions_max(), 0);
3699 },
3700 );
3701 }
3702
3703 #[test]
3704 fn php_zero_abc() {
3705 check_metrics::<PhpParser>("<?php\n", "foo.php", |metric| {
3706 assert_eq!(metric.abc.assignments_sum(), 0);
3707 assert_eq!(metric.abc.branches_sum(), 0);
3708 assert_eq!(metric.abc.conditions_sum(), 0);
3709 insta::assert_json_snapshot!(metric.abc);
3710 });
3711 }
3712
3713 #[test]
3714 fn php_simple_assignment() {
3715 check_metrics::<PhpParser>(
3716 "<?php
3717function f(): void {
3718 $a = 1;
3719 $b = 2;
3720}",
3721 "foo.php",
3722 |metric| insta::assert_json_snapshot!(metric.abc),
3723 );
3724 }
3725
3726 #[test]
3727 fn php_augmented_assignment() {
3728 check_metrics::<PhpParser>(
3729 "<?php
3730function f(int $x): int {
3731 $a = 0;
3732 $a += $x;
3733 $a -= 1;
3734 $a *= 2;
3735 return $a;
3736}",
3737 "foo.php",
3738 |metric| insta::assert_json_snapshot!(metric.abc),
3739 );
3740 }
3741
3742 #[test]
3743 fn php_const_excluded() {
3744 // Constant declarations and enum cases are NOT counted as
3745 // assignments — they declare immutable values.
3746 check_metrics::<PhpParser>(
3747 "<?php
3748class A {
3749 const PI = 3.14;
3750 const E = 2.71;
3751}
3752enum Color {
3753 case Red;
3754 case Green;
3755}",
3756 "foo.php",
3757 |metric| insta::assert_json_snapshot!(metric.abc),
3758 );
3759 }
3760
3761 #[test]
3762 fn php_function_call() {
3763 check_metrics::<PhpParser>(
3764 "<?php
3765function f(): void {
3766 foo();
3767 bar(1, 2);
3768}",
3769 "foo.php",
3770 |metric| insta::assert_json_snapshot!(metric.abc),
3771 );
3772 }
3773
3774 #[test]
3775 fn php_method_call() {
3776 check_metrics::<PhpParser>(
3777 "<?php
3778function f($obj): void {
3779 $obj->m1();
3780 $obj->m2(1);
3781}",
3782 "foo.php",
3783 |metric| insta::assert_json_snapshot!(metric.abc),
3784 );
3785 }
3786
3787 #[test]
3788 fn php_static_call() {
3789 check_metrics::<PhpParser>(
3790 "<?php
3791function f(): void {
3792 Foo::bar();
3793 Foo::baz(1);
3794}",
3795 "foo.php",
3796 |metric| insta::assert_json_snapshot!(metric.abc),
3797 );
3798 }
3799
3800 #[test]
3801 fn php_nullsafe_call() {
3802 check_metrics::<PhpParser>(
3803 "<?php
3804function f($obj): void {
3805 $obj?->m1();
3806 $obj?->m2(1);
3807}",
3808 "foo.php",
3809 |metric| insta::assert_json_snapshot!(metric.abc),
3810 );
3811 }
3812
3813 #[test]
3814 fn php_object_creation() {
3815 check_metrics::<PhpParser>(
3816 "<?php
3817function f(): void {
3818 new Foo();
3819 new Bar(1);
3820}",
3821 "foo.php",
3822 |metric| insta::assert_json_snapshot!(metric.abc),
3823 );
3824 }
3825
3826 #[test]
3827 fn php_comparison_eq() {
3828 check_metrics::<PhpParser>(
3829 "<?php
3830function f(int $a, int $b): bool {
3831 return $a == $b || $a != $b;
3832}",
3833 "foo.php",
3834 |metric| insta::assert_json_snapshot!(metric.abc),
3835 );
3836 }
3837
3838 #[test]
3839 fn php_comparison_strict() {
3840 check_metrics::<PhpParser>(
3841 "<?php
3842function f(int $a, int $b): bool {
3843 return $a === $b || $a !== $b;
3844}",
3845 "foo.php",
3846 |metric| insta::assert_json_snapshot!(metric.abc),
3847 );
3848 }
3849
3850 #[test]
3851 fn php_spaceship() {
3852 check_metrics::<PhpParser>(
3853 "<?php
3854function f(int $a, int $b): int {
3855 return $a <=> $b;
3856}",
3857 "foo.php",
3858 |metric| insta::assert_json_snapshot!(metric.abc),
3859 );
3860 }
3861
3862 #[test]
3863 fn php_instanceof() {
3864 check_metrics::<PhpParser>(
3865 "<?php
3866function f($x): bool {
3867 return $x instanceof Foo;
3868}",
3869 "foo.php",
3870 |metric| insta::assert_json_snapshot!(metric.abc),
3871 );
3872 }
3873
3874 #[test]
3875 fn php_complex_function() {
3876 // One snippet exercising A, B, C buckets together.
3877 check_metrics::<PhpParser>(
3878 "<?php
3879function f(int $a, int $b): int {
3880 $sum = $a + $b;
3881 $prod = $a * $b;
3882 if ($sum > 0 && $prod === 0) {
3883 return foo($sum);
3884 }
3885 return bar()->double();
3886}",
3887 "foo.php",
3888 |metric| insta::assert_json_snapshot!(metric.abc),
3889 );
3890 }
3891
3892 #[test]
3893 fn php_if_boolean_literal_condition() {
3894 check_metrics::<PhpParser>(
3895 "<?php\n\
3896 function f() {\n\
3897 \x20 if (true) {} // +1c\n\
3898 \x20 if (!false) {} // +1c\n\
3899 \x20 while (true) {} // +1c\n\
3900 \x20 do {} while (false); // +1c\n\
3901 }\n",
3902 "foo.php",
3903 |metric| {
3904 assert_eq!(metric.abc.conditions_sum(), 4);
3905 insta::assert_json_snapshot!(metric.abc);
3906 },
3907 );
3908 }
3909
3910 #[test]
3911 fn php_methods_arguments_with_conditions() {
3912 check_metrics::<PhpParser>(
3913 "<?php\n\
3914 function f($a, $b) {\n\
3915 \x20 m($a, $b); // +1b\n\
3916 \x20 m(!$a, !$b); // +1b +2c\n\
3917 }\n",
3918 "foo.php",
3919 |metric| {
3920 assert_eq!(metric.abc.branches_sum(), 2);
3921 assert_eq!(metric.abc.conditions_sum(), 2);
3922 insta::assert_json_snapshot!(metric.abc);
3923 },
3924 );
3925 }
3926
3927 #[test]
3928 fn php_return_with_conditions() {
3929 check_metrics::<PhpParser>(
3930 "<?php\n\
3931 function m1($z) { return !($z >= 0); }\n\
3932 function m2($x) { return (((!$x))); }\n\
3933 function m3($x, $y) { return $x && $y; }\n",
3934 "foo.php",
3935 |metric| {
3936 // m1: `>=` (1). m2: walker unwraps to $x (1).
3937 // m3: `&&` walker counts both (2). Sum: 4.
3938 assert_eq!(metric.abc.conditions_sum(), 4);
3939 insta::assert_json_snapshot!(metric.abc);
3940 },
3941 );
3942 }
3943
3944 #[test]
3945 fn php_name2_hidden_rule_drift_marker() {
3946 // Drift marker (findings.md round-2 #3): `Php::Name2` maps
3947 // to the hidden grammar rule `_name`. At the pinned
3948 // tree-sitter-php version it is never emitted as a concrete
3949 // node — the visible `Name` (= 1) carries every name.
3950 // We list `Name2` defensively in `php_bool_terminal_kinds!()`
3951 // (lesson 34); if a future grammar bump promotes `_name`
3952 // to a visible rule, this assertion fails loudly.
3953 let src = "<?php\nfunction f($x) { if ($x) { foo($x); } }\n";
3954 let parser = PhpParser::new(
3955 src.as_bytes().to_vec(),
3956 &std::path::PathBuf::from("foo.php"),
3957 None,
3958 );
3959 assert!(!ast_has_kind_id(&parser, Php::Name2 as u16));
3960 }
3961
3962 #[test]
3963 fn php_scoped_property_access_condition_counts() {
3964 // Regression for findings.md round-2 #1 (PHP):
3965 // `if (Config::$enabled) {}` parses with
3966 // `scoped_property_access_expression` as the condition
3967 // node (kind_id 333 at the pinned grammar version — the
3968 // `*2` alias). Pre-fix, neither `ScopedPropertyAccessExpression`
3969 // nor its alias was in `php_bool_terminal_kinds!()`. The
3970 // walker reached the access node, found it non-terminal,
3971 // and broke. Mirrors C#'s `MemberAccessExpression` rule
3972 // (lesson 19, #372).
3973 check_metrics::<PhpParser>(
3974 "<?php\n\
3975 class Config { public static $enabled = true; }\n\
3976 function f() { if (Config::$enabled) { } }\n",
3977 "foo.php",
3978 |metric| {
3979 assert_eq!(metric.abc.conditions_sum(), 1);
3980 insta::assert_json_snapshot!(metric.abc);
3981 },
3982 );
3983 }
3984
3985 #[test]
3986 fn php_named_argument_unary_conditional_counts() {
3987 // Regression for the code-review finding: PHP 8 named-argument
3988 // syntax `m(name: !$a)` parses as `argument(name, ':',
3989 // unary_op_expression)`. Pre-fix, the count walker took
3990 // `argument.child(0)` (the name) and missed the value at the
3991 // last child. Now it picks the last named child as the value.
3992 check_metrics::<PhpParser>(
3993 "<?php\nfunction f($a) { m(name: !$a); }\n",
3994 "foo.php",
3995 |metric| {
3996 // 1 call (branch) + 1 unary-conditional named argument.
3997 assert_eq!(metric.abc.branches_sum(), 1);
3998 assert_eq!(metric.abc.conditions_sum(), 1);
3999 insta::assert_json_snapshot!(metric.abc);
4000 },
4001 );
4002 }
4003
4004 #[test]
4005 fn php_low_precedence_keyword_logical_ops_trigger_walker() {
4006 // Regression: pre-fix, `$a or $b` reported 0 conditions
4007 // because the dispatcher only handled `AMPAMP|PIPEPIPE`,
4008 // skipping the PHP-specific `and` / `or` / `xor` keyword
4009 // forms even though they parse under the same
4010 // `binary_expression` shape.
4011 check_metrics::<PhpParser>(
4012 "<?php\n\
4013 function f($a, $b) {\n\
4014 \x20 return $a or $b;\n\
4015 }\n",
4016 "foo.php",
4017 |metric| {
4018 assert_eq!(metric.abc.conditions_sum(), 2);
4019 insta::assert_json_snapshot!(metric.abc);
4020 },
4021 );
4022 }
4023
4024 #[test]
4025 fn php_if_multiple_conditions() {
4026 check_metrics::<PhpParser>(
4027 "<?php\n\
4028 function f($a, $b, $c, $d) {\n\
4029 \x20 if ($a || $b || $c || $d) {} // +4c\n\
4030 \x20 if ($a && $b && $c) {} // +3c\n\
4031 \x20 if (!$a && !$b) {} // +2c\n\
4032 }\n",
4033 "foo.php",
4034 |metric| {
4035 assert_eq!(metric.abc.conditions_sum(), 9);
4036 insta::assert_json_snapshot!(metric.abc);
4037 },
4038 );
4039 }
4040
4041 #[test]
4042 fn php_while_and_do_while_conditions() {
4043 check_metrics::<PhpParser>(
4044 "<?php\n\
4045 function f($a, $b) {\n\
4046 \x20 while ($a || $b) {} // +2c\n\
4047 \x20 do {} while ($a && !$b); // +2c\n\
4048 }\n",
4049 "foo.php",
4050 |metric| {
4051 assert_eq!(metric.abc.conditions_sum(), 4);
4052 insta::assert_json_snapshot!(metric.abc);
4053 },
4054 );
4055 }
4056
4057 #[test]
4058 fn php_short_circuit_with_boolean_literal_operand() {
4059 check_metrics::<PhpParser>(
4060 "<?php\nfunction f($a) { return $a && true; }\n",
4061 "foo.php",
4062 |metric| {
4063 assert_eq!(metric.abc.conditions_sum(), 2);
4064 insta::assert_json_snapshot!(metric.abc);
4065 },
4066 );
4067 }
4068
4069 // Issue #1102, PHP half. See
4070 // `cpp_ternary_operand_slots_count_as_unary_conditions` for the
4071 // rule. PHP's ABC dispatcher has no `?`-token arm — the grammar
4072 // does emit the token, but the `conditional_expression` node is
4073 // what carries the tally's +1 — so the arm keeps that increment and
4074 // adds the operand slots.
4075 #[test]
4076 fn php_ternary_operand_slots_count_as_unary_conditions() {
4077 // ternary (1) + condition `$a` (1) + `!$b` (1) + `!$c` (1) = 4.
4078 check_metrics::<PhpParser>(
4079 "<?php\nfunction f() { $x = $a ? !$b : !$c; }\n",
4080 "foo.php",
4081 |metric| assert_eq!(metric.abc.conditions_sum(), 4),
4082 );
4083 // No-double-count pin: ternary (1) + `>` (1) = 2, unchanged by
4084 // the fix.
4085 check_metrics::<PhpParser>(
4086 "<?php\nfunction f() { $x = ($a > 0) ? $b : -$b; }\n",
4087 "foo.php",
4088 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
4089 );
4090 // Nested (PHP 8 requires the inner ternary parenthesised): two
4091 // ternary nodes plus the two bare-variable conditions = 4.
4092 check_metrics::<PhpParser>(
4093 "<?php\nfunction f() { $x = $a ? ($b ? $c : $d) : $e; }\n",
4094 "foo.php",
4095 |metric| assert_eq!(metric.abc.conditions_sum(), 4),
4096 );
4097 // A negated condition is the only input reaching the walker's
4098 // `else` fallback — see the C++ sibling for why. ternary (1) +
4099 // `!$a` (1) = 2.
4100 check_metrics::<PhpParser>(
4101 "<?php\nfunction f() { $x = !$a ? $b : $c; }\n",
4102 "foo.php",
4103 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
4104 );
4105 }
4106
4107 // PHP's short ternary `$a ?: $b` elides the consequence, which the
4108 // grammar names `body` (not `consequence`) and marks optional. The
4109 // alternative lands at child(3), so addressing the slot by field
4110 // name rather than a fixed child(4) is what keeps `!$b` counted.
4111 #[test]
4112 fn php_elided_ternary_body_still_walks_the_alternative() {
4113 // ternary (1) + condition `$a` (1) + `!$b` (1) = 3.
4114 check_metrics::<PhpParser>(
4115 "<?php\nfunction f() { $x = $a ?: !$b; }\n",
4116 "foo.php",
4117 |metric| assert_eq!(metric.abc.conditions_sum(), 3),
4118 );
4119 }
4120
4121 // Issue #1276, PHP half. The `for` header's condition slot was the
4122 // one condition slot `PhpCode::compute` never dispatched, so a
4123 // bare / negated / parenthesised loop condition scored zero while
4124 // the identical predicate in an `if` header scored one. Each
4125 // fixture below is a shape only the new arm can classify — a
4126 // comparison-shaped condition proves nothing here, because the `<`
4127 // token arm counts it either way (grammar-dispatch §11).
4128 #[test]
4129 fn php_for_condition_slot_counts_unary_conditions() {
4130 // Bare variable: the whole condition, no operator token.
4131 check_metrics::<PhpParser>(
4132 "<?php\nfunction f($a) { for (; $a; ) {} }\n",
4133 "foo.php",
4134 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
4135 );
4136 // Negation: reaches the terminal through
4137 // `php_inspect_container`'s `!` unwrap.
4138 check_metrics::<PhpParser>(
4139 "<?php\nfunction f($a) { for (; !$a; ) {} }\n",
4140 "foo.php",
4141 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
4142 );
4143 // Parentheses: counts only because the `for_statement` parent
4144 // seeds `has_boolean_content`, the seed #1276 found dead.
4145 check_metrics::<PhpParser>(
4146 "<?php\nfunction f($a) { for (; ($a); ) {} }\n",
4147 "foo.php",
4148 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
4149 );
4150 // No-double-count pin: the `<` token arm already counted this
4151 // shape before the fix, and the walker must not add a second.
4152 // Two assignments (`$i = 0`, `$i++`) confirm the header parsed
4153 // as the three-clause form rather than degenerating.
4154 check_metrics::<PhpParser>(
4155 "<?php\nfunction f($n) { for ($i = 0; $i < $n; $i++) {} }\n",
4156 "foo.php",
4157 |metric| {
4158 assert_eq!(metric.abc.conditions_sum(), 1);
4159 assert_eq!(metric.abc.assignments_sum(), 2);
4160 },
4161 );
4162 // Empty condition: no `condition` field, no decision, zero.
4163 check_metrics::<PhpParser>(
4164 "<?php\nfunction f() { for (;;) { break; } }\n",
4165 "foo.php",
4166 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
4167 );
4168 }
4169
4170 // --- Kotlin ABC tests -------------------------------------------------
4171
4172 #[test]
4173 fn kotlin_empty_class() {
4174 check_metrics::<KotlinParser>("class C {}", "foo.kt", |metric| {
4175 assert_eq!(metric.abc.assignments_sum(), 0);
4176 assert_eq!(metric.abc.branches_sum(), 0);
4177 assert_eq!(metric.abc.conditions_sum(), 0);
4178 insta::assert_json_snapshot!(metric.abc);
4179 });
4180 }
4181
4182 #[test]
4183 fn kotlin_val_declarations_are_not_assignments() {
4184 // `val` introduces an immutable binding — the `=` initialising it
4185 // is not an assignment in the ABC sense.
4186 check_metrics::<KotlinParser>(
4187 "class C {
4188 val a: Int = 1
4189 val b: Int = 2
4190 val c: Int = 3
4191 }",
4192 "foo.kt",
4193 |metric| {
4194 assert_eq!(metric.abc.assignments_sum(), 0);
4195 assert_eq!(metric.abc.branches_sum(), 0);
4196 insta::assert_json_snapshot!(metric.abc);
4197 },
4198 );
4199 }
4200
4201 #[test]
4202 fn kotlin_var_declarations_count_assignment() {
4203 // `var` initialisers count as assignments (mutable binding).
4204 check_metrics::<KotlinParser>(
4205 "class C {
4206 var a: Int = 1
4207 var b: Int = 2
4208 }",
4209 "foo.kt",
4210 |metric| {
4211 assert_eq!(metric.abc.assignments_sum(), 2);
4212 insta::assert_json_snapshot!(metric.abc);
4213 },
4214 );
4215 }
4216
4217 #[test]
4218 fn kotlin_val_then_assignments_count() {
4219 // Regression for #455: a `val` initialiser must not suppress the
4220 // standalone `=` assignments that follow it. tree-sitter-kotlin
4221 // emits no `SEMI` token (even for explicit semicolons), so the
4222 // pre-#455 `SEMI`-cleared declaration stack never cleared and the
4223 // immutable-`val` sentinel leaked, reporting A=0 here.
4224 check_metrics::<KotlinParser>(
4225 "fun f() {
4226 val cfg = 0
4227 a = 1
4228 b = 2
4229 }",
4230 "foo.kt",
4231 |metric| {
4232 // val initialiser suppressed; `a = 1` and `b = 2` count.
4233 assert_eq!(metric.abc.assignments_sum(), 2);
4234 insta::assert_json_snapshot!(metric.abc);
4235 },
4236 );
4237 }
4238
4239 #[test]
4240 fn kotlin_var_then_assignments_count() {
4241 // Companion to the #455 regression: a `var` declaration leaves a
4242 // mutable-binding sentinel that *permits* the `=` — this path
4243 // accidentally masked the leak (its `Var` sentinel never suppressed
4244 // anything), so it must keep counting both the initialiser and the
4245 // following standalone assignments.
4246 check_metrics::<KotlinParser>(
4247 "fun f() {
4248 var cfg = 0
4249 a = 1
4250 b = 2
4251 }",
4252 "foo.kt",
4253 |metric| {
4254 // var initialiser (+1) plus `a = 1` and `b = 2` (+2).
4255 assert_eq!(metric.abc.assignments_sum(), 3);
4256 insta::assert_json_snapshot!(metric.abc);
4257 },
4258 );
4259 }
4260
4261 #[test]
4262 fn kotlin_augmented_assignments_count() {
4263 // Augmented operators (+=, -=, etc.) and ++/-- always count.
4264 check_metrics::<KotlinParser>(
4265 "fun m() {
4266 var x = 0
4267 x += 1
4268 x -= 2
4269 x *= 3
4270 x++
4271 --x
4272 }",
4273 "foo.kt",
4274 |metric| {
4275 // var declaration (var x = 0): +1
4276 // x += 1, x -= 2, x *= 3, x++, --x: +5
4277 assert_eq!(metric.abc.assignments_sum(), 6);
4278 insta::assert_json_snapshot!(metric.abc);
4279 },
4280 );
4281 }
4282
4283 #[test]
4284 fn kotlin_branches_call_expression() {
4285 check_metrics::<KotlinParser>(
4286 "fun m() {
4287 println(\"a\")
4288 println(\"b\")
4289 println(\"c\")
4290 }",
4291 "foo.kt",
4292 |metric| {
4293 assert_eq!(metric.abc.branches_sum(), 3);
4294 insta::assert_json_snapshot!(metric.abc);
4295 },
4296 );
4297 }
4298
4299 #[test]
4300 fn kotlin_object_construction_branch() {
4301 // Kotlin's object construction is just `Foo()` — a `CallExpression`.
4302 check_metrics::<KotlinParser>(
4303 "class P(val x: Int)
4304 fun m(): P = P(1)",
4305 "foo.kt",
4306 |metric| {
4307 assert_eq!(metric.abc.branches_sum(), 1);
4308 insta::assert_json_snapshot!(metric.abc);
4309 },
4310 );
4311 }
4312
4313 #[test]
4314 fn kotlin_comparisons_count_conditions() {
4315 check_metrics::<KotlinParser>(
4316 "fun m(a: Int, b: Int): Boolean {
4317 val r1 = a < b
4318 val r2 = a > b
4319 val r3 = a <= b
4320 val r4 = a >= b
4321 val r5 = a == b
4322 val r6 = a != b
4323 return r1 || r2 || r3 || r4 || r5 || r6
4324 }",
4325 "foo.kt",
4326 |metric| {
4327 // Six comparison operators in the `val` initialisers
4328 // (<, >, <=, >=, ==, !=) → 6, plus the six bare-identifier
4329 // operands of the `r1 || … || r6` return chain, each a
4330 // Fitzpatrick Rule 9 unary condition (issue #557) → 6.
4331 // Total 12. Before the Kotlin walker was wired the chain
4332 // operands were silently dropped and this read 6.
4333 assert_eq!(metric.abc.conditions_sum(), 12);
4334 insta::assert_json_snapshot!(metric.abc);
4335 },
4336 );
4337 }
4338
4339 #[test]
4340 fn kotlin_identity_equality_conditions() {
4341 // `===` / `!==` are referential equality in Kotlin; they count too.
4342 check_metrics::<KotlinParser>(
4343 "fun m(a: Any, b: Any): Boolean {
4344 return a === b || a !== b
4345 }",
4346 "foo.kt",
4347 |metric| {
4348 assert_eq!(metric.abc.conditions_sum(), 2);
4349 insta::assert_json_snapshot!(metric.abc);
4350 },
4351 );
4352 }
4353
4354 #[test]
4355 fn kotlin_else_branch_counts() {
4356 check_metrics::<KotlinParser>(
4357 "fun m(x: Int): Int {
4358 return if (x > 0) 1 else -1
4359 }",
4360 "foo.kt",
4361 |metric| {
4362 // condition: > (1) + else (1) = 2
4363 assert_eq!(metric.abc.conditions_sum(), 2);
4364 insta::assert_json_snapshot!(metric.abc);
4365 },
4366 );
4367 }
4368
4369 #[test]
4370 fn kotlin_when_entries_count() {
4371 check_metrics::<KotlinParser>(
4372 "fun m(x: Int): Int {
4373 return when (x) {
4374 1 -> 10
4375 2 -> 20
4376 else -> 0
4377 }
4378 }",
4379 "foo.kt",
4380 |metric| {
4381 // Non-`else` WhenEntry arms count; the `else ->` fallback
4382 // arm does not (issue #456). Two case arms + zero for the
4383 // `else` arm = 2.
4384 assert_eq!(metric.abc.conditions_sum(), 2);
4385 insta::assert_json_snapshot!(metric.abc);
4386 },
4387 );
4388 }
4389
4390 // Pins the `else ->` exclusion directly: a `when` whose only fallback
4391 // is `else ->` must not count that arm. Revert-verified — gating the
4392 // `WhenEntry` arm on `!kotlin_when_entry_is_else` is what drops this
4393 // from 3 to 2 (issue #456, lesson 11). Mirrors the cyclomatic gate.
4394 #[test]
4395 fn kotlin_when_else_not_a_condition() {
4396 check_metrics::<KotlinParser>(
4397 "fun m(x: Int): Int {
4398 return when (x) { 1 -> 10; 2 -> 20; else -> 0 }
4399 }",
4400 "foo.kt",
4401 |metric| {
4402 // case `1 ->` (+1) + case `2 ->` (+1) + `else ->` (+0) = 2.
4403 assert_eq!(metric.abc.conditions_sum(), 2);
4404 },
4405 );
4406 }
4407
4408 #[test]
4409 fn kotlin_catch_block_counts() {
4410 check_metrics::<KotlinParser>(
4411 "fun m() {
4412 try {
4413 println(\"ok\")
4414 } catch (e: Exception) {
4415 println(\"err\")
4416 }
4417 }",
4418 "foo.kt",
4419 |metric| {
4420 // `try` (+1) and `catch` (+1) each contribute one condition,
4421 // matching Java / C# / C++ / Groovy (Fitzpatrick counts both
4422 // keywords). Before #696 Kotlin counted only the catch block.
4423 assert_eq!(metric.abc.conditions_sum(), 2);
4424 insta::assert_json_snapshot!(metric.abc);
4425 },
4426 );
4427 }
4428
4429 #[test]
4430 fn kotlin_elvis_and_safe_cast() {
4431 // `?:` (elvis) and `as?` (safe cast) are condition-like.
4432 check_metrics::<KotlinParser>(
4433 "fun m(s: String?): Int {
4434 val n = (s as? Int) ?: 0
4435 return n
4436 }",
4437 "foo.kt",
4438 |metric| {
4439 // as? (+1) + ?: (+1) = 2 conditions.
4440 assert_eq!(metric.abc.conditions_sum(), 2);
4441 insta::assert_json_snapshot!(metric.abc);
4442 },
4443 );
4444 }
4445
4446 #[test]
4447 fn kotlin_generic_brackets_not_conditions() {
4448 // `<` / `>` used as type-parameter brackets must not be counted.
4449 check_metrics::<KotlinParser>(
4450 "class Box<T>(val v: T)
4451 fun <T> wrap(x: T): Box<T> = Box(x)",
4452 "foo.kt",
4453 |metric| {
4454 // No comparisons — only generic brackets.
4455 assert_eq!(metric.abc.conditions_sum(), 0);
4456 insta::assert_json_snapshot!(metric.abc);
4457 },
4458 );
4459 }
4460
4461 #[test]
4462 fn kotlin_class_with_methods_and_branches() {
4463 check_metrics::<KotlinParser>(
4464 "class C {
4465 var counter: Int = 0
4466 fun bump() {
4467 counter += 1
4468 println(counter)
4469 }
4470 }",
4471 "foo.kt",
4472 |metric| {
4473 // assignments: var counter = 0 (+1), counter += 1 (+1) = 2
4474 // branches: println(counter) = 1
4475 assert_eq!(metric.abc.assignments_sum(), 2);
4476 assert_eq!(metric.abc.branches_sum(), 1);
4477 assert_eq!(metric.abc.conditions_sum(), 0);
4478 insta::assert_json_snapshot!(metric.abc);
4479 },
4480 );
4481 }
4482
4483 #[test]
4484 fn kotlin_object_singleton_abc() {
4485 check_metrics::<KotlinParser>(
4486 "object Util {
4487 fun work(x: Int): Int {
4488 var y = x
4489 y += 1
4490 if (y > 0) {
4491 return y
4492 }
4493 return -1
4494 }
4495 }",
4496 "foo.kt",
4497 |metric| {
4498 // assignments: var y = x (+1), y += 1 (+1) = 2
4499 // branches: 0 (return is not a call)
4500 // conditions: y > 0 (+1) = 1
4501 assert_eq!(metric.abc.assignments_sum(), 2);
4502 assert_eq!(metric.abc.branches_sum(), 0);
4503 assert_eq!(metric.abc.conditions_sum(), 1);
4504 insta::assert_json_snapshot!(metric.abc);
4505 },
4506 );
4507 }
4508
4509 #[test]
4510 fn kotlin_interface_abc() {
4511 // Pure-abstract interface with no bodies — all-zero.
4512 check_metrics::<KotlinParser>(
4513 "interface I {
4514 fun work(): Int
4515 fun describe(): String
4516 }",
4517 "foo.kt",
4518 |metric| {
4519 assert_eq!(metric.abc.assignments_sum(), 0);
4520 assert_eq!(metric.abc.branches_sum(), 0);
4521 assert_eq!(metric.abc.conditions_sum(), 0);
4522 insta::assert_json_snapshot!(metric.abc);
4523 },
4524 );
4525 }
4526
4527 #[test]
4528 fn kotlin_nested_class_abc() {
4529 check_metrics::<KotlinParser>(
4530 "class Outer {
4531 var o: Int = 0
4532 class Nested {
4533 var n: Int = 0
4534 fun bump() { n += 1 }
4535 }
4536 }",
4537 "foo.kt",
4538 |metric| {
4539 // Outer: var o = 0 (+1)
4540 // Nested: var n = 0 (+1), n += 1 (+1) = 2
4541 // total assignments = 3
4542 assert_eq!(metric.abc.assignments_sum(), 3);
4543 insta::assert_json_snapshot!(metric.abc);
4544 },
4545 );
4546 }
4547
4548 #[test]
4549 fn kotlin_data_class_abc() {
4550 // `data class` with primary-constructor `val`s — no assignments
4551 // (vals don't count) and no body conditions.
4552 check_metrics::<KotlinParser>(
4553 "data class Point(val x: Int, val y: Int)",
4554 "foo.kt",
4555 |metric| {
4556 assert_eq!(metric.abc.assignments_sum(), 0);
4557 assert_eq!(metric.abc.branches_sum(), 0);
4558 assert_eq!(metric.abc.conditions_sum(), 0);
4559 insta::assert_json_snapshot!(metric.abc);
4560 },
4561 );
4562 }
4563
4564 #[test]
4565 fn kotlin_primary_constructor_default_value_not_assignment() {
4566 // Regression: default values on primary-constructor `val`
4567 // parameters are initialisers, not assignments. Without
4568 // `ClassParameter` pushing a declaration sentinel, the `=` token
4569 // here would be counted unconditionally as a standalone
4570 // assignment.
4571 check_metrics::<KotlinParser>("class C(val a: Int = 5)", "foo.kt", |metric| {
4572 // `val a = 5` → suppressed (Const sentinel).
4573 assert_eq!(metric.abc.assignments_sum(), 0);
4574 insta::assert_json_snapshot!(metric.abc);
4575 });
4576 }
4577
4578 #[test]
4579 fn kotlin_unary_conditions_in_chain() {
4580 // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
4581 // `&&` / `||` chain is one condition. `a && b || c` → a, b, c each
4582 // contribute one; the `&&` / `||` operators contribute nothing.
4583 // expected: 3 unary conditions, no comparisons, no `if`-keyword
4584 // condition in Kotlin (matches the Java byte-equivalent of 3).
4585 check_metrics::<KotlinParser>(
4586 "fun f(a: Boolean, b: Boolean, c: Boolean) {
4587 if (a && b || c) { println(\"x\") }
4588 }",
4589 "foo.kt",
4590 |metric| {
4591 assert_eq!(metric.abc.conditions_sum(), 3);
4592 },
4593 );
4594 }
4595
4596 #[test]
4597 fn kotlin_comparison_operands_add_nothing() {
4598 // Isolation check: comparison operands of a `&&` chain are nested
4599 // `binary_expression` nodes, not bare boolean leaves, so the
4600 // walker adds nothing — only the two `>` comparisons count.
4601 // expected: 2 (the two `>` tokens), walker contributes 0.
4602 check_metrics::<KotlinParser>(
4603 "fun g(x: Int, y: Int) {
4604 if (x > 0 && y > 0) { println(\"x\") }
4605 }",
4606 "foo.kt",
4607 |metric| {
4608 assert_eq!(metric.abc.conditions_sum(), 2);
4609 },
4610 );
4611 }
4612
4613 #[test]
4614 fn kotlin_negated_operand_is_unary_condition() {
4615 // A `!`-negated operand is still a unary condition: `a && !b`
4616 // unwraps the `unary_expression` to reach the inner identifier.
4617 // expected: 2 (`a` and the `!b` operand).
4618 check_metrics::<KotlinParser>(
4619 "fun f(a: Boolean, b: Boolean) {
4620 if (a && !b) { println(\"x\") }
4621 }",
4622 "foo.kt",
4623 |metric| {
4624 assert_eq!(metric.abc.conditions_sum(), 2);
4625 },
4626 );
4627 }
4628
4629 #[test]
4630 fn kotlin_bare_if_predicate_is_one_condition() {
4631 // Issue #773: a bare-boolean `if` predicate (`if (flag)`) is one
4632 // Fitzpatrick unary condition. Before the Phase-2B arm it counted
4633 // 0, so `if (flag) 1 else -1` scored 1 (only the `else`) instead of
4634 // 2, dropping below Kotlin's own cyclomatic decision count.
4635 // expected: predicate (1) + else (1) = 2.
4636 check_metrics::<KotlinParser>(
4637 "fun m(flag: Boolean): Int { return if (flag) 1 else -1 }",
4638 "foo.kt",
4639 |metric| {
4640 assert_eq!(metric.abc.conditions_sum(), 2);
4641 },
4642 );
4643 }
4644
4645 #[test]
4646 fn kotlin_bare_while_predicate_is_one_condition() {
4647 // Issue #773: the bare predicate of a `while` loop counts one
4648 // condition via the `condition` field. expected: 1.
4649 check_metrics::<KotlinParser>(
4650 "fun m(running: Boolean) { while (running) { println(\"x\") } }",
4651 "foo.kt",
4652 |metric| {
4653 assert_eq!(metric.abc.conditions_sum(), 1);
4654 },
4655 );
4656 }
4657
4658 #[test]
4659 fn kotlin_bare_do_while_predicate_is_one_condition() {
4660 // Issue #773: the bare predicate of a `do`/`while` loop counts one
4661 // condition. expected: 1.
4662 check_metrics::<KotlinParser>(
4663 "fun m(ok: Boolean) { do { println(\"x\") } while (ok) }",
4664 "foo.kt",
4665 |metric| {
4666 assert_eq!(metric.abc.conditions_sum(), 1);
4667 },
4668 );
4669 }
4670
4671 #[test]
4672 fn kotlin_comparison_predicate_not_double_counted() {
4673 // Double-count guard (#773): a comparison predicate (`if (a == b)`)
4674 // is a nested `binary_expression` already counted by the `==` token
4675 // arm, so the Phase-2B condition-slot arm must add nothing here.
4676 // expected: `==` (1) + else (1) = 2 — unchanged by the new arm.
4677 check_metrics::<KotlinParser>(
4678 "fun m(a: Int, b: Int): Int { return if (a == b) 1 else -1 }",
4679 "foo.kt",
4680 |metric| {
4681 assert_eq!(metric.abc.conditions_sum(), 2);
4682 },
4683 );
4684 }
4685
4686 #[test]
4687 fn kotlin_short_circuit_predicate_not_double_counted() {
4688 // Double-count guard (#773): an `&&`/`||` predicate is counted by
4689 // the Rule 9 chain walker (each operand once); the Phase-2B arm
4690 // must add nothing for it. expected: `x` (1) + `y` (1) + else (1)
4691 // = 3 — unchanged by the new arm.
4692 check_metrics::<KotlinParser>(
4693 "fun m(x: Boolean, y: Boolean): Int { return if (x && y) 1 else -1 }",
4694 "foo.kt",
4695 |metric| {
4696 assert_eq!(metric.abc.conditions_sum(), 3);
4697 },
4698 );
4699 }
4700
4701 #[test]
4702 fn kotlin_parenthesised_bare_predicate_is_one_condition() {
4703 // A parenthesised bare predicate (`if ((flag))`) is unwrapped by
4704 // `kotlin_inspect_container` and still counts one condition (#773).
4705 // expected: 1.
4706 check_metrics::<KotlinParser>(
4707 "fun m(flag: Boolean) { if ((flag)) { println(\"x\") } }",
4708 "foo.kt",
4709 |metric| {
4710 assert_eq!(metric.abc.conditions_sum(), 1);
4711 },
4712 );
4713 }
4714
4715 // --- TypeScript / TSX ABC tests --------------------------------------
4716 //
4717 // Assignment, branch, condition counting per Fitzpatrick:
4718 // - Augmented assignment / `++` / `--` always count.
4719 // - Plain `=` counts unless inside `const` declaration.
4720 // - `call_expression` / `new_expression` count as branches.
4721 // - Comparison / equality operators, ternary `?`, `??`, control-flow
4722 // arms (`else`, `case`, `default`, `catch`, `try`, `instanceof`),
4723 // and `<`/`>` (outside `type_arguments` / `type_parameters`) count
4724 // as conditions.
4725
4726 #[test]
4727 fn typescript_assignments_basic() {
4728 check_metrics::<TypescriptParser>(
4729 "class C {
4730 m(): void {
4731 let x = 0; // +1 — only a `const` initializer is suppressed
4732 x = 1; // +1
4733 x += 2; // +1
4734 x++; // +1
4735 }
4736 }",
4737 "foo.ts",
4738 |metric| {
4739 assert_eq!(metric.abc.assignments_sum(), 4);
4740 insta::assert_json_snapshot!(metric.abc);
4741 },
4742 );
4743 }
4744
4745 #[test]
4746 fn typescript_const_excluded_from_assignments() {
4747 check_metrics::<TypescriptParser>(
4748 "class C {
4749 m(): void {
4750 const a = 1; // suppressed (`const` initializer)
4751 const b = 2; // suppressed
4752 let c = 3; // +1 — `let` initializers count
4753 }
4754 }",
4755 "foo.ts",
4756 |metric| {
4757 assert_eq!(metric.abc.assignments_sum(), 1);
4758 insta::assert_json_snapshot!(metric.abc);
4759 },
4760 );
4761 }
4762
4763 // Regression cluster for #1277. The pre-fix implementation decided
4764 // "is this `=` a `const` initializer?" from a sentinel stack cleared
4765 // only on a `SEMI` token, so the answer for one statement depended on
4766 // the *previous* statement's terminator. Automatic semicolon
4767 // insertion makes that terminator optional in all four JS-family
4768 // languages. The replacement is structural — see
4769 // `impl_js_family_const_binding!` in `src/metrics/abc/js_family.rs`.
4770
4771 #[test]
4772 fn typescript_asi_const_does_not_suppress_later_assignments() {
4773 check_metrics::<TypescriptParser>(
4774 "function f() {
4775 const a = 1
4776 x = 2
4777 y = 3
4778 }",
4779 "foo.ts",
4780 |metric| {
4781 // `const a = 1` suppressed; `x = 2` and `y = 3` count.
4782 // Pre-#1277 this reported 0: the unterminated `const`
4783 // never popped its sentinel.
4784 assert_eq!(metric.abc.assignments_sum(), 2);
4785 },
4786 );
4787 }
4788
4789 #[test]
4790 fn typescript_semicolon_const_does_not_suppress_later_assignments() {
4791 // The semicolon-terminated spelling of the fixture above, which
4792 // must score the same as it. Pre-#1277 they scored 2 and 0 — the
4793 // pair is what pins the terminator out of the answer.
4794 check_metrics::<TypescriptParser>(
4795 "function f() {
4796 const a = 1;
4797 x = 2;
4798 y = 3;
4799 }",
4800 "foo.ts",
4801 |metric| {
4802 assert_eq!(metric.abc.assignments_sum(), 2);
4803 },
4804 );
4805 }
4806
4807 #[test]
4808 fn typescript_as_const_does_not_suppress_later_assignments() {
4809 // The sentinel stack was also reachable from the other side: the
4810 // `const` token of a TypeScript `x as const` assertion promoted a
4811 // live `let` slot to `Const` and suppressed every `=` until the
4812 // next `;`. Structurally that `const` is a child of an
4813 // `as_expression`, not a declaration keyword.
4814 check_metrics::<TypescriptParser>(
4815 "function f(x: number) {
4816 let y = x as const
4817 w = 3
4818 }",
4819 "foo.ts",
4820 |metric| {
4821 // `let` initializer (+1) and `w = 3` (+1); pre-#1277: 1.
4822 assert_eq!(metric.abc.assignments_sum(), 2);
4823 },
4824 );
4825 }
4826
4827 #[test]
4828 fn typescript_const_declarator_shapes_stay_suppressed() {
4829 // Shapes the sentinel stack handled implicitly, which the
4830 // structural predicate must reproduce: *every* declarator under a
4831 // `const`-bearing `lexical_declaration` is suppressed, including
4832 // the second element of a multi-declarator list, a destructuring
4833 // pattern, and the defaults inside one — `c = 5`, the nested
4834 // `e = 6` and its `= {}`, `g = 7`, and the rest element's `h = 8`,
4835 // each one more pattern layer the predicate's climb has to cross
4836 // — while `for (const x of xs)` carries no `=` at all. Only
4837 // `let i = 3` and `var j = 4` count — the deliberate deviation
4838 // documented on `js_abc_compute!`. Gating on
4839 // `variable_declaration` instead of `lexical_declaration`,
4840 // dropping the `const` check, or stopping the climb at the
4841 // declarator's own `=`, each flips one of these rows. The
4842 // `for`-of row is the exception: it carries no `=`, so no change
4843 // to the predicate can move it. It pins the grammar shape instead
4844 // — a future grammar emitting a `variable_declarator` there would
4845 // start suppressing something that never counted.
4846 check_metrics::<TypescriptParser>(
4847 "function f(o: any, xs: number[]) {
4848 const a = 1, b = 2
4849 const {c = 5, d: {e = 6} = {}} = o
4850 const [g = 7, ...[h = 8]] = xs
4851 let i = 3
4852 var j = 4
4853 for (const x of xs) { k(x) }
4854 }",
4855 "foo.ts",
4856 |metric| {
4857 assert_eq!(metric.abc.assignments_sum(), 2);
4858 },
4859 );
4860 }
4861
4862 #[test]
4863 fn typescript_const_initializer_value_assignments_still_count() {
4864 // TypeScript half of
4865 // `javascript_const_initializer_value_assignments_still_count`.
4866 check_metrics::<TypescriptParser>(
4867 "function m(o: any, a: any, b: any) { const x = (o.p = 1); const y = a || (b = 2); }",
4868 "foo.ts",
4869 |metric| {
4870 assert_eq!(metric.abc.assignments_sum(), 2);
4871 },
4872 );
4873 }
4874
4875 #[test]
4876 fn typescript_branches_function_calls() {
4877 check_metrics::<TypescriptParser>(
4878 "class C {
4879 m(): void {
4880 foo(); // +1
4881 bar(1, 2); // +1
4882 new Date(); // +1
4883 }
4884 }",
4885 "foo.ts",
4886 |metric| {
4887 assert_eq!(metric.abc.branches_sum(), 3);
4888 insta::assert_json_snapshot!(metric.abc);
4889 },
4890 );
4891 }
4892
4893 #[test]
4894 fn typescript_conditions_comparison_operators() {
4895 check_metrics::<TypescriptParser>(
4896 "class C {
4897 m(x: number, y: number): boolean {
4898 return x == y // +1
4899 || x === y // +1
4900 || x != y // +1
4901 || x !== y // +1
4902 || x < y // +1
4903 || x <= y // +1
4904 || x > y // +1
4905 || x >= y; // +1
4906 }
4907 }",
4908 "foo.ts",
4909 |metric| {
4910 assert_eq!(metric.abc.conditions_sum(), 8);
4911 insta::assert_json_snapshot!(metric.abc);
4912 },
4913 );
4914 }
4915
4916 #[test]
4917 fn typescript_conditions_control_flow_arms() {
4918 check_metrics::<TypescriptParser>(
4919 "class C {
4920 m(x: number): number {
4921 try { // +1 (try)
4922 if (x > 0) { // +1 (>)
4923 return 1;
4924 } else { // +1 (else)
4925 return -1;
4926 }
4927 } catch (e) { // +1 (catch)
4928 return 0;
4929 }
4930 }
4931 }",
4932 "foo.ts",
4933 |metric| {
4934 assert_eq!(metric.abc.conditions_sum(), 4);
4935 insta::assert_json_snapshot!(metric.abc);
4936 },
4937 );
4938 }
4939
4940 #[test]
4941 fn typescript_conditions_switch_case() {
4942 check_metrics::<TypescriptParser>(
4943 "class C {
4944 m(x: number): number {
4945 switch (x) {
4946 case 1: // +1
4947 return 1;
4948 case 2: // +1
4949 return 2;
4950 default: // +0 (fallthrough, #469)
4951 return 0;
4952 }
4953 }
4954 }",
4955 "foo.ts",
4956 |metric| {
4957 assert_eq!(metric.abc.conditions_sum(), 2);
4958 insta::assert_json_snapshot!(metric.abc);
4959 },
4960 );
4961 }
4962
4963 #[test]
4964 fn typescript_ternary_and_nullish() {
4965 check_metrics::<TypescriptParser>(
4966 "class C {
4967 m(x: number | null): number {
4968 return x !== null // +1 (!==)
4969 ? x // +1 (ternary ?)
4970 : 0;
4971 }
4972 n(x: number | null): number {
4973 return x ?? 0; // +1 (??)
4974 }
4975 }",
4976 "foo.ts",
4977 |metric| {
4978 assert_eq!(metric.abc.conditions_sum(), 3);
4979 insta::assert_json_snapshot!(metric.abc);
4980 },
4981 );
4982 }
4983
4984 #[test]
4985 fn typescript_instanceof_counts_as_condition() {
4986 check_metrics::<TypescriptParser>(
4987 "class C {
4988 m(o: unknown): boolean {
4989 return o instanceof C; // +1
4990 }
4991 }",
4992 "foo.ts",
4993 |metric| {
4994 assert_eq!(metric.abc.conditions_sum(), 1);
4995 insta::assert_json_snapshot!(metric.abc);
4996 },
4997 );
4998 }
4999
5000 #[test]
5001 fn typescript_generic_lt_gt_not_a_condition() {
5002 // `<T>` in `class C<T>` and `Array<number>` should not contribute
5003 // to conditions even though the tokens are `<` and `>`.
5004 check_metrics::<TypescriptParser>(
5005 "class C<T> {
5006 xs: Array<number> = [];
5007 m(): void {
5008 const arr: Array<string> = []; // suppressed const
5009 void arr;
5010 }
5011 }",
5012 "foo.ts",
5013 |metric| {
5014 assert_eq!(metric.abc.conditions_sum(), 0);
5015 insta::assert_json_snapshot!(metric.abc);
5016 },
5017 );
5018 }
5019
5020 // #1275, TypeScript half. Eleven grammar productions emit a bare
5021 // `?` and only `ternary_expression` is a decision; the other ten are
5022 // type syntax. This fixture exercises five of them — `optional_
5023 // parameter`, `property_signature`, `method_signature`,
5024 // `abstract_method_signature` and `public_field_definition`, plus an
5025 // `optional_type` in a tuple — against one real ternary over one
5026 // real `>`.
5027 //
5028 // Six type-syntax `?` means the numbers separate cleanly: 8 pre-fix,
5029 // 2 once the allowlist is aimed at `TernaryExpression`, 1 if it is
5030 // aimed at anything else (the ternary stops counting too), 0 if the
5031 // fixture stops parsing. Any partial gate — one that named some of
5032 // the type-syntax parents in a denylist instead — lands between 3
5033 // and 7 and is equally visible.
5034 #[test]
5035 fn typescript_optional_type_syntax_is_not_a_condition() {
5036 check_metrics::<TypescriptParser>(
5037 "interface I { a?: string; m?(x: number): void; }
5038 abstract class K { f?: number; abstract g?(): void; }
5039 type Tup = [number, string?];
5040 function h(x?: number, y: number = 0): number { return y > 1 ? 1 : 2; }",
5041 "foo.ts",
5042 |metric| {
5043 assert_eq!(metric.abc.conditions_sum(), 2);
5044 },
5045 );
5046 }
5047
5048 // The explicit half of #1275's TypeScript decision: a conditional
5049 // type (`T extends U ? X : Y`) is resolved by the type checker and
5050 // erased before runtime, so its `?` is not a condition. That falls
5051 // out of the `TernaryExpression` allowlist rather than being named,
5052 // which is exactly why it needs its own test — the choice reads as
5053 // an omission otherwise, and nothing else here would notice if a
5054 // later edit added `ConditionalType` to the allowlist "for
5055 // symmetry".
5056 //
5057 // The real ternary below keeps the expectation off zero: 3 pre-fix,
5058 // 2 with the conditional type excluded, 1 if the ternary is
5059 // swallowed too.
5060 #[test]
5061 fn typescript_conditional_type_is_not_a_condition() {
5062 check_metrics::<TypescriptParser>(
5063 "type Cond<T> = T extends string ? number : boolean;
5064 function pick(a: number, b: number): number { return a > b ? 1 : 2; }",
5065 "foo.ts",
5066 |metric| {
5067 assert_eq!(metric.abc.conditions_sum(), 2);
5068 },
5069 );
5070 }
5071
5072 // grammar-dispatch §2. `Typescript::QMARK2` is the enum entry for
5073 // the external scanner's `_ternary_qmark` token, and
5074 // tree-sitter-typescript's public symbol map folds it back onto
5075 // `anon_sym_QMARK`, so `kind_id()` never reports it — every ternary
5076 // `?` in the fixture below arrives as plain `QMARK`. Listing
5077 // `QMARK2` in the gated arm would therefore be dead code, and
5078 // leaving it out is safe only for as long as that mapping holds.
5079 // This pins both halves so a grammar bump that starts exposing the
5080 // alias fails here rather than silently zeroing every TypeScript
5081 // ternary.
5082 //
5083 // The fixture's only `?` is the ternary's, deliberately. An optional
5084 // parameter (`b?: number`) would emit a `QMARK` of its own and
5085 // satisfy the positive assertion on its own, leaving it true no
5086 // matter what id the ternary's `?` came back as — decoration rather
5087 // than the non-vacuity guard it is here for.
5088 #[test]
5089 fn typescript_ternary_qmark_alias_stays_unreachable() {
5090 let parser = TypescriptParser::new(
5091 "function f(a: boolean): number { return a ? 1 : 2; }\n"
5092 .as_bytes()
5093 .to_vec(),
5094 std::path::Path::new("foo.ts"),
5095 None,
5096 );
5097 assert!(ast_has_kind_id(&parser, Typescript::QMARK as u16));
5098 assert!(!ast_has_kind_id(&parser, Typescript::QMARK2 as u16));
5099 }
5100
5101 #[test]
5102 fn typescript_abstract_class_abc() {
5103 // Abstract methods have no body — they contribute nothing.
5104 check_metrics::<TypescriptParser>(
5105 "abstract class C {
5106 abstract a(): void;
5107 m(x: number): number {
5108 if (x > 0) return 1; // +1 condition
5109 return 0;
5110 }
5111 }",
5112 "foo.ts",
5113 |metric| {
5114 assert_eq!(metric.abc.conditions_sum(), 1);
5115 assert_eq!(metric.abc.branches_sum(), 0);
5116 insta::assert_json_snapshot!(metric.abc);
5117 },
5118 );
5119 }
5120
5121 #[test]
5122 fn typescript_interface_abc_zero() {
5123 check_metrics::<TypescriptParser>(
5124 "interface I {
5125 a(): void;
5126 b(): number;
5127 p: string;
5128 }",
5129 "foo.ts",
5130 |metric| {
5131 assert_eq!(metric.abc.assignments_sum(), 0);
5132 assert_eq!(metric.abc.branches_sum(), 0);
5133 assert_eq!(metric.abc.conditions_sum(), 0);
5134 insta::assert_json_snapshot!(metric.abc);
5135 },
5136 );
5137 }
5138
5139 #[test]
5140 fn typescript_arrow_field_contributes_abc() {
5141 // Arrow function class members are function spaces; their
5142 // assignments/branches/conditions are counted.
5143 check_metrics::<TypescriptParser>(
5144 "class C {
5145 arrow = (x: number) => {
5146 if (x > 0) { // +1 condition
5147 return foo(); // +1 branch
5148 }
5149 return 0;
5150 };
5151 }",
5152 "foo.ts",
5153 |metric| {
5154 assert_eq!(metric.abc.conditions_sum(), 1);
5155 assert_eq!(metric.abc.branches_sum(), 1);
5156 insta::assert_json_snapshot!(metric.abc);
5157 },
5158 );
5159 }
5160
5161 #[test]
5162 fn typescript_parameter_property_init_not_assignment() {
5163 // Parameter properties don't introduce a `=` token themselves;
5164 // only the explicit `let z = 0` body assignment is counted.
5165 // The class field initializer `f: number = 0` likewise has a `=`
5166 // that DOES count (matches `typescript_assignments_basic`).
5167 check_metrics::<TypescriptParser>(
5168 "class C {
5169 f: number = 0;
5170 constructor(public x: number, private y: string) {
5171 let z = 0;
5172 }
5173 }",
5174 "foo.ts",
5175 |metric| {
5176 // f's initializer + `let z = 0` = 2 assignments; the
5177 // parameter properties contribute zero.
5178 assert_eq!(metric.abc.assignments_sum(), 2);
5179 insta::assert_json_snapshot!(metric.abc);
5180 },
5181 );
5182 }
5183
5184 // TSX parity
5185
5186 #[test]
5187 fn tsx_assignments_basic() {
5188 check_metrics::<TsxParser>(
5189 "class C {
5190 m(): void {
5191 let x = 0;
5192 x = 1;
5193 x += 2;
5194 x++;
5195 }
5196 }",
5197 "foo.tsx",
5198 |metric| {
5199 assert_eq!(metric.abc.assignments_sum(), 4);
5200 insta::assert_json_snapshot!(metric.abc);
5201 },
5202 );
5203 }
5204
5205 #[test]
5206 fn tsx_const_excluded_from_assignments() {
5207 check_metrics::<TsxParser>(
5208 "class C {
5209 m(): void {
5210 const a = 1;
5211 let b = 2;
5212 }
5213 }",
5214 "foo.tsx",
5215 |metric| {
5216 assert_eq!(metric.abc.assignments_sum(), 1);
5217 insta::assert_json_snapshot!(metric.abc);
5218 },
5219 );
5220 }
5221
5222 #[test]
5223 fn tsx_branches_function_calls() {
5224 check_metrics::<TsxParser>(
5225 "class C {
5226 m(): void {
5227 foo();
5228 new Date();
5229 }
5230 }",
5231 "foo.tsx",
5232 |metric| {
5233 assert_eq!(metric.abc.branches_sum(), 2);
5234 insta::assert_json_snapshot!(metric.abc);
5235 },
5236 );
5237 }
5238
5239 #[test]
5240 fn tsx_conditions_comparison_operators() {
5241 check_metrics::<TsxParser>(
5242 "class C {
5243 m(x: number, y: number): boolean {
5244 return x == y || x < y || x >= y;
5245 }
5246 }",
5247 "foo.tsx",
5248 |metric| {
5249 assert_eq!(metric.abc.conditions_sum(), 3);
5250 insta::assert_json_snapshot!(metric.abc);
5251 },
5252 );
5253 }
5254
5255 #[test]
5256 fn tsx_conditions_control_flow_arms() {
5257 check_metrics::<TsxParser>(
5258 "class C {
5259 m(x: number): number {
5260 try {
5261 if (x > 0) return 1;
5262 else return -1;
5263 } catch (e) {
5264 return 0;
5265 }
5266 }
5267 }",
5268 "foo.tsx",
5269 |metric| {
5270 assert_eq!(metric.abc.conditions_sum(), 4);
5271 insta::assert_json_snapshot!(metric.abc);
5272 },
5273 );
5274 }
5275
5276 #[test]
5277 fn tsx_conditions_switch_case() {
5278 check_metrics::<TsxParser>(
5279 "class C {
5280 m(x: number): number {
5281 switch (x) {
5282 case 1: return 1; // +1
5283 case 2: return 2; // +1
5284 default: return 0; // +0 (fallthrough, #469)
5285 }
5286 }
5287 }",
5288 "foo.tsx",
5289 |metric| {
5290 assert_eq!(metric.abc.conditions_sum(), 2);
5291 insta::assert_json_snapshot!(metric.abc);
5292 },
5293 );
5294 }
5295
5296 #[test]
5297 fn tsx_ternary_and_nullish() {
5298 check_metrics::<TsxParser>(
5299 "class C {
5300 m(x: number | null): number {
5301 return x !== null ? x : 0;
5302 }
5303 n(x: number | null): number { return x ?? 0; }
5304 }",
5305 "foo.tsx",
5306 |metric| {
5307 assert_eq!(metric.abc.conditions_sum(), 3);
5308 insta::assert_json_snapshot!(metric.abc);
5309 },
5310 );
5311 }
5312
5313 #[test]
5314 fn tsx_instanceof_counts_as_condition() {
5315 check_metrics::<TsxParser>(
5316 "class C { m(o: unknown): boolean { return o instanceof C; } }",
5317 "foo.tsx",
5318 |metric| {
5319 assert_eq!(metric.abc.conditions_sum(), 1);
5320 insta::assert_json_snapshot!(metric.abc);
5321 },
5322 );
5323 }
5324
5325 #[test]
5326 fn tsx_generic_lt_gt_not_a_condition() {
5327 check_metrics::<TsxParser>(
5328 "class C<T> { xs: Array<number> = []; }",
5329 "foo.tsx",
5330 |metric| {
5331 assert_eq!(metric.abc.conditions_sum(), 0);
5332 insta::assert_json_snapshot!(metric.abc);
5333 },
5334 );
5335 }
5336
5337 // #1275 in the second expansion of `ts_abc_compute!`. TSX shares
5338 // TypeScript's `?` productions and its own `TernaryExpression` /
5339 // `QMARK` ids, so the gate is a distinct instantiation and needs its
5340 // own fixture — a passing TypeScript test says nothing about the
5341 // macro's other expansion. Four type-syntax `?` plus one `>` and one
5342 // ternary: 6 pre-fix, 2 after, 1 if the allowlist is misaimed.
5343 #[test]
5344 fn tsx_optional_type_syntax_is_not_a_condition() {
5345 check_metrics::<TsxParser>(
5346 "interface I { a?: string; m?(x: number): void; }
5347 class K { f?: number; }
5348 function h(x?: number, y: number = 0): number { return y > 1 ? 1 : 2; }",
5349 "foo.tsx",
5350 |metric| {
5351 assert_eq!(metric.abc.conditions_sum(), 2);
5352 },
5353 );
5354 }
5355
5356 // The TSX half of `typescript_conditional_type_is_not_a_condition`.
5357 // `ts_abc_compute!` expands twice and the two expansions are
5358 // independent code (grammar-dispatch §11): adding `ConditionalType`
5359 // to the allowlist "for symmetry" fails only the TypeScript test
5360 // without this one, which reads as the TSX expansion being fine
5361 // rather than untested. `conditional_type` is in the tsx grammar's
5362 // `?` set exactly as it is in typescript's.
5363 #[test]
5364 fn tsx_conditional_type_is_not_a_condition() {
5365 check_metrics::<TsxParser>(
5366 "type Cond<T> = T extends string ? number : boolean;
5367 function pick(a: number, b: number): number { return a > b ? 1 : 2; }",
5368 "foo.tsx",
5369 |metric| {
5370 assert_eq!(metric.abc.conditions_sum(), 2);
5371 },
5372 );
5373 }
5374
5375 // The TSX half of `typescript_ternary_qmark_alias_stays_unreachable`
5376 // — the tsx grammar declares the same `_ternary_qmark` external and
5377 // maps it back onto `anon_sym_QMARK` at its own id. Same
5378 // single-`?` fixture rule; see that test for why.
5379 #[test]
5380 fn tsx_ternary_qmark_alias_stays_unreachable() {
5381 let parser = TsxParser::new(
5382 "function f(a: boolean): number { return a ? 1 : 2; }\n"
5383 .as_bytes()
5384 .to_vec(),
5385 std::path::Path::new("foo.tsx"),
5386 None,
5387 );
5388 assert!(ast_has_kind_id(&parser, Tsx::QMARK as u16));
5389 assert!(!ast_has_kind_id(&parser, Tsx::QMARK2 as u16));
5390 }
5391
5392 #[test]
5393 fn tsx_abstract_class_abc() {
5394 check_metrics::<TsxParser>(
5395 "abstract class C {
5396 abstract a(): void;
5397 m(x: number): number {
5398 if (x > 0) return 1;
5399 return 0;
5400 }
5401 }",
5402 "foo.tsx",
5403 |metric| {
5404 assert_eq!(metric.abc.conditions_sum(), 1);
5405 assert_eq!(metric.abc.branches_sum(), 0);
5406 insta::assert_json_snapshot!(metric.abc);
5407 },
5408 );
5409 }
5410
5411 #[test]
5412 fn tsx_interface_abc_zero() {
5413 check_metrics::<TsxParser>(
5414 "interface I { a(): void; p: string; }",
5415 "foo.tsx",
5416 |metric| {
5417 assert_eq!(metric.abc.assignments_sum(), 0);
5418 assert_eq!(metric.abc.branches_sum(), 0);
5419 assert_eq!(metric.abc.conditions_sum(), 0);
5420 insta::assert_json_snapshot!(metric.abc);
5421 },
5422 );
5423 }
5424
5425 #[test]
5426 fn tsx_arrow_field_contributes_abc() {
5427 check_metrics::<TsxParser>(
5428 "class C {
5429 arrow = (x: number) => {
5430 if (x > 0) return foo();
5431 return 0;
5432 };
5433 }",
5434 "foo.tsx",
5435 |metric| {
5436 assert_eq!(metric.abc.conditions_sum(), 1);
5437 assert_eq!(metric.abc.branches_sum(), 1);
5438 insta::assert_json_snapshot!(metric.abc);
5439 },
5440 );
5441 }
5442
5443 #[test]
5444 fn tsx_asi_const_does_not_suppress_later_assignments() {
5445 // TSX half of the #1277 cluster; see
5446 // `typescript_asi_const_does_not_suppress_later_assignments`.
5447 // TSX has its own `Const` / `VariableDeclarator` /
5448 // `LexicalDeclaration` kind ids, so the predicate is generated
5449 // separately and needs its own fixture.
5450 check_metrics::<TsxParser>(
5451 "function f() {
5452 const a = 1
5453 x = 2
5454 y = 3
5455 }",
5456 "foo.tsx",
5457 |metric| {
5458 assert_eq!(metric.abc.assignments_sum(), 2);
5459 },
5460 );
5461 }
5462
5463 #[test]
5464 fn tsx_const_declarator_shapes_stay_suppressed() {
5465 // TSX half of `typescript_const_declarator_shapes_stay_suppressed`.
5466 check_metrics::<TsxParser>(
5467 "function f(o: any, xs: number[]) {
5468 const a = 1, b = 2
5469 const {c = 5, d: {e = 6} = {}} = o
5470 const [g = 7, ...[h = 8]] = xs
5471 let i = 3
5472 var j = 4
5473 for (const x of xs) { k(x) }
5474 }",
5475 "foo.tsx",
5476 |metric| {
5477 assert_eq!(metric.abc.assignments_sum(), 2);
5478 },
5479 );
5480 }
5481
5482 #[test]
5483 fn tsx_parameter_property_init_not_assignment() {
5484 // Parameter properties contribute no `=`; the body's `let z = 0`
5485 // and the field initializer do.
5486 check_metrics::<TsxParser>(
5487 "class C {
5488 f: number = 0;
5489 constructor(public x: number) { let z = 0; }
5490 }",
5491 "foo.tsx",
5492 |metric| {
5493 assert_eq!(metric.abc.assignments_sum(), 2);
5494 insta::assert_json_snapshot!(metric.abc);
5495 },
5496 );
5497 }
5498
5499 // --- Ruby ABC tests ---------------------------------------------------
5500 //
5501 // Each Ruby `assignment` / `operator_assignment` is one assignment
5502 // regardless of whether the LHS is a local, instance, or class
5503 // variable. Every `call` / `super` / `yield` is one branch. Every
5504 // comparison-operator token inside a `binary` node plus each
5505 // `else` / `elsif` / `when` / `then` / `?` / `rescue` clause is
5506 // one condition.
5507
5508 #[test]
5509 fn ruby_zero_abc() {
5510 check_metrics::<RubyParser>("\n", "foo.rb", |metric| {
5511 assert_eq!(metric.abc.assignments_sum(), 0);
5512 assert_eq!(metric.abc.branches_sum(), 0);
5513 assert_eq!(metric.abc.conditions_sum(), 0);
5514 insta::assert_json_snapshot!(metric.abc);
5515 });
5516 }
5517
5518 #[test]
5519 fn ruby_simple_assignment() {
5520 check_metrics::<RubyParser>("def f\n a = 1\n b = 2\nend\n", "foo.rb", |metric| {
5521 assert_eq!(metric.abc.assignments_sum(), 2);
5522 assert_eq!(metric.abc.branches_sum(), 0);
5523 assert_eq!(metric.abc.conditions_sum(), 0);
5524 insta::assert_json_snapshot!(metric.abc);
5525 });
5526 }
5527
5528 #[test]
5529 fn ruby_augmented_assignment() {
5530 // `+=`, `-=`, `*=` are `operator_assignment` nodes — each is
5531 // one assignment. Plain `=` to set the initial value adds one
5532 // more.
5533 check_metrics::<RubyParser>(
5534 "def f(x)\n a = 0\n a += x\n a -= 1\n a *= 2\nend\n",
5535 "foo.rb",
5536 |metric| {
5537 assert_eq!(metric.abc.assignments_sum(), 4);
5538 insta::assert_json_snapshot!(metric.abc);
5539 },
5540 );
5541 }
5542
5543 #[test]
5544 fn ruby_logical_augmented_assignment() {
5545 // `||=` and `&&=` are also `operator_assignment` nodes.
5546 check_metrics::<RubyParser>("def f\n @x ||= 0\n @x &&= 1\nend\n", "foo.rb", |metric| {
5547 assert_eq!(metric.abc.assignments_sum(), 2);
5548 insta::assert_json_snapshot!(metric.abc);
5549 });
5550 }
5551
5552 #[test]
5553 fn ruby_method_call_branch() {
5554 // Each method invocation is one branch.
5555 check_metrics::<RubyParser>(
5556 "def f(obj)\n foo()\n obj.bar(1)\nend\n",
5557 "foo.rb",
5558 |metric| {
5559 assert_eq!(metric.abc.branches_sum(), 2);
5560 insta::assert_json_snapshot!(metric.abc);
5561 },
5562 );
5563 }
5564
5565 #[test]
5566 fn ruby_super_and_yield_branches() {
5567 // `super` and `yield` both count as branches (control-pass).
5568 check_metrics::<RubyParser>("def f\n super\n yield\nend\n", "foo.rb", |metric| {
5569 assert_eq!(metric.abc.branches_sum(), 2);
5570 assert_eq!(metric.abc.assignments_sum(), 0);
5571 insta::assert_json_snapshot!(metric.abc);
5572 });
5573 }
5574
5575 #[test]
5576 fn ruby_attr_macro_is_branch() {
5577 // `attr_accessor` is a `Call3` node and registers as a branch
5578 // like any method invocation.
5579 check_metrics::<RubyParser>("class A\n attr_accessor :x\nend\n", "foo.rb", |metric| {
5580 assert_eq!(metric.abc.branches_sum(), 1);
5581 insta::assert_json_snapshot!(metric.abc);
5582 });
5583 }
5584
5585 #[test]
5586 fn ruby_comparison_conditions() {
5587 // Each comparison operator is one condition.
5588 check_metrics::<RubyParser>(
5589 "def f(a, b)\n a == b\n a != b\n a < b\n a > b\n a <= b\n a >= b\nend\n",
5590 "foo.rb",
5591 |metric| {
5592 assert_eq!(metric.abc.conditions_sum(), 6);
5593 insta::assert_json_snapshot!(metric.abc);
5594 },
5595 );
5596 }
5597
5598 #[test]
5599 fn ruby_superclass_clause_is_not_a_condition() {
5600 // Regression for #1280: a superclass clause spells its `<` with the
5601 // same `LT` token as a comparison, but parents it under
5602 // `superclass` rather than `binary`, so the parent gate excludes
5603 // it. Before the gate every subclass declaration scored a phantom
5604 // condition.
5605 // expected: 0 conditions — the file contains no conditional at all;
5606 // the single assignment is `x = 1`.
5607 check_metrics::<RubyParser>(
5608 "class Foo < Bar\n def plain\n x = 1\n end\nend\n",
5609 "foo.rb",
5610 |metric| {
5611 assert_eq!(metric.abc.conditions_sum(), 0);
5612 assert_eq!(metric.abc.assignments_sum(), 1);
5613 },
5614 );
5615 }
5616
5617 #[test]
5618 fn ruby_operator_method_name_is_not_a_condition() {
5619 // The `<` naming an operator method parents under `operator`, which
5620 // the `binary` gate likewise excludes (#1280). The body carries a
5621 // real comparison so the expected value is not the all-zero default:
5622 // 1 discriminates "only the name token is excluded" from both "the
5623 // gate excludes everything" (0) and "nothing is gated" (2).
5624 // expected: 1 condition — the `@v < other` comparison, not the `def <`.
5625 check_metrics::<RubyParser>("def <(other)\n @v < other\nend\n", "foo.rb", |metric| {
5626 assert_eq!(metric.abc.conditions_sum(), 1);
5627 });
5628 }
5629
5630 #[test]
5631 fn ruby_every_comparison_operator_method_name_is_not_a_condition() {
5632 // The sibling half of #1280. `<` is not special: every comparison
5633 // and equality token Ruby lets you `def` parents under `operator`
5634 // in that position, so gating only `LT` / `GT` left `def ==`,
5635 // `def <=`, `def >=`, `def <=>`, `def !=` and `def =~` each
5636 // scoring a phantom condition — measured at 1 apiece for a body
5637 // containing no conditional at all.
5638 // expected: 0 conditions per definition; only the name token is on
5639 // the line, so a single non-zero total localises the regression.
5640 check_metrics::<RubyParser>(
5641 "def ==(o)\n 1\nend\n\
5642 def !=(o)\n 1\nend\n\
5643 def <=(o)\n 1\nend\n\
5644 def >=(o)\n 1\nend\n\
5645 def <=>(o)\n 1\nend\n\
5646 def =~(o)\n 1\nend\n\
5647 def <(o)\n 1\nend\n\
5648 def >(o)\n 1\nend\n",
5649 "foo.rb",
5650 |metric| {
5651 assert_eq!(metric.abc.conditions_sum(), 0);
5652 },
5653 );
5654 // The positive control: the same tokens inside a `binary` are real
5655 // comparisons and still count, so the gate is not blanket
5656 // suppression.
5657 check_metrics::<RubyParser>(
5658 "def cmp(a, b)\n a == b || a <= b || a <=> b\nend\n",
5659 "foo.rb",
5660 |metric| {
5661 assert_eq!(metric.abc.conditions_sum(), 3);
5662 },
5663 );
5664 }
5665
5666 #[test]
5667 fn ruby_case_match_in_arms_are_conditions() {
5668 // Regression for #977: each non-wildcard `case … in` arm is one
5669 // ABC condition, matching Python's `case_clause` handling. Using
5670 // literal patterns (no comparison operators) isolates the
5671 // `in_clause` contribution from any operand tokens.
5672 // expected: 2 conditions — one per `in 1` / `in 2` arm.
5673 check_metrics::<RubyParser>(
5674 "def f(x)\n case x\n in 1 then :one\n in 2 then :two\n end\nend\n",
5675 "foo.rb",
5676 |metric| {
5677 assert_eq!(metric.abc.conditions_sum(), 2);
5678 },
5679 );
5680 }
5681
5682 #[test]
5683 fn ruby_case_match_guarded_wildcard_is_a_condition() {
5684 // Regression for #977: a guarded wildcard arm `in _ if x` is not a
5685 // bare default and counts as one ABC condition, while the trailing
5686 // bare `in _` adds none. The guard predicate here is a bare
5687 // identifier (no comparison operator), so the single counted
5688 // condition is the guarded `in_clause` itself.
5689 // expected: 1 condition — the guarded `in _ if x` arm only.
5690 check_metrics::<RubyParser>(
5691 "def f(x)\n case x\n in _ if x then :y\n in _ then :default\n end\nend\n",
5692 "foo.rb",
5693 |metric| {
5694 assert_eq!(metric.abc.conditions_sum(), 1);
5695 },
5696 );
5697 }
5698
5699 #[test]
5700 fn ruby_case_match_bare_wildcard_is_not_a_condition() {
5701 // Regression for #977: a `case … in` whose only arm is the bare
5702 // wildcard `in _` (no guard) is the default arm and contributes no
5703 // ABC condition, keeping ABC and cyclomatic in lockstep on the
5704 // same construct.
5705 // expected: 0 conditions.
5706 check_metrics::<RubyParser>(
5707 "def f(x)\n case x\n in _ then :default\n end\nend\n",
5708 "foo.rb",
5709 |metric| {
5710 assert_eq!(metric.abc.conditions_sum(), 0);
5711 },
5712 );
5713 }
5714
5715 #[test]
5716 fn ruby_bare_predicate_control_flow_counts_one_condition() {
5717 // Regression for #696: idiomatic Ruby bare predicates
5718 // (`if flag` / `while flag` / `unless flag` / `until flag`) each
5719 // count one unary condition, matching Rust / C# / PHP / Python. The
5720 // condition field is read for both block and modifier forms.
5721 //
5722 // expected: 8 conditions — four block forms (`if`/`unless`/`while`/
5723 // `until`) plus the same four as modifiers, one each.
5724 check_metrics::<RubyParser>(
5725 "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",
5726 "foo.rb",
5727 |metric| {
5728 assert_eq!(metric.abc.conditions_sum(), 8);
5729 },
5730 );
5731 }
5732
5733 #[test]
5734 fn ruby_bare_predicate_does_not_double_count_comparison_or_chain() {
5735 // `if a == b` counts only the `==` comparison (the condition field
5736 // is a `binary` node, adding nothing). `if a && b` counts the two
5737 // chain operands via the `&&` walker, again with the condition-field
5738 // arm adding nothing — so neither shape is double-counted (#696).
5739 //
5740 // expected: 3 — `==` (1) + the `a`,`b` operands of `&&` (2).
5741 check_metrics::<RubyParser>(
5742 "def f(a, b)\n if a == b\n x\n end\n if a && b\n y\n end\nend\n",
5743 "foo.rb",
5744 |metric| {
5745 assert_eq!(metric.abc.conditions_sum(), 3);
5746 },
5747 );
5748 }
5749
5750 #[test]
5751 fn ruby_spaceship_and_case_equality() {
5752 // `<=>` and `===` are comparison operators (conditions).
5753 check_metrics::<RubyParser>(
5754 "def f(a, b)\n a <=> b\n a === b\nend\n",
5755 "foo.rb",
5756 |metric| {
5757 assert_eq!(metric.abc.conditions_sum(), 2);
5758 insta::assert_json_snapshot!(metric.abc);
5759 },
5760 );
5761 }
5762
5763 #[test]
5764 fn ruby_ternary_condition() {
5765 // The `?` ternary marker is one condition; the inner `==` is
5766 // another.
5767 check_metrics::<RubyParser>("def f(x)\n x == 0 ? :z : :nz\nend\n", "foo.rb", |metric| {
5768 assert_eq!(metric.abc.conditions_sum(), 2);
5769 insta::assert_json_snapshot!(metric.abc);
5770 });
5771 }
5772
5773 // Issue #1161. Ruby's ternary carried only the `?` token arm, so
5774 // `a ? !b : !c` scored 1 against the 4 that Java, C#, Groovy, the C
5775 // family, the JS family, PHP and Perl all report for the same
5776 // expression (#1102) — and `ruby_inspect_container`'s `Conditional`
5777 // boolean-context seed was unreachable for the same reason.
5778 //
5779 // Every expectation below is the value its C++ sibling
5780 // (`cpp_ternary_operand_slots_count_as_unary_conditions`) already
5781 // asserts for the same expression, so the two read as one table.
5782 #[test]
5783 fn ruby_ternary_operand_slots_count_as_unary_conditions() {
5784 // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4.
5785 check_metrics::<RubyParser>("def f\n x = a ? !b : !c\nend\n", "foo.rb", |metric| {
5786 assert_eq!(metric.abc.conditions_sum(), 4);
5787 });
5788 // No-double-count pin, and the assertion that catches the trap
5789 // this grammar sets: `-b` and `!b` are the SAME node kind
5790 // (`unary:284`), separated only by child(0). Routing the branch
5791 // slots through `ruby_inspect_container` — which tests for the
5792 // `!` token, not for the kind — is what keeps this at 2. An
5793 // implementation keying on `Unary` reads 3 here.
5794 // `?` (1) + `>` (1) = 2, unchanged by the fix: the parenthesised
5795 // condition unwraps to a `binary`, which is not a boolean
5796 // terminal, and neither branch is negated.
5797 check_metrics::<RubyParser>("def f\n x = (a > 0) ? b : -b\nend\n", "foo.rb", |metric| {
5798 assert_eq!(metric.abc.conditions_sum(), 2);
5799 });
5800 // Nested — Ruby needs the inner ternary parenthesised. Outer `?`
5801 // (1) + outer condition `a` (1) + inner `?` (1) + inner
5802 // condition `b` (1) = 4. The outer consequence unwraps to the
5803 // inner `conditional`, which is neither a boolean terminal nor a
5804 // further paren / `!` layer, so it adds nothing on its own; the
5805 // inner ternary is reached by the walk, not by descent.
5806 check_metrics::<RubyParser>(
5807 "def f\n x = a ? (b ? c : d) : e\nend\n",
5808 "foo.rb",
5809 |metric| {
5810 assert_eq!(metric.abc.conditions_sum(), 4);
5811 },
5812 );
5813 // A parenthesised condition, pinning the `is_parens` unwrap on
5814 // the condition slot: `(a)` is `parenthesized_statements`, not a
5815 // boolean terminal, so it reaches the walker's `else` fallback
5816 // and only `ruby_inspect_container` can resolve it.
5817 // `?` (1) + `(a)` (1) + `!b` (1) + `!c` (1) = 4; drop the
5818 // fallback and this reads 3 while every other case here holds.
5819 check_metrics::<RubyParser>("def f\n x = (a) ? !b : !c\nend\n", "foo.rb", |metric| {
5820 assert_eq!(metric.abc.conditions_sum(), 4);
5821 });
5822 // A negated condition takes the same fallback through the `!`
5823 // unwrap rather than the paren one. `?` (1) + `!a` (1) = 2.
5824 check_metrics::<RubyParser>("def f\n x = !a ? b : c\nend\n", "foo.rb", |metric| {
5825 assert_eq!(metric.abc.conditions_sum(), 2);
5826 });
5827 }
5828
5829 // The boolean-context seed must discriminate between the condition
5830 // slot and the two branch slots — not merely exist. None of the
5831 // fixtures above can tell the difference: their branches are either
5832 // `!`-unaries (which set the flag inside the unwrap loop regardless
5833 // of the seed) or kinds the loop breaks on before any terminal test.
5834 // A seed that returned `true` for every slot of a `Conditional`
5835 // leaves all five at their asserted values and fails nothing.
5836 //
5837 // A parenthesised *branch* is the input that separates them: the
5838 // unwrap reaches a bare terminal, so only the seed decides whether
5839 // it counts. `?` (1) + condition `a` (1) = 2 in both directions.
5840 #[test]
5841 fn ruby_ternary_branch_operands_are_not_double_counted() {
5842 check_metrics::<RubyParser>("def f\n x = a ? (b) : c\nend\n", "foo.rb", |metric| {
5843 assert_eq!(metric.abc.conditions_sum(), 2);
5844 });
5845 check_metrics::<RubyParser>("def f\n x = a ? b : (c)\nend\n", "foo.rb", |metric| {
5846 assert_eq!(metric.abc.conditions_sum(), 2);
5847 });
5848 // The same pair with a comment before the operand. Comments are
5849 // tree-sitter `extras`, so they become the branch's previous
5850 // sibling — which is why the seed asks the grammar which child
5851 // is the `condition` field rather than testing that sibling for
5852 // `?` / `:` as the C family does. Under the token form both of
5853 // these read 3.
5854 check_metrics::<RubyParser>(
5855 "def f\n x = a ?\n # note\n (b) : c\nend\n",
5856 "foo.rb",
5857 |metric| {
5858 assert_eq!(metric.abc.conditions_sum(), 2);
5859 },
5860 );
5861 check_metrics::<RubyParser>(
5862 "def f\n x = a ? b :\n # note\n (c)\nend\n",
5863 "foo.rb",
5864 |metric| {
5865 assert_eq!(metric.abc.conditions_sum(), 2);
5866 },
5867 );
5868 }
5869
5870 #[test]
5871 fn ruby_case_when_arms() {
5872 // Each `when` named clause and the `else` clause count as one
5873 // condition each; the `case` head and the implicit `then`
5874 // wrappers do not.
5875 check_metrics::<RubyParser>(
5876 "def f(x)\n case x\n when 1 then 'one'\n when 2 then 'two'\n else 'other'\n end\nend\n",
5877 "foo.rb",
5878 |metric| {
5879 // 2 `when` + 1 `else` = 3 conditions.
5880 assert_eq!(metric.abc.conditions_sum(), 3);
5881 insta::assert_json_snapshot!(metric.abc);
5882 },
5883 );
5884 }
5885
5886 #[test]
5887 fn ruby_elsif_and_else() {
5888 // `elsif` and `else` named clauses are conditions; their inner
5889 // `then` wrappers are not.
5890 check_metrics::<RubyParser>(
5891 "def f(x)\n if x > 0\n 1\n elsif x < 0\n -1\n else\n 0\n end\nend\n",
5892 "foo.rb",
5893 |metric| {
5894 // `>`(1) + `elsif`(1) + `<`(1) + `else`(1) = 4.
5895 assert_eq!(metric.abc.conditions_sum(), 4);
5896 insta::assert_json_snapshot!(metric.abc);
5897 },
5898 );
5899 }
5900
5901 #[test]
5902 fn ruby_rescue_clause_condition() {
5903 // The `rescue` named clause is one condition; the `rescue`
5904 // keyword token (`Rescue2`) is not counted on its own.
5905 // `do_it` without parens is an `identifier`, not a `call`, so
5906 // it contributes no branch. `handle(e)` is a `call` (1 branch).
5907 check_metrics::<RubyParser>(
5908 "def f\n begin\n do_it\n rescue StandardError => e\n handle(e)\n end\nend\n",
5909 "foo.rb",
5910 |metric| {
5911 assert_eq!(metric.abc.conditions_sum(), 1);
5912 assert_eq!(metric.abc.branches_sum(), 1);
5913 insta::assert_json_snapshot!(metric.abc);
5914 },
5915 );
5916 }
5917
5918 #[test]
5919 fn ruby_class_complex_function() {
5920 // Mixed: assignment(=), branch(call), conditions(`>` and `==`).
5921 check_metrics::<RubyParser>(
5922 "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",
5923 "foo.rb",
5924 |metric| {
5925 assert_eq!(metric.abc.assignments_sum(), 1);
5926 assert_eq!(metric.abc.branches_sum(), 1);
5927 // `>`(1) + `==`(1) = 2 conditions. `if` is not a
5928 // token; `&&` is `AMPAMP` and is not counted (see
5929 // the module-level `Stats` doc-comment for the
5930 // cross-language policy; #395, walker tracked in
5931 // #403).
5932 assert_eq!(metric.abc.conditions_sum(), 2);
5933 insta::assert_json_snapshot!(metric.abc);
5934 },
5935 );
5936 }
5937
5938 #[test]
5939 fn ruby_unary_conditions_in_chain() {
5940 // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
5941 // `&&` / `||` chain is one condition. `a && b || c` → a, b, c each
5942 // contribute one. Ruby's `if` keyword is not a condition token.
5943 // expected: 3 unary conditions (matches the Java byte-equivalent).
5944 check_metrics::<RubyParser>(
5945 "def f(a, b, c)\n if a && b || c\n puts \"x\"\n end\nend\n",
5946 "foo.rb",
5947 |metric| {
5948 assert_eq!(metric.abc.conditions_sum(), 3);
5949 },
5950 );
5951 }
5952
5953 #[test]
5954 fn ruby_keyword_and_or_chain_counts_operands() {
5955 // The keyword forms `and` / `or` get the same Rule 9 treatment as
5956 // `&&` / `||`. expected: 3 unary conditions (a, b, c).
5957 check_metrics::<RubyParser>(
5958 "def f(a, b, c)\n if a and b or c\n puts \"x\"\n end\nend\n",
5959 "foo.rb",
5960 |metric| {
5961 assert_eq!(metric.abc.conditions_sum(), 3);
5962 },
5963 );
5964 }
5965
5966 #[test]
5967 fn ruby_negated_operand_is_unary_condition() {
5968 // A `!`-negated operand unwraps the `unary` node to the inner
5969 // identifier. expected: 2 (`a` and the `!b` operand).
5970 check_metrics::<RubyParser>(
5971 "def f(a, b)\n if a && !b\n puts \"x\"\n end\nend\n",
5972 "foo.rb",
5973 |metric| {
5974 assert_eq!(metric.abc.conditions_sum(), 2);
5975 },
5976 );
5977 }
5978
5979 #[test]
5980 fn ruby_comparison_operands_add_nothing() {
5981 // Isolation for Rule 9 (issue #557): when the `&&` operands are
5982 // themselves comparisons, the unary-condition walker must add
5983 // nothing on top of the two `>` comparisons already counted as
5984 // conditions — distinguishing the gap (bare boolean operands)
5985 // from ordinary relational conditions. Mirrors the Kotlin and
5986 // Elixir isolation tests. expected: 2 (the two `>` comparisons).
5987 check_metrics::<RubyParser>(
5988 "def f(x, y)\n if x > 0 && y > 0\n puts \"x\"\n end\nend\n",
5989 "foo.rb",
5990 |metric| {
5991 assert_eq!(metric.abc.conditions_sum(), 2);
5992 },
5993 );
5994 }
5995
5996 // ---------------------------------------------------------------
5997 // Default-impl placeholder smoke tests (audited in #188).
5998 //
5999 // These tests assert that the *current* default-impl languages
6000 // return ABC = 0/0/0 for source that DOES contain branches,
6001 // conditions, and assignments. When the real impl lands for any
6002 // of these languages, the corresponding assertion below will fire
6003 // — the implementer must update the expected values, which is the
6004 // gate. Tag the follow-up issue in each test.
6005 // ---------------------------------------------------------------
6006
6007 // --- Python ABC ---------------------------------------------------
6008
6009 #[test]
6010 fn python_empty_module_zero() {
6011 check_metrics::<PythonParser>("", "empty.py", |metric| {
6012 assert_eq!(metric.abc.assignments_sum(), 0);
6013 assert_eq!(metric.abc.branches_sum(), 0);
6014 assert_eq!(metric.abc.conditions_sum(), 0);
6015 insta::assert_json_snapshot!(metric.abc);
6016 });
6017 }
6018
6019 #[test]
6020 fn python_plain_assignments_count() {
6021 // Three plain `=` assignments → A=3. No branches, no conditions.
6022 check_metrics::<PythonParser>("x = 1\ny = 2\nz = x\n", "foo.py", |metric| {
6023 assert_eq!(metric.abc.assignments_sum(), 3);
6024 assert_eq!(metric.abc.branches_sum(), 0);
6025 assert_eq!(metric.abc.conditions_sum(), 0);
6026 insta::assert_json_snapshot!(metric.abc);
6027 });
6028 }
6029
6030 #[test]
6031 fn python_typed_assignment_counts_bare_annotation_does_not() {
6032 // `x: int = 1` carries an `=`, so it counts.
6033 // `y: int` is a bare annotation (no `=`) — declares a type but
6034 // binds nothing; it must NOT inflate the assignment count.
6035 check_metrics::<PythonParser>("x: int = 1\ny: int\n", "foo.py", |metric| {
6036 assert_eq!(metric.abc.assignments_sum(), 1);
6037 insta::assert_json_snapshot!(metric.abc);
6038 });
6039 }
6040
6041 #[test]
6042 fn python_augmented_assignments_count() {
6043 // Each augmented op counts once.
6044 check_metrics::<PythonParser>("x = 0\nx += 1\nx -= 1\nx *= 2\n", "foo.py", |metric| {
6045 // 1 plain `=` + 3 augmented = 4 assignments.
6046 assert_eq!(metric.abc.assignments_sum(), 4);
6047 insta::assert_json_snapshot!(metric.abc);
6048 });
6049 }
6050
6051 #[test]
6052 fn python_walrus_counts_as_assignment() {
6053 // `x := 10` is a `NamedExpression` (PEP 572). It binds a value
6054 // → one assignment under Fitzpatrick's rule.
6055 check_metrics::<PythonParser>("if (n := 10) > 5:\n pass\n", "foo.py", |metric| {
6056 // 1 assignment (walrus) + 1 condition (`> 5` is a
6057 // ComparisonOperator).
6058 assert_eq!(metric.abc.assignments_sum(), 1);
6059 assert_eq!(metric.abc.conditions_sum(), 1);
6060 insta::assert_json_snapshot!(metric.abc);
6061 });
6062 }
6063
6064 #[test]
6065 fn python_calls_are_branches() {
6066 // `foo()`, `bar()`, `Baz()` (constructor) all parse as `Call`
6067 // → three branches.
6068 check_metrics::<PythonParser>(
6069 "def foo():\n pass\ndef bar():\n pass\nclass Baz:\n pass\nfoo()\nbar()\nBaz()\n",
6070 "foo.py",
6071 |metric| {
6072 assert_eq!(metric.abc.branches_sum(), 3);
6073 assert_eq!(metric.abc.assignments_sum(), 0);
6074 insta::assert_json_snapshot!(metric.abc);
6075 },
6076 );
6077 }
6078
6079 #[test]
6080 fn python_comparisons_count_conditions() {
6081 // `x > 0`, `x == y`, `x is None` are each a single
6082 // `ComparisonOperator` node — three conditions.
6083 check_metrics::<PythonParser>(
6084 "def f(x, y):\n a = x > 0\n b = x == y\n c = x is None\n",
6085 "foo.py",
6086 |metric| {
6087 assert_eq!(metric.abc.conditions_sum(), 3);
6088 // 3 plain assignments; the comparisons are operands.
6089 assert_eq!(metric.abc.assignments_sum(), 3);
6090 insta::assert_json_snapshot!(metric.abc);
6091 },
6092 );
6093 }
6094
6095 #[test]
6096 fn python_chained_comparison_counts_once() {
6097 // tree-sitter-python collapses `0 < x < 10` into a single
6098 // `ComparisonOperator` — one condition, not two.
6099 check_metrics::<PythonParser>("def f(x):\n return 0 < x < 10\n", "foo.py", |metric| {
6100 assert_eq!(metric.abc.conditions_sum(), 1);
6101 insta::assert_json_snapshot!(metric.abc);
6102 });
6103 }
6104
6105 #[test]
6106 fn python_number_truthy_condition_counts() {
6107 // Regression for #772: Python treats every non-zero number as
6108 // truthy, so `if 5:` and `x and 5` should each count their
6109 // numeric literal as a Fitzpatrick unary condition. Pre-fix
6110 // `python_bool_terminal_kinds!()` listed `True` / `False` but
6111 // omitted `Integer` / `Float`, so the walker dropped every
6112 // numeric-truthy operand (mirrors the Lua `Number` fix).
6113 check_metrics::<PythonParser>(
6114 "def f(a):\n if 5:\n pass\n return a and 2\n",
6115 "foo.py",
6116 |metric| {
6117 // `if 5:` → walker counts the Integer literal (+1).
6118 // `a and 2` → `and` walker counts both operands:
6119 // identifier `a` (+1), Integer `2` (+1).
6120 // Total: 3.
6121 assert_eq!(metric.abc.conditions_sum(), 3);
6122 insta::assert_json_snapshot!(metric.abc);
6123 },
6124 );
6125 }
6126
6127 #[test]
6128 fn python_boolean_operators_not_counted_directly() {
6129 // Python's `and` / `or` are not counted as conditions on
6130 // their own (Fitzpatrick Rule 5; #395). Each operand is
6131 // instead counted as a unary conditional by the walker
6132 // (Rule 9; #403). `if a and b or c:` parses left-to-right
6133 // with `or` lower precedence: `(a and b) or c`. Walker
6134 // tallies: inner `and` counts `a`, `b` (+2); outer `or`
6135 // counts only the new outer operand `c` (+1; the inner
6136 // `(a and b)` BooleanOperator is not a terminal). Total
6137 // C = 3.
6138 check_metrics::<PythonParser>(
6139 "def f(a, b, c):\n if a and b or c:\n pass\n",
6140 "foo.py",
6141 |metric| {
6142 assert_eq!(metric.abc.conditions_sum(), 3);
6143 insta::assert_json_snapshot!(metric.abc);
6144 },
6145 );
6146 }
6147
6148 /// Python's unary `not` operator parses as `NotOperator` and now
6149 /// counts as one condition, matching Java's `!x` rule. Closes
6150 /// the parity gap noted in #214: without this, `if not flag:`
6151 /// reported 0 conditions while the Java equivalent reports 1.
6152 #[test]
6153 fn python_unary_not_counts_as_condition() {
6154 check_metrics::<PythonParser>(
6155 "def f(flag):\n if not flag:\n return 1\n return 0\n",
6156 "foo.py",
6157 |metric| {
6158 // One `NotOperator` -> 1 condition. The `if` itself
6159 // is structural and doesn't add an Abc condition.
6160 assert_eq!(metric.abc.conditions_sum(), 1);
6161 insta::assert_json_snapshot!(metric.abc);
6162 },
6163 );
6164 }
6165
6166 /// `return not flag` — the unary `not` is the entire return
6167 /// expression. Without `NotOperator` counted, this reports zero
6168 /// conditions; with it, one. Java's `return !flag;` is one.
6169 #[test]
6170 fn python_return_unary_not_counts() {
6171 check_metrics::<PythonParser>("def f(flag):\n return not flag\n", "foo.py", |metric| {
6172 assert_eq!(metric.abc.conditions_sum(), 1);
6173 insta::assert_json_snapshot!(metric.abc);
6174 });
6175 }
6176
6177 /// `foo(not ready, value)` — the unary `not` inside an argument
6178 /// list still contributes. Mirrors Java's
6179 /// `java_count_unary_conditions` walk over argument lists.
6180 #[test]
6181 fn python_unary_not_in_argument_list_counts() {
6182 check_metrics::<PythonParser>(
6183 "def f(ready, value):\n log(not ready, value)\n",
6184 "foo.py",
6185 |metric| {
6186 // 1 Call (log) -> 1 branch.
6187 // 1 NotOperator (not ready) -> 1 condition.
6188 assert_eq!(metric.abc.branches_sum(), 1);
6189 assert_eq!(metric.abc.conditions_sum(), 1);
6190 insta::assert_json_snapshot!(metric.abc);
6191 },
6192 );
6193 }
6194
6195 /// Nested `not` + comparison counts each unique node once.
6196 /// `not (x > 0)` parses as `NotOperator(ParenthesizedExpression(
6197 /// ComparisonOperator))`; both the unary and the comparison
6198 /// contribute one condition (mirrors Java's `!(x > 0)` = 2
6199 /// conditions).
6200 #[test]
6201 fn python_unary_not_with_comparison_counts_each_once() {
6202 check_metrics::<PythonParser>(
6203 "def f(x):\n if not (x > 0):\n return 1\n return 0\n",
6204 "foo.py",
6205 |metric| {
6206 // NotOperator (1) + ComparisonOperator (1) = 2.
6207 assert_eq!(metric.abc.conditions_sum(), 2);
6208 insta::assert_json_snapshot!(metric.abc);
6209 },
6210 );
6211 }
6212
6213 /// `not x and y` parses as `BooleanOperator(NotOperator(x), and,
6214 /// y)`. The `and` itself is NOT counted (Fitzpatrick Rule 5
6215 /// lists only comparison operators); the `NotOperator` is
6216 /// counted at the top level (Rule 7); and the `y` operand is
6217 /// counted by the Rule 9 walker (issue #403). Total: 2.
6218 /// `NotOperator` is intentionally not walked-into a second
6219 /// time — the walker skips it to avoid double-counting.
6220 #[test]
6221 fn python_unary_not_with_boolean_combinator_counts_each() {
6222 check_metrics::<PythonParser>(
6223 "def f(x, y):\n if not x and y:\n return 1\n return 0\n",
6224 "foo.py",
6225 |metric| {
6226 // NotOperator (1) + walker on `and` finds `y` (1) = 2.
6227 assert_eq!(metric.abc.conditions_sum(), 2);
6228 insta::assert_json_snapshot!(metric.abc);
6229 },
6230 );
6231 }
6232
6233 #[test]
6234 fn python_control_flow_arms_count_conditions() {
6235 // `elif`, `else`, `except`, `finally`, `case` each contribute
6236 // one condition. The comparisons in the `if`/`elif`/`while`
6237 // headers contribute their own ComparisonOperator counts.
6238 check_metrics::<PythonParser>(
6239 "def f(x):\n if x > 0:\n a = 1\n elif x > -1:\n a = 2\n else:\n a = 3\n",
6240 "foo.py",
6241 |metric| {
6242 // 2 ComparisonOperator (`x > 0`, `x > -1`) + 1
6243 // ElifClause + 1 ElseClause = 4 conditions.
6244 assert_eq!(metric.abc.conditions_sum(), 4);
6245 insta::assert_json_snapshot!(metric.abc);
6246 },
6247 );
6248 }
6249
6250 #[test]
6251 fn python_ternary_counts_as_condition() {
6252 // `a if c else b` is `ConditionalExpression` → 1 condition.
6253 // `c > 0` adds 1 more (ComparisonOperator).
6254 check_metrics::<PythonParser>(
6255 "def f(c):\n return 1 if c > 0 else 0\n",
6256 "foo.py",
6257 |metric| {
6258 assert_eq!(metric.abc.conditions_sum(), 2);
6259 insta::assert_json_snapshot!(metric.abc);
6260 },
6261 );
6262 }
6263
6264 // Issue #1161. Python counted the `conditional_expression` node but
6265 // never its condition slot, so `a if c() else b` reported 1 where
6266 // the equivalent `c() ? a : b` reports 2 everywhere else — and
6267 // `python_inspect_container`'s `ConditionalExpression` boolean-
6268 // context seed was unreachable, no call site having passed that
6269 // parent.
6270 #[test]
6271 fn python_ternary_condition_slot_counts_as_a_unary_condition() {
6272 // ternary (1) + condition `c()` (1) = 2. `c()` is a `Call`, a
6273 // boolean terminal; it also adds one *branch*, not a condition.
6274 check_metrics::<PythonParser>(
6275 "def f(a, b, c):\n return a if c() else b\n",
6276 "foo.py",
6277 |metric| {
6278 assert_eq!(metric.abc.conditions_sum(), 2);
6279 },
6280 );
6281 // A parenthesised condition, pinning the seed line this fix made
6282 // reachable: `(c)` is a `parenthesized_expression`, so only
6283 // `python_inspect_container` can resolve it, and it counts the
6284 // unwrapped terminal only when the parent seeds boolean context.
6285 // ternary (1) + `(c)` (1) = 2.
6286 check_metrics::<PythonParser>(
6287 "def f(a, b, c):\n return a if (c) else b\n",
6288 "foo.py",
6289 |metric| {
6290 assert_eq!(metric.abc.conditions_sum(), 2);
6291 },
6292 );
6293 // A negated condition is *not* counted by the new slot: it is a
6294 // `NotOperator`, which already has its own top-level dispatcher
6295 // arm. ternary (1) + `not c` (1) = 2, not 3.
6296 check_metrics::<PythonParser>(
6297 "def f(a, b, c):\n return a if not c else b\n",
6298 "foo.py",
6299 |metric| {
6300 assert_eq!(metric.abc.conditions_sum(), 2);
6301 },
6302 );
6303 // The cross-language reference case. `(not b) if a else (not c)`
6304 // is the exact semantic equivalent of `a ? !b : !c`, which every
6305 // other language reports as 4: ternary (1) + condition `a` (1) +
6306 // two `NotOperator`s (2). The negated operands come from
6307 // Python's own arm, the condition from the slot added here.
6308 //
6309 // #1161's resolution plan predicted this would stay 3, having
6310 // measured the condition slot's contribution against the pre-fix
6311 // total. 3 would have left Python disagreeing with every other
6312 // language on the reference expression — the gap the issue was
6313 // filed about.
6314 check_metrics::<PythonParser>(
6315 "def f(a, b, c):\n return (not b) if a else (not c)\n",
6316 "foo.py",
6317 |metric| {
6318 assert_eq!(metric.abc.conditions_sum(), 4);
6319 },
6320 );
6321 }
6322
6323 // The double-count pin, and the reason Python gets a condition-slot
6324 // helper rather than a copy of `cpp_walk_ternary`: Python's branch
6325 // operands are counted by the top-level `NotOperator` /
6326 // `ComparisonOperator` arms, a different mechanism from every other
6327 // language's walker. Routing the branch slots through
6328 // `python_inspect_container` as the C family does would count a
6329 // parenthesised operand that the identical unparenthesised
6330 // expression scores at zero.
6331 //
6332 // Both fixtures below are 2 today and 4 under such a copy, so a
6333 // later "make Python consistent with the others" change cannot land
6334 // silently.
6335 #[test]
6336 fn python_ternary_branch_operands_are_not_double_counted() {
6337 // ternary (1) + condition `a` (1) = 2. The two parenthesised
6338 // operands add nothing — an unnegated branch is type-free.
6339 check_metrics::<PythonParser>(
6340 "def f(a, b, c):\n return (b) if a else (c)\n",
6341 "foo.py",
6342 |metric| {
6343 assert_eq!(metric.abc.conditions_sum(), 2);
6344 },
6345 );
6346 // The unparenthesised form must agree: nothing about `(b)`
6347 // versus `b` is a condition.
6348 check_metrics::<PythonParser>(
6349 "def f(a, b, c):\n return b if a else c\n",
6350 "foo.py",
6351 |metric| {
6352 assert_eq!(metric.abc.conditions_sum(), 2);
6353 },
6354 );
6355 }
6356
6357 // Comments are tree-sitter `extras`, so they arrive as direct
6358 // children of `conditional_expression` and shift every positional
6359 // index after them. `python_count_ternary_condition` therefore
6360 // anchors on the `if` keyword and skips comments after it; both
6361 // halves are needed and each fixture below fails without one.
6362 #[test]
6363 fn python_ternary_condition_survives_an_interposed_comment() {
6364 // Comment before the keyword: `child(2)` is the `if` token here,
6365 // so a positional lookup reads 1. ternary (1) + `f()` (1) = 2.
6366 check_metrics::<PythonParser>(
6367 "def f(b, c):\n return (b\n # why\n if f() else c)\n",
6368 "foo.py",
6369 |metric| {
6370 assert_eq!(metric.abc.conditions_sum(), 2);
6371 },
6372 );
6373 // Comment after the keyword: the child immediately following
6374 // `if` is the comment, so taking the first rather than the first
6375 // non-comment reads 1. ternary (1) + `f()` (1) = 2.
6376 check_metrics::<PythonParser>(
6377 "def f(b, c):\n return (b if\n # why\n f() else c)\n",
6378 "foo.py",
6379 |metric| {
6380 assert_eq!(metric.abc.conditions_sum(), 2);
6381 },
6382 );
6383 }
6384
6385 #[test]
6386 fn python_try_except_finally_count_conditions() {
6387 // ExceptClause + FinallyClause → 2 conditions.
6388 check_metrics::<PythonParser>(
6389 "def f():\n try:\n pass\n except ValueError:\n pass\n finally:\n pass\n",
6390 "foo.py",
6391 |metric| {
6392 assert_eq!(metric.abc.conditions_sum(), 2);
6393 insta::assert_json_snapshot!(metric.abc);
6394 },
6395 );
6396 }
6397
6398 #[test]
6399 fn python_match_case_counts_conditions() {
6400 // Each non-wildcard `CaseClause` → 1 condition. The bare
6401 // `case _:` arm is the language-neutral `default:` equivalent
6402 // and is excluded (matches Rust's bare-`_` MatchArm filter and
6403 // Java/C#'s `default:` rule). Source has `case 1:` (counts) +
6404 // `case _:` (excluded) → C = 1.
6405 check_metrics::<PythonParser>(
6406 "def f(x):\n match x:\n case 1:\n pass\n case _:\n pass\n",
6407 "foo.py",
6408 |metric| {
6409 assert_eq!(metric.abc.conditions_sum(), 1);
6410 insta::assert_json_snapshot!(metric.abc);
6411 },
6412 );
6413 }
6414
6415 #[test]
6416 fn python_match_case_guarded_wildcard_counts() {
6417 // `case _ if g:` is NOT a bare wildcard — the guard
6418 // contributes real branching, so the arm counts as a
6419 // condition. Mirrors Rust's `_ if g => ...` behavior.
6420 // Source: `case 1:` (counts) + `case _ if x > 0:` (guarded
6421 // wildcard, counts) + `case _:` (bare wildcard, excluded) →
6422 // C from CaseClause = 2; the guard's `x > 0` adds one
6423 // ComparisonOperator → total C = 3.
6424 check_metrics::<PythonParser>(
6425 "def f(x):\n match x:\n case 1:\n pass\n case _ if x > 0:\n pass\n case _:\n pass\n",
6426 "foo.py",
6427 |metric| {
6428 assert_eq!(metric.abc.conditions_sum(), 3);
6429 insta::assert_json_snapshot!(metric.abc);
6430 },
6431 );
6432 }
6433
6434 #[test]
6435 fn python_complex_function_abc() {
6436 // Mixed-shape regression: assignments, calls, conditions all in
6437 // a single function.
6438 check_metrics::<PythonParser>(
6439 "def f(items, threshold):\n\
6440 \x20 result = []\n\
6441 \x20 for item in items:\n\
6442 \x20 if item > threshold:\n\
6443 \x20 result.append(item)\n\
6444 \x20 return result\n",
6445 "foo.py",
6446 |metric| {
6447 // assignments: `result = []` → 1
6448 // branches: `result.append(item)` is one call → 1
6449 // conditions: `item > threshold` is one
6450 // ComparisonOperator → 1
6451 assert_eq!(metric.abc.assignments_sum(), 1);
6452 assert_eq!(metric.abc.branches_sum(), 1);
6453 assert_eq!(metric.abc.conditions_sum(), 1);
6454 insta::assert_json_snapshot!(metric.abc);
6455 },
6456 );
6457 }
6458
6459 #[test]
6460 fn python_if_multiple_conditions() {
6461 // Fitzpatrick Rule 9 walker on `and` / `or` (issue #403).
6462 // - `if a or b or c or d:` → 4 (each operand counted once)
6463 // - `if a and b and c:` → 3
6464 // - `if not a and not b:` → 2 (two `NotOperator`s counted
6465 // by the top-level dispatcher arm; the walker SKIPS
6466 // `NotOperator` children to avoid double-counting)
6467 // Total: 4 + 3 + 2 = 9.
6468 check_metrics::<PythonParser>(
6469 "def f(a, b, c, d):\n\
6470 \x20 if a or b or c or d: # +4c\n\
6471 \x20 pass\n\
6472 \x20 if a and b and c: # +3c\n\
6473 \x20 pass\n\
6474 \x20 if not a and not b: # +2c (NotOperator x2)\n\
6475 \x20 pass\n",
6476 "foo.py",
6477 |metric| {
6478 assert_eq!(metric.abc.conditions_sum(), 9);
6479 insta::assert_json_snapshot!(metric.abc);
6480 },
6481 );
6482 }
6483
6484 #[test]
6485 fn python_while_conditions() {
6486 // Python has no `do { ... } while(cond);` construct, so this
6487 // mirrors only the `while` half of the Java suite. The
6488 // walker fires on each `and` / `or` token inside the loop
6489 // header.
6490 check_metrics::<PythonParser>(
6491 "def f(a, b):\n\
6492 \x20 while a or b: # +2c\n\
6493 \x20 break\n\
6494 \x20 while a and not b: # +2c (a + NotOperator)\n\
6495 \x20 break\n",
6496 "foo.py",
6497 |metric| {
6498 assert_eq!(metric.abc.conditions_sum(), 4);
6499 insta::assert_json_snapshot!(metric.abc);
6500 },
6501 );
6502 }
6503
6504 #[test]
6505 fn python_short_circuit_with_boolean_literal_operand() {
6506 // `a and True` reports 2 conditions: one identifier, one
6507 // True literal. Confirms `True` / `False` are in the walker
6508 // terminal set.
6509 check_metrics::<PythonParser>("def f(a):\n return a and True\n", "foo.py", |metric| {
6510 assert_eq!(metric.abc.conditions_sum(), 2);
6511 insta::assert_json_snapshot!(metric.abc);
6512 });
6513 }
6514
6515 #[test]
6516 fn python_await_expression_condition_counts() {
6517 // Regression for findings.md round-2 #2 (Python):
6518 // `if await ready(): pass` parses with `await` as the
6519 // condition node. Adding `Python::Await` to the
6520 // terminal-bool set mirrors the C# reference (lesson 19).
6521 check_metrics::<PythonParser>(
6522 "async def ready(): return True\n\
6523 async def f():\n if await ready(): pass\n",
6524 "foo.py",
6525 |metric| {
6526 // ready() is a call (1 branch); await is the
6527 // condition (1).
6528 assert_eq!(metric.abc.branches_sum(), 1);
6529 assert_eq!(metric.abc.conditions_sum(), 1);
6530 insta::assert_json_snapshot!(metric.abc);
6531 },
6532 );
6533 }
6534
6535 #[test]
6536 fn python_if_call_terminal_condition_counts_once() {
6537 // Pins the Phase-2B behaviour for Python's `Call` terminal-bool
6538 // kind: `if foo():` is a Fitzpatrick Rule 6 unary conditional
6539 // (a bare boolean-evaluating call as the if-condition). The
6540 // walker's terminal-at-top check fires once per call-condition;
6541 // the call itself separately contributes 1 branch. Surfaced
6542 // (and verified intentional) by the code-review pass on
6543 // Phase 2B.
6544 check_metrics::<PythonParser>("def f():\n if foo(): pass\n", "foo.py", |metric| {
6545 assert_eq!(metric.abc.branches_sum(), 1);
6546 assert_eq!(metric.abc.conditions_sum(), 1);
6547 insta::assert_json_snapshot!(metric.abc);
6548 });
6549 }
6550
6551 #[test]
6552 fn python_if_boolean_literal_condition() {
6553 // Phase 2B (issue #403): bare-boolean conditions count once.
6554 // Python has no paren wrap around if-conditions, so the
6555 // condition node is checked directly. The existing
6556 // NotOperator / ComparisonOperator arms continue to fire
6557 // for those shapes; only the bare-terminal cases (Identifier,
6558 // True, False, etc.) are added by the new arm.
6559 check_metrics::<PythonParser>(
6560 "def f(a):\n\
6561 \x20 if True: pass # +1c\n\
6562 \x20 if False: pass # +1c\n\
6563 \x20 while True: break # +1c\n\
6564 \x20 if a: pass # +1c (Rule 6 — bare identifier as condition)\n",
6565 "foo.py",
6566 |metric| {
6567 assert_eq!(metric.abc.conditions_sum(), 4);
6568 insta::assert_json_snapshot!(metric.abc);
6569 },
6570 );
6571 }
6572
6573 #[test]
6574 fn python_methods_arguments_with_conditions() {
6575 // `m(not a, not b)` reports 2 conditions — both `NotOperator`
6576 // nodes are counted by Python's pre-existing top-level
6577 // NotOperator dispatcher arm. The argument-list walker does
6578 // not need a separate Python arm.
6579 check_metrics::<PythonParser>(
6580 "def f(a, b):\n\
6581 \x20 m(a, b) # +1b\n\
6582 \x20 m(not a, not b) # +1b +2c\n",
6583 "foo.py",
6584 |metric| {
6585 assert_eq!(metric.abc.branches_sum(), 2);
6586 assert_eq!(metric.abc.conditions_sum(), 2);
6587 insta::assert_json_snapshot!(metric.abc);
6588 },
6589 );
6590 }
6591
6592 #[test]
6593 fn python_return_with_conditions() {
6594 // Phase 2B (issue #403). Python uses the pre-existing top-
6595 // level NotOperator / ComparisonOperator arms for return
6596 // expressions; no dedicated ReturnStatement walker arm is
6597 // needed.
6598 check_metrics::<PythonParser>(
6599 "def m1(z): return not (z >= 0)\n\
6600 def m2(x): return (((not x)))\n\
6601 def m3(x, y): return x and y\n",
6602 "foo.py",
6603 |metric| {
6604 // m1: NotOperator (1) + ComparisonOperator (1) = 2.
6605 // m2: NotOperator (1).
6606 // m3: walker on `and` counts both operands = 2.
6607 // Sum: 5.
6608 assert_eq!(metric.abc.conditions_sum(), 5);
6609 insta::assert_json_snapshot!(metric.abc);
6610 },
6611 );
6612 }
6613
6614 #[test]
6615 fn rust_empty_unit_zero() {
6616 // No code at all → A=B=C=0. Establishes the trait is wired up
6617 // and the per-language compute is reachable.
6618 check_metrics::<RustParser>("", "empty.rs", |metric| {
6619 assert_eq!(metric.abc.assignments_sum(), 0);
6620 assert_eq!(metric.abc.branches_sum(), 0);
6621 assert_eq!(metric.abc.conditions_sum(), 0);
6622 insta::assert_json_snapshot!(metric.abc);
6623 });
6624 }
6625
6626 #[test]
6627 fn rust_assignments_let_init_plain_and_compound() {
6628 // `let mut x = 0` is a `let_declaration` carrying an `=`
6629 // initializer → counts as 1 (matches Fitzpatrick's literal
6630 // "every `=` is an assignment" rule and the JS impl's
6631 // treatment of `let x = 5`). `x = 5` and `x = 7` are plain
6632 // `=` assignments → 2. `x += 2` is a compound assignment → 1.
6633 // Total A = 4.
6634 check_metrics::<RustParser>(
6635 "fn f() { let mut x = 0; x = 5; x += 2; x = 7; }",
6636 "foo.rs",
6637 |metric| {
6638 assert_eq!(metric.abc.assignments_sum(), 4);
6639 assert_eq!(metric.abc.branches_sum(), 0);
6640 assert_eq!(metric.abc.conditions_sum(), 0);
6641 insta::assert_json_snapshot!(metric.abc);
6642 },
6643 );
6644 }
6645
6646 #[test]
6647 fn rust_let_without_initializer_does_not_count() {
6648 // `let a;` is a `let_declaration` with NO `=` and no `value`
6649 // field — the binding is uninitialised. The arm only fires
6650 // when `value` is present, so this contributes zero to A.
6651 // `let _b;` is the same shape (the `_` pattern is still a
6652 // pattern, not a wildcard suppression of the binding).
6653 // Regression test for issue #393: only `=` counts, not the
6654 // bare declaration.
6655 check_metrics::<RustParser>(
6656 "fn f() { let a: i32; let _b: i32; a = 5; }",
6657 "foo.rs",
6658 |metric| {
6659 // Only `a = 5` (assignment_expression) → A = 1.
6660 assert_eq!(metric.abc.assignments_sum(), 1);
6661 insta::assert_json_snapshot!(metric.abc);
6662 },
6663 );
6664 }
6665
6666 #[test]
6667 fn rust_let_initializers_immutable_and_mutable_count() {
6668 // Issue #393: `let a = 1;`, `let b = 2;`, `let c = a + b;`,
6669 // `let mut d = 0;` are all `let_declaration` nodes carrying
6670 // an `=` initializer — each counts as 1 (Option B in the
6671 // issue body: literal Fitzpatrick, both `let` and `let mut`
6672 // count). `d = 5;` is one plain assignment_expression, `d
6673 // += 1;` is one compound. Total A = 4 + 1 + 1 = 6.
6674 check_metrics::<RustParser>(
6675 "fn f() { let a=1; let b=2; let c=a+b; let mut d=0; d=5; d+=1; }",
6676 "foo.rs",
6677 |metric| {
6678 assert_eq!(metric.abc.assignments_sum(), 6);
6679 insta::assert_json_snapshot!(metric.abc);
6680 },
6681 );
6682 }
6683
6684 #[test]
6685 fn rust_calls_are_branches() {
6686 // Free function call + method call (parses as call_expression
6687 // with a field_expression callee) + associated-fn call. All
6688 // three are `call_expression` → B = 3. Macro invocations like
6689 // `println!` parse as `macro_invocation`, NOT `call_expression`,
6690 // so they are not branches.
6691 check_metrics::<RustParser>(
6692 "fn f() { g(); 1.to_string(); String::new(); }\nfn g() {}\n",
6693 "foo.rs",
6694 |metric| {
6695 assert_eq!(metric.abc.branches_sum(), 3);
6696 assert_eq!(metric.abc.assignments_sum(), 0);
6697 assert_eq!(metric.abc.conditions_sum(), 0);
6698 insta::assert_json_snapshot!(metric.abc);
6699 },
6700 );
6701 }
6702
6703 #[test]
6704 fn rust_try_operator_is_branch() {
6705 // `?` parses as `try_expression` and counts as one branch
6706 // (short-circuit return on Err / None). The `Err(())` call
6707 // contributes one branch in addition (call_expression).
6708 check_metrics::<RustParser>(
6709 "fn f() -> Result<i32, ()> { let r: Result<i32, ()> = Err(()); Ok(r?) }",
6710 "foo.rs",
6711 |metric| {
6712 // Err(()) + Ok(...) + r? → 2 calls + 1 try = 3 branches.
6713 assert_eq!(metric.abc.branches_sum(), 3);
6714 insta::assert_json_snapshot!(metric.abc);
6715 },
6716 );
6717 }
6718
6719 #[test]
6720 fn rust_comparisons_count_conditions() {
6721 // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six
6722 // comparisons → C = 6.
6723 check_metrics::<RustParser>(
6724 "fn f(a: i32, b: i32) -> bool { a < b || a > b || a <= b || a >= b || a == b || a != b }",
6725 "foo.rs",
6726 |metric| {
6727 assert_eq!(metric.abc.conditions_sum(), 6);
6728 insta::assert_json_snapshot!(metric.abc);
6729 },
6730 );
6731 }
6732
6733 #[test]
6734 fn rust_generic_brackets_not_conditions() {
6735 // `<` / `>` in `Vec<i32>` are TypeArguments delimiters, not
6736 // comparison operators. The parent-check in the LT/GT arms
6737 // must filter them out. Expected C = 0.
6738 check_metrics::<RustParser>(
6739 "fn f() -> Vec<i32> { Vec::<i32>::new() }",
6740 "foo.rs",
6741 |metric| {
6742 assert_eq!(metric.abc.conditions_sum(), 0);
6743 insta::assert_json_snapshot!(metric.abc);
6744 },
6745 );
6746 }
6747
6748 #[test]
6749 fn rust_if_let_counts_as_condition() {
6750 // `if let Some(v) = opt { ... }` introduces a `let_condition`
6751 // → 1 condition. The `if` keyword itself does not add another
6752 // count — Fitzpatrick counts conditions, not branch keywords.
6753 check_metrics::<RustParser>(
6754 "fn f(opt: Option<i32>) { if let Some(_v) = opt { } }",
6755 "foo.rs",
6756 |metric| {
6757 assert_eq!(metric.abc.conditions_sum(), 1);
6758 insta::assert_json_snapshot!(metric.abc);
6759 },
6760 );
6761 }
6762
6763 #[test]
6764 fn rust_while_let_counts_as_condition() {
6765 // `while let Some(y) = it.next() { ... }` is also a
6766 // `let_condition` (the `while` form). One condition; the
6767 // `it.next()` call adds one branch.
6768 check_metrics::<RustParser>(
6769 "fn f(mut it: std::vec::IntoIter<i32>) { while let Some(_y) = it.next() { } }",
6770 "foo.rs",
6771 |metric| {
6772 assert_eq!(metric.abc.conditions_sum(), 1);
6773 assert_eq!(metric.abc.branches_sum(), 1);
6774 insta::assert_json_snapshot!(metric.abc);
6775 },
6776 );
6777 }
6778
6779 #[test]
6780 fn rust_match_arms_count_conditions_wildcard_excluded() {
6781 // Three arms: `0 => 1`, `n if n > 0 => n`, `_ => -1`. The
6782 // bare wildcard is the `default:` equivalent and is skipped.
6783 // The guarded arm has a `n if n > 0` pattern (more than one
6784 // child in the match_pattern) and still counts. Two non-wildcard
6785 // arms → C = 2 from MatchArm. Plus the comparison `n > 0`
6786 // adds one more → C = 3.
6787 check_metrics::<RustParser>(
6788 "fn f(x: i32) -> i32 { match x { 0 => 1, n if n > 0 => n, _ => -1, } }",
6789 "foo.rs",
6790 |metric| {
6791 assert_eq!(metric.abc.conditions_sum(), 3);
6792 insta::assert_json_snapshot!(metric.abc);
6793 },
6794 );
6795 }
6796
6797 #[test]
6798 fn rust_else_counts_as_condition() {
6799 // `if a > b { ... } else { ... }` → `a > b` is one condition,
6800 // `else` is one condition → C = 2.
6801 check_metrics::<RustParser>(
6802 "fn f(a: i32, b: i32) -> i32 { if a > b { a } else { b } }",
6803 "foo.rs",
6804 |metric| {
6805 assert_eq!(metric.abc.conditions_sum(), 2);
6806 insta::assert_json_snapshot!(metric.abc);
6807 },
6808 );
6809 }
6810
6811 #[test]
6812 fn rust_let_chain2_hidden_rule_drift_marker() {
6813 // Drift marker (findings.md round-2 #3): `Rust::LetChain2`
6814 // maps to the hidden grammar rule `_let_chain`. At the
6815 // pinned tree-sitter-rust version it is never emitted as a
6816 // concrete node — the visible `LetChain` (= 352) carries
6817 // every let-chain. We list `LetChain2` defensively in
6818 // `rust_inspect_container` and `rust_count_unary_conditions`
6819 // (lesson 34); if a future grammar bump promotes
6820 // `_let_chain` to a visible rule, this assertion fails
6821 // loudly so the maintainer knows to verify the walker still
6822 // counts correctly for the new shape.
6823 let src = "fn f(a: bool, b: Option<i32>) {\n\
6824 \x20 if a && let Some(_) = b { }\n\
6825 }\n";
6826 let parser = RustParser::new(
6827 src.as_bytes().to_vec(),
6828 &std::path::PathBuf::from("foo.rs"),
6829 None,
6830 );
6831 assert!(!ast_has_kind_id(&parser, Rust::LetChain2 as u16));
6832 }
6833
6834 #[test]
6835 fn rust_scoped_identifier_condition_counts() {
6836 // Regression for findings.md round-2 #1 (Rust):
6837 // `if crate::FLAG {}` parses with `scoped_identifier` as the
6838 // condition node. Pre-fix, `rust_bool_terminal_kinds!()`
6839 // listed only `Identifier` so the walker reached the
6840 // `scoped_identifier` child, found it non-terminal /
6841 // non-paren / non-unary, and broke without counting.
6842 // Mirrors the C# fix in #372 (lesson 19) for
6843 // `MemberAccessExpression`.
6844 check_metrics::<RustParser>("fn f() { if crate::FLAG { } }\n", "foo.rs", |metric| {
6845 assert_eq!(metric.abc.conditions_sum(), 1);
6846 insta::assert_json_snapshot!(metric.abc);
6847 });
6848 }
6849
6850 #[test]
6851 fn rust_await_expression_condition_counts() {
6852 // Regression for findings.md round-2 #2 (Rust):
6853 // `if ready().await {}` parses with `await_expression` as
6854 // the condition node. Adding `Rust::AwaitExpression` to the
6855 // terminal-bool set closes the parity gap with the C#
6856 // reference (`csharp_bool_terminal_kinds!()`).
6857 check_metrics::<RustParser>(
6858 "async fn ready() -> bool { true }\n\
6859 async fn f() { if ready().await { } }\n",
6860 "foo.rs",
6861 |metric| {
6862 // ready() is a call (1 branch); `ready().await` is
6863 // the unary boolean condition (1).
6864 assert_eq!(metric.abc.branches_sum(), 1);
6865 assert_eq!(metric.abc.conditions_sum(), 1);
6866 insta::assert_json_snapshot!(metric.abc);
6867 },
6868 );
6869 }
6870
6871 #[test]
6872 fn rust_complex_function_abc() {
6873 // Mixed-shape regression: assignments, calls, conditions, `?`,
6874 // `if let`, `match` in one body. Verified by hand:
6875 // - assignments: `let mut x = 0` (let init), `x = 5`, `x += 2`,
6876 // `let _ = ...` (let init), `let r: ... = Err(())` (let init),
6877 // `let _v = r?` (let init) → A = 6 (post-#393: every `=`
6878 // initializer in a `let_declaration` is one assignment, in
6879 // line with the literal Fitzpatrick reading).
6880 // - branches: `xs.iter()`, `.next()`, `Err(())`, `r?` → B = 4
6881 // (3 calls + 1 try).
6882 // - conditions: `if let Some(v) = opt` → 1, `match x` arms
6883 // `0`, `n if n>0` (wildcard excluded) → 2, `n > 0` → 1.
6884 // Total C = 4.
6885 check_metrics::<RustParser>(
6886 "fn f(opt: Option<i32>, xs: Vec<i32>) -> Result<i32, ()> {\n\
6887 \x20 let mut x = 0;\n\
6888 \x20 x = 5;\n\
6889 \x20 x += 2;\n\
6890 \x20 if let Some(_v) = opt { }\n\
6891 \x20 let _ = xs.iter().next();\n\
6892 \x20 let r: Result<i32, ()> = Err(());\n\
6893 \x20 let _v = r?;\n\
6894 \x20 Ok(match x {\n\
6895 \x20 0 => 1,\n\
6896 \x20 n if n > 0 => n,\n\
6897 \x20 _ => -1,\n\
6898 \x20 })\n\
6899 }\n",
6900 "foo.rs",
6901 |metric| {
6902 assert_eq!(metric.abc.assignments_sum(), 6);
6903 // calls: xs.iter(), .next(), Err(()), Ok(...) → 4 calls
6904 // plus 1 try (`r?`) → 5 branches.
6905 assert_eq!(metric.abc.branches_sum(), 5);
6906 // 1 let_condition + 2 non-wildcard match_arms + 1
6907 // comparison (`n > 0`) → 4.
6908 assert_eq!(metric.abc.conditions_sum(), 4);
6909 insta::assert_json_snapshot!(metric.abc);
6910 },
6911 );
6912 }
6913
6914 #[test]
6915 fn rust_let_chain_bare_identifier_operand_counts() {
6916 // Regression: pre-fix, `if a && let Some(_z) = y { }` reported
6917 // 1 condition (only the LetCondition). The bare-identifier
6918 // `a` operand was lost because Rust 2024 wraps let-chain
6919 // `&&` operands in a `LetChain` node (not `BinaryExpression`)
6920 // and `rust_count_unary_conditions` only counted terminals
6921 // under a `BinaryExpression` parent. Allowing `LetChain` /
6922 // `LetChain2` as known-bool list parents fixes the loss.
6923 // Expected: LetCondition (1) + walker on `a` (1) = 2.
6924 check_metrics::<RustParser>(
6925 "fn f(a: bool, y: Option<i32>) {\n\
6926 \x20 if a && let Some(_z) = y { }\n\
6927 }\n",
6928 "foo.rs",
6929 |metric| {
6930 assert_eq!(metric.abc.conditions_sum(), 2);
6931 insta::assert_json_snapshot!(metric.abc);
6932 },
6933 );
6934 }
6935
6936 #[test]
6937 fn rust_if_multiple_conditions() {
6938 // Fitzpatrick Rule 7 / Listing 2 (issue #403): every operand of
6939 // a `&&` / `||` chain is one condition. Mirrors
6940 // `java_if_multiple_conditions`. Rust's `if` head has no
6941 // parentheses, but the walker fires on each `&&` / `||` token
6942 // and walks the parent `binary_expression` regardless.
6943 check_metrics::<RustParser>(
6944 "fn f(a: bool, b: bool, c: bool, d: bool) -> i32 {\n\
6945 \x20 if a || b || c || d { return 1; } // +4c\n\
6946 \x20 if a && b && c { return 2; } // +3c\n\
6947 \x20 if !a && !b { return 3; } // +2c\n\
6948 \x20 0\n\
6949 }\n",
6950 "foo.rs",
6951 |metric| {
6952 // 4 + 3 + 2 = 9
6953 assert_eq!(metric.abc.conditions_sum(), 9);
6954 insta::assert_json_snapshot!(metric.abc);
6955 },
6956 );
6957 }
6958
6959 #[test]
6960 fn rust_while_conditions() {
6961 // Rust has no `do { ... } while(cond);` construct, so this
6962 // mirrors only the `while` half of `java_while_and_do_while_conditions`.
6963 // Each operand of the `&&` / `||` chain in the loop condition
6964 // counts as one Fitzpatrick condition (Rule 7).
6965 check_metrics::<RustParser>(
6966 "fn f(a: bool, b: bool) {\n\
6967 \x20 while a || b { break; } // +2c\n\
6968 \x20 while a && !b { break; } // +2c\n\
6969 }\n",
6970 "foo.rs",
6971 |metric| {
6972 assert_eq!(metric.abc.conditions_sum(), 4);
6973 insta::assert_json_snapshot!(metric.abc);
6974 },
6975 );
6976 }
6977
6978 #[test]
6979 fn rust_if_boolean_literal_condition() {
6980 // Phase 2B (issue #403): a condition whose entire body is a
6981 // boolean literal counts as one Fitzpatrick condition.
6982 // `if true {}` → 1, `if !false {}` → 1 (unary unwrap), and
6983 // `while true { break }` → 1.
6984 check_metrics::<RustParser>(
6985 "fn f() {\n\
6986 \x20 if true { } // +1c\n\
6987 \x20 if !false { } // +1c\n\
6988 \x20 while true { break; } // +1c\n\
6989 }\n",
6990 "foo.rs",
6991 |metric| {
6992 assert_eq!(metric.abc.conditions_sum(), 3);
6993 insta::assert_json_snapshot!(metric.abc);
6994 },
6995 );
6996 }
6997
6998 #[test]
6999 fn rust_methods_arguments_with_conditions() {
7000 // Phase 2B (issue #403): unary-conditional arguments to a
7001 // call each count once. `m(!a, !b)` → 2 conditions + 1
7002 // branch (the call itself). Bare identifier arguments do
7003 // NOT count (they reach the count_unary_conditions list with
7004 // list_kind = Arguments, not BinaryExpression).
7005 check_metrics::<RustParser>(
7006 "fn f(a: bool, b: bool) {\n\
7007 \x20 m(a, b); // +1b\n\
7008 \x20 m(!a, !b); // +1b +2c\n\
7009 \x20 m(!a, b, !a); // +1b +2c\n\
7010 }\n",
7011 "foo.rs",
7012 |metric| {
7013 assert_eq!(metric.abc.branches_sum(), 3);
7014 assert_eq!(metric.abc.conditions_sum(), 4);
7015 insta::assert_json_snapshot!(metric.abc);
7016 },
7017 );
7018 }
7019
7020 #[test]
7021 fn rust_return_with_conditions() {
7022 // Phase 2B (issue #403). Mirrors `java_return_with_conditions`
7023 // — `return !a` / `return x && y` count their unary
7024 // conditional operands. Per Fitzpatrick Rule 7, a `!`-wrapped
7025 // relational expression contributes ONE condition (the
7026 // relational op itself) — the `!` does not add a second
7027 // count when its operand is already a comparison.
7028 check_metrics::<RustParser>(
7029 "fn m1(z: i32) -> bool { return !(z >= 0); }\n\
7030 fn m2(x: bool) -> bool { return (((!x))); }\n\
7031 fn m3(x: bool, y: bool) -> bool { return x && y; }\n\
7032 fn m4(y: bool, z: i32) -> bool { return y || (z < 0); }\n",
7033 "foo.rs",
7034 |metric| {
7035 // m1: !(z >= 0) → the `>=` contributes 1; the unary
7036 // `!` wraps a paren'd BinaryExpression, which
7037 // inspect_container does not unwrap further →
7038 // no walker count. Total: 1.
7039 // m2: (((!x))) → ReturnExpression arm walks (((!x))).
7040 // inspect_container unwraps three parens and one
7041 // unary, reaches Identifier `x`, has_boolean_content
7042 // was seeded true by the unary-not flip. +1.
7043 // m3: x && y → `&&` walker counts both terminals → 2.
7044 // m4: y || (z < 0) → `||` walker counts `y` (terminal,
7045 // +1); the `<` contributes 1 via its own arm; the
7046 // paren'd BinaryExpression `(z < 0)` is not
7047 // terminal under the walker → no extra count.
7048 // Total: 2.
7049 // Sum: 1 + 1 + 2 + 2 = 6.
7050 assert_eq!(metric.abc.conditions_sum(), 6);
7051 insta::assert_json_snapshot!(metric.abc);
7052 },
7053 );
7054 }
7055
7056 #[test]
7057 fn rust_short_circuit_with_boolean_literal_operand() {
7058 // `if a && true` reports 2 conditions: one for the identifier
7059 // operand, one for the boolean-literal operand. Confirms the
7060 // walker terminal set includes `BooleanLiteral`.
7061 check_metrics::<RustParser>(
7062 "fn f(a: bool) -> bool { a && true }\n",
7063 "foo.rs",
7064 |metric| {
7065 assert_eq!(metric.abc.conditions_sum(), 2);
7066 insta::assert_json_snapshot!(metric.abc);
7067 },
7068 );
7069 }
7070
7071 // ----- Go -----
7072
7073 #[test]
7074 fn go_empty_unit_zero() {
7075 // Package declaration only — no Fitzpatrick events. Confirms the
7076 // GoCode Abc trait is wired up and emits zero counts.
7077 check_metrics::<GoParser>("package main\n", "empty.go", |metric| {
7078 assert_eq!(metric.abc.assignments_sum(), 0);
7079 assert_eq!(metric.abc.branches_sum(), 0);
7080 assert_eq!(metric.abc.conditions_sum(), 0);
7081 insta::assert_json_snapshot!(metric.abc);
7082 });
7083 }
7084
7085 #[test]
7086 fn go_assignments_count_plain_compound_short_var_and_incdec() {
7087 // `x := 0` (short var decl), `x = 5` and `x = 7` (plain `=`),
7088 // `x += 2` (compound), `x++` (inc), and the initialized
7089 // declaration `var y = 1` — which is counted, matching the Rust
7090 // and Java rules for `let y = 1` / `int y = 1` (both measured at
7091 // one assignment each). The comment here previously claimed the
7092 // opposite and pinned Go at 6 (#1278).
7093 check_metrics::<GoParser>(
7094 "package main\nfunc f() { var y = 1; _ = y; x := 0; x = 5; x += 2; x = 7; x++ }\n",
7095 "foo.go",
7096 |metric| {
7097 // `_ = y` is itself an assignment_statement → +1.
7098 // var y=1 + _=y + x:= + x=5 + x+=2 + x=7 + x++ → 7
7099 assert_eq!(metric.abc.assignments_sum(), 7);
7100 assert_eq!(metric.abc.branches_sum(), 0);
7101 assert_eq!(metric.abc.conditions_sum(), 0);
7102 insta::assert_json_snapshot!(metric.abc);
7103 },
7104 );
7105 }
7106
7107 #[test]
7108 fn go_var_declarations_count_only_when_initialized() {
7109 // Regression for #1278: a `var` declaration with an initializer is
7110 // a `var_spec` carrying a `value` field, not an
7111 // `assignment_statement` or `short_var_declaration`, so it scored
7112 // zero — `var x = 5` and `x := 5` are the same binding spelled two
7113 // ways. Both typed and untyped initializers count; an
7114 // uninitialized `var z int` and a `const` do not.
7115 // expected: 3 assignments — `var x = 5`, `var y int = 6`, `z := 7`;
7116 // `var w int` and `const c = 1` contribute nothing.
7117 check_metrics::<GoParser>(
7118 "package main\nfunc f() int {\n\tvar x = 5\n\tvar y int = 6\n\tvar w int\n\tconst c = 1\n\tz := 7\n\treturn x + y + w + c + z\n}\n",
7119 "foo.go",
7120 |metric| {
7121 assert_eq!(metric.abc.assignments_sum(), 3);
7122 assert_eq!(metric.abc.branches_sum(), 0);
7123 },
7124 );
7125 }
7126
7127 #[test]
7128 fn go_grouped_var_block_counts_each_initialized_spec() {
7129 // A grouped `var ( … )` block is one `var_declaration` holding one
7130 // `var_spec` per line, so matching the spec counts each initialized
7131 // line on its own. A multi-name spec is still one binding
7132 // statement, matching `p, q := 1, 2` (#1278).
7133 // expected: 3 assignments — `a = 1`, `p, q = 1, 2`, and `r := 0`;
7134 // the uninitialized `b int` contributes nothing.
7135 check_metrics::<GoParser>(
7136 "package main\nfunc f() {\n\tvar (\n\t\ta = 1\n\t\tb int\n\t)\n\tvar p, q = 1, 2\n\tr := 0\n\t_ = a + b + p + q + r\n}\n",
7137 "foo.go",
7138 |metric| {
7139 // The trailing `_ = …` is itself an assignment_statement.
7140 assert_eq!(metric.abc.assignments_sum(), 4);
7141 },
7142 );
7143 }
7144
7145 #[test]
7146 fn go_calls_are_branches() {
7147 // Three calls: free function `g()`, method call `r.Inc()`, and
7148 // builtin call `len(s)`. All parse as `call_expression` → B = 3.
7149 // Composite literal `Foo{}` is NOT a call.
7150 check_metrics::<GoParser>(
7151 "package main\n\
7152 type R struct{}\n\
7153 func (r R) Inc() {}\n\
7154 func g() {}\n\
7155 func f(s string) { g(); var r R = R{}; r.Inc(); _ = len(s) }\n",
7156 "foo.go",
7157 |metric| {
7158 assert_eq!(metric.abc.branches_sum(), 3);
7159 insta::assert_json_snapshot!(metric.abc);
7160 },
7161 );
7162 }
7163
7164 #[test]
7165 fn go_comparisons_count_conditions() {
7166 // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six
7167 // comparisons → C = 6.
7168 check_metrics::<GoParser>(
7169 "package main\nfunc f(a, b int) bool { return a < b || a > b || a <= b || a >= b || a == b || a != b }\n",
7170 "foo.go",
7171 |metric| {
7172 assert_eq!(metric.abc.conditions_sum(), 6);
7173 insta::assert_json_snapshot!(metric.abc);
7174 },
7175 );
7176 }
7177
7178 #[test]
7179 fn go_generic_brackets_not_conditions() {
7180 // Generic instantiation `Min[int](a, b)` puts `int` inside
7181 // `TypeArguments`, not `BinaryExpression`. The parent guard on
7182 // `<` / `>` must not count these. Expected C = 0; B = 1 (one call).
7183 check_metrics::<GoParser>(
7184 "package main\nfunc Min[T int | float64](a, b T) T { return a }\nfunc f() { _ = Min[int](1, 2) }\n",
7185 "foo.go",
7186 |metric| {
7187 assert_eq!(metric.abc.conditions_sum(), 0);
7188 assert_eq!(metric.abc.branches_sum(), 1);
7189 insta::assert_json_snapshot!(metric.abc);
7190 },
7191 );
7192 }
7193
7194 #[test]
7195 fn go_switch_arms_count_conditions_default_excluded() {
7196 // Four arms: `case 1:`, `case 2:`, `case 3:`, `default:`. The
7197 // bare `default` is the C/Java `default:` equivalent and is
7198 // excluded — 3 conditions from ExpressionCase. The switch
7199 // expression `x` is bare (no comparison), so no extra
7200 // condition from `==`-style operators.
7201 check_metrics::<GoParser>(
7202 "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",
7203 "foo.go",
7204 |metric| {
7205 assert_eq!(metric.abc.conditions_sum(), 3);
7206 insta::assert_json_snapshot!(metric.abc);
7207 },
7208 );
7209 }
7210
7211 #[test]
7212 fn go_type_switch_arms_count_conditions() {
7213 // Type switch: `case int:`, `case string:`, `default:`. Two
7214 // non-default type-case arms → C = 2.
7215 check_metrics::<GoParser>(
7216 "package main\nfunc f(v interface{}) { switch v.(type) { case int: return; case string: return; default: return } }\n",
7217 "foo.go",
7218 |metric| {
7219 assert_eq!(metric.abc.conditions_sum(), 2);
7220 insta::assert_json_snapshot!(metric.abc);
7221 },
7222 );
7223 }
7224
7225 #[test]
7226 fn go_select_arms_count_conditions() {
7227 // `select { case <-ch: ...; case ch <- 1: ...; default: ... }`.
7228 // Two non-default communication cases → C = 2.
7229 check_metrics::<GoParser>(
7230 "package main\nfunc f(ch chan int) { select { case <-ch: return; case ch <- 1: return; default: return } }\n",
7231 "foo.go",
7232 |metric| {
7233 assert_eq!(metric.abc.conditions_sum(), 2);
7234 insta::assert_json_snapshot!(metric.abc);
7235 },
7236 );
7237 }
7238
7239 #[test]
7240 fn go_else_counts_as_condition() {
7241 // `if a > b { ... } else { ... }` → `a > b` is one condition,
7242 // `else` is one condition → C = 2.
7243 check_metrics::<GoParser>(
7244 "package main\nfunc f(a, b int) int { if a > b { return a } else { return b } }\n",
7245 "foo.go",
7246 |metric| {
7247 assert_eq!(metric.abc.conditions_sum(), 2);
7248 insta::assert_json_snapshot!(metric.abc);
7249 },
7250 );
7251 }
7252
7253 #[test]
7254 fn go_complex_function_abc() {
7255 // Mixed shape, verified by hand:
7256 // - Assignments: `var x = 10` (an initialized declaration, #1278),
7257 // `_ = x`, `n := 0`, `n = n + 1`, `n += 2`, `n++`,
7258 // `_ = len(s)` → A = 7. Every `_ = ...` IS counted as an
7259 // assignment_statement.
7260 // - Branches: `len(s)` → B = 1.
7261 // - Conditions: `n < 10` → 1, `else` → 1, switch arms `case 0:`
7262 // and `case 1:` (default excluded) → 2 → total C = 4.
7263 check_metrics::<GoParser>(
7264 "package main\nfunc f(s string) int {\n\
7265 \x20 var x = 10\n\
7266 \x20 _ = x\n\
7267 \x20 n := 0\n\
7268 \x20 if n < 10 { n = n + 1 } else { n += 2 }\n\
7269 \x20 n++\n\
7270 \x20 _ = len(s)\n\
7271 \x20 switch n {\n\
7272 \x20 case 0: return 0\n\
7273 \x20 case 1: return 1\n\
7274 \x20 default: return n\n\
7275 \x20 }\n\
7276 }\n",
7277 "foo.go",
7278 |metric| {
7279 assert_eq!(metric.abc.assignments_sum(), 7);
7280 assert_eq!(metric.abc.branches_sum(), 1);
7281 assert_eq!(metric.abc.conditions_sum(), 4);
7282 insta::assert_json_snapshot!(metric.abc);
7283 },
7284 );
7285 }
7286
7287 #[test]
7288 fn go_if_multiple_conditions() {
7289 // Fitzpatrick Rule 7 walker fan-out (issue #403). Mirrors
7290 // `rust_if_multiple_conditions`.
7291 check_metrics::<GoParser>(
7292 "package p\n\
7293 func F(a, b, c, d bool) int {\n\
7294 \x20 if a || b || c || d { return 1 } // +4c\n\
7295 \x20 if a && b && c { return 2 } // +3c\n\
7296 \x20 if !a && !b { return 3 } // +2c\n\
7297 \x20 return 0\n\
7298 }\n",
7299 "foo.go",
7300 |metric| {
7301 assert_eq!(metric.abc.conditions_sum(), 9);
7302 insta::assert_json_snapshot!(metric.abc);
7303 },
7304 );
7305 }
7306
7307 #[test]
7308 fn go_for_with_conditions() {
7309 // Go has no `while` or `do { … } while(…);` — the `for` loop
7310 // header is the sole condition slot. Each operand of the
7311 // `&&` / `||` chain in the for-condition counts as one
7312 // Fitzpatrick condition.
7313 check_metrics::<GoParser>(
7314 "package p\n\
7315 func F(a, b bool) {\n\
7316 \x20 for a || b { break } // +2c\n\
7317 \x20 for a && !b { break } // +2c\n\
7318 }\n",
7319 "foo.go",
7320 |metric| {
7321 assert_eq!(metric.abc.conditions_sum(), 4);
7322 insta::assert_json_snapshot!(metric.abc);
7323 },
7324 );
7325 }
7326
7327 #[test]
7328 fn go_for_bare_condition_counts() {
7329 // Regression for findings.md #1: `for true {}` / `for !ready {}`
7330 // are Go's only loop-condition slot. Pre-fix, the Phase-2B
7331 // dispatcher had no `G::ForStatement` arm, so bare-boolean
7332 // and `!`-wrapped `for` conditions silently reported zero.
7333 // `go_count_condition`'s terminal-bool / paren / unary filter
7334 // makes the walker safe across all three for-statement shapes:
7335 // bare condition, `for_clause` (init; cond; post) — whose own
7336 // `condition` field #1276 taught the walker to read — and
7337 // `range_clause`, which has no such field and contributes
7338 // nothing.
7339 check_metrics::<GoParser>(
7340 "package p\n\
7341 func F(ready bool) {\n\
7342 \x20 for true { break } // +1c\n\
7343 \x20 for !ready { break } // +1c\n\
7344 \x20 for i := 0; i < 3; i++ { _ = i } // +1c (the `<`)\n\
7345 }\n",
7346 "foo.go",
7347 |metric| {
7348 // `for true`: walker counts True (+1).
7349 // `for !ready`: walker on unary unwraps to `ready`
7350 // (+1).
7351 // `for_clause`'s condition is `i < 3`, a
7352 // `binary_expression` that `go_count_condition`
7353 // filters out; the `<` itself contributes 1 via the
7354 // pre-existing LT/GT arm.
7355 // Total: 3.
7356 assert_eq!(metric.abc.conditions_sum(), 3);
7357 insta::assert_json_snapshot!(metric.abc);
7358 },
7359 );
7360 }
7361
7362 // Issue #1276, Go's share. `for_statement`'s child(1) is the
7363 // condition only in the `for cond {}` spelling; the three-clause
7364 // form puts it one level down, in the `for_clause`'s `condition`
7365 // field. Letting the `for_clause` fall through — which the arm's
7366 // own comment used to call harmless — scored a bare three-clause
7367 // condition zero while `for a {}` scored one.
7368 #[test]
7369 fn go_three_clause_for_condition_counts() {
7370 // Bare identifier in the three-clause header: no comparison
7371 // token, so only the `for_clause` lookup can count it.
7372 check_metrics::<GoParser>(
7373 "package p\n\
7374 func F(a bool) {\n\
7375 \x20 for i := 0; a; i++ { _ = i }\n\
7376 }\n",
7377 "foo.go",
7378 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
7379 );
7380 // Negation, through `go_inspect_container`'s `!` unwrap.
7381 check_metrics::<GoParser>(
7382 "package p\n\
7383 func F(a bool) {\n\
7384 \x20 for i := 0; !a; i++ { _ = i }\n\
7385 }\n",
7386 "foo.go",
7387 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
7388 );
7389 // Empty condition: the `for_clause` exposes no `condition`
7390 // field, so zero — the same answer as Go's bare `for {}` and
7391 // as every other language since #1276.
7392 check_metrics::<GoParser>(
7393 "package p\n\
7394 func F() {\n\
7395 \x20 for i := 0; ; i++ { break }\n\
7396 }\n",
7397 "foo.go",
7398 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
7399 );
7400 // A `range_clause` carries no condition either, and the walker
7401 // must not mistake the clause itself for one.
7402 check_metrics::<GoParser>(
7403 "package p\n\
7404 func F(xs []int) {\n\
7405 \x20 for _, v := range xs { _ = v }\n\
7406 }\n",
7407 "foo.go",
7408 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
7409 );
7410 }
7411
7412 // Go's `for_statement` is the one grammar here that exposes no
7413 // `condition` field, so its header slot is located structurally and
7414 // has to skip what the field-addressed siblings get for free. Two
7415 // things it must skip, each of which `node.child(1)` got wrong:
7416 // a leading comment (tree-sitter counts comments among a node's
7417 // children — the #1181 failure), and the body of a bare `for {}`,
7418 // which IS child(1) and would otherwise be offered to
7419 // `go_count_condition` as though it were a condition.
7420 #[test]
7421 fn go_for_header_slot_skips_comments_and_the_body() {
7422 // Each pair is (source, expected conditions). The commented
7423 // spelling must agree with its bare twin.
7424 let cases = [
7425 ("for a { break }", 1),
7426 ("for /* n */ a { break }", 1),
7427 ("for i := 0; a; i++ { _ = i }", 1),
7428 ("for /* n */ i := 0; a; i++ { _ = i }", 1),
7429 ("for i := 0; /* n */ a; i++ { _ = i }", 1),
7430 // Bare infinite loop: the body is child(1) and must not be
7431 // read as the condition.
7432 ("for { break }", 0),
7433 ("for /* n */ { break }", 0),
7434 ("for _, v := range xs { _ = v }", 0),
7435 ("for /* n */ _, v := range xs { _ = v }", 0),
7436 ];
7437 let mut ran = 0;
7438 for (body, expected) in cases {
7439 let src = format!("package p\nfunc F(a bool, xs []int) {{\n\t{body}\n}}\n");
7440 assert_eq!(abc_conditions(LANG::Go, &src), expected, "`{body}`");
7441 ran += 1;
7442 }
7443 // Non-vacuity, both halves: the loop must actually have run
7444 // every row, and the rows must carry both answers — a walker
7445 // stuck at 0 or at 1 would otherwise pass half the table
7446 // silently.
7447 assert_eq!(ran, cases.len());
7448 assert!(cases.iter().any(|&(_, n)| n == 1));
7449 assert!(cases.iter().any(|&(_, n)| n == 0));
7450 }
7451
7452 #[test]
7453 fn go_if_init_statement_condition_counts() {
7454 // Regression for the code-review finding: Go's
7455 // `if x := f(); x { ... }` init-statement form puts the
7456 // short-var declaration at child(1) and the condition at
7457 // child(2). Pre-fix, the dispatcher used child(1) and
7458 // counted zero conditions for this idiomatic Go shape.
7459 // The fix uses `child_by_field_name("condition")` which
7460 // returns the condition regardless of init presence.
7461 check_metrics::<GoParser>(
7462 "package p\nfunc F() { if x := g(); x { } }\n",
7463 "foo.go",
7464 |metric| {
7465 // `x` bare-identifier condition contributes 1
7466 // (Rule 6 — bare boolean identifier in if-condition).
7467 // `g()` call contributes 1 branch but no condition.
7468 assert_eq!(metric.abc.branches_sum(), 1);
7469 assert_eq!(metric.abc.conditions_sum(), 1);
7470 insta::assert_json_snapshot!(metric.abc);
7471 },
7472 );
7473 }
7474
7475 #[test]
7476 fn go_if_boolean_literal_condition() {
7477 check_metrics::<GoParser>(
7478 "package p\n\
7479 func F() {\n\
7480 \x20 if true {} // +1c\n\
7481 \x20 if !false {} // +1c\n\
7482 }\n",
7483 "foo.go",
7484 |metric| {
7485 assert_eq!(metric.abc.conditions_sum(), 2);
7486 insta::assert_json_snapshot!(metric.abc);
7487 },
7488 );
7489 }
7490
7491 #[test]
7492 fn go_methods_arguments_with_conditions() {
7493 check_metrics::<GoParser>(
7494 "package p\n\
7495 func F(a, b bool) {\n\
7496 \x20 m(a, b) // +1b\n\
7497 \x20 m(!a, !b) // +1b +2c\n\
7498 }\n",
7499 "foo.go",
7500 |metric| {
7501 assert_eq!(metric.abc.branches_sum(), 2);
7502 assert_eq!(metric.abc.conditions_sum(), 2);
7503 insta::assert_json_snapshot!(metric.abc);
7504 },
7505 );
7506 }
7507
7508 #[test]
7509 fn go_return_with_conditions() {
7510 check_metrics::<GoParser>(
7511 "package p\n\
7512 func M1(z int) bool { return !(z >= 0) }\n\
7513 func M2(x bool) bool { return !x }\n\
7514 func M3(x, y bool) bool { return x && y }\n",
7515 "foo.go",
7516 |metric| {
7517 // M1: `>=` (1). `!(z >= 0)` walker on the unary
7518 // doesn't reach a terminal — stops at the
7519 // BinaryExpression z>=0 inside the parens. +1.
7520 // M2: walker on `!x` → 1.
7521 // M3: `&&` walker counts both → 2.
7522 // Sum: 1 + 1 + 2 = 4.
7523 assert_eq!(metric.abc.conditions_sum(), 4);
7524 insta::assert_json_snapshot!(metric.abc);
7525 },
7526 );
7527 }
7528
7529 #[test]
7530 fn go_short_circuit_with_boolean_literal_operand() {
7531 // `a && true` reports 2 conditions: one identifier, one
7532 // boolean literal. Confirms the terminal set includes
7533 // `True` / `False`.
7534 check_metrics::<GoParser>(
7535 "package p\nfunc F(a bool) bool { return a && true }\n",
7536 "foo.go",
7537 |metric| {
7538 assert_eq!(metric.abc.conditions_sum(), 2);
7539 insta::assert_json_snapshot!(metric.abc);
7540 },
7541 );
7542 }
7543
7544 // ----- Elixir -----
7545
7546 // No top-level Calls and no operators → all three vectors are
7547 // zero. Uses a bare expression rather than a `defmodule` wrapper
7548 // (which would itself be a Call → 1 branch). Confirms the
7549 // ElixirCode Abc trait is wired up and the metric emits.
7550 #[test]
7551 fn elixir_empty_unit_zero() {
7552 check_metrics::<ElixirParser>(":ok\n", "foo.ex", |metric| {
7553 assert_eq!(metric.abc.assignments_sum(), 0);
7554 assert_eq!(metric.abc.branches_sum(), 0);
7555 assert_eq!(metric.abc.conditions_sum(), 0);
7556 insta::assert_json_snapshot!(metric.abc);
7557 });
7558 }
7559
7560 // An empty `defmodule Foo do ... end` is itself ONE `Call` →
7561 // Documents that module-/function-defining macros (`defmodule`,
7562 // `def`, `defp`, `defmacro`, `defmacrop`) and declarative
7563 // directives (`alias`, `import`, `require`, `use`) are NOT
7564 // runtime dispatch and therefore do NOT inflate `branches`,
7565 // matching Cognitive's treatment.
7566 #[test]
7567 fn elixir_defmodule_is_zero_branches() {
7568 check_metrics::<ElixirParser>("defmodule Foo do\nend\n", "foo.ex", |metric| {
7569 assert_eq!(metric.abc.branches_sum(), 0);
7570 assert_eq!(metric.abc.assignments_sum(), 0);
7571 assert_eq!(metric.abc.conditions_sum(), 0);
7572 insta::assert_json_snapshot!(metric.abc);
7573 });
7574 }
7575
7576 // Pattern-match `=` counts as an assignment. Two bindings → A = 2.
7577 // `defmodule` and `def` are declarative-Call wrappers and are
7578 // filtered out of branches; the assertion focuses on assignments
7579 // so we only pin that vector.
7580 #[test]
7581 fn elixir_pattern_match_is_assignment() {
7582 check_metrics::<ElixirParser>(
7583 "defmodule Foo do\n def f do\n x = 1\n y = x + 1\n y\n end\nend\n",
7584 "foo.ex",
7585 |metric| {
7586 assert_eq!(metric.abc.assignments_sum(), 2);
7587 insta::assert_json_snapshot!(metric.abc);
7588 },
7589 );
7590 }
7591
7592 // `|>` pipeline operator: each `|>` token contributes one branch.
7593 // Two `|>` ops → +2 from the pipe operator itself. Each pipeline
7594 // step also dispatches a Call (`String.upcase(...)`,
7595 // `String.trim(...)`) — these are wrapped inside the outer
7596 // pipeline Call tree, contributing additional Call branches.
7597 // The headline assertion confirms (a) `|>` is detected and (b)
7598 // pipeline steps are not silently dropped.
7599 #[test]
7600 fn elixir_pipeline_each_step_is_branch() {
7601 check_metrics::<ElixirParser>(
7602 "defmodule Foo do\n def normalize(s) do\n s |> String.trim() |> String.upcase()\n end\nend\n",
7603 "foo.ex",
7604 |metric| {
7605 // Pipeline yields 2 `|>` branches plus Calls for
7606 // String.trim, String.upcase, and the outer pipeline
7607 // (which surfaces as a Call wrapping the binary
7608 // operator). `def` and `defmodule` are declarative
7609 // and excluded. Empirical total: B = 5.
7610 assert_eq!(metric.abc.branches_sum(), 5);
7611 assert_eq!(metric.abc.assignments_sum(), 0);
7612 insta::assert_json_snapshot!(metric.abc);
7613 },
7614 );
7615 }
7616
7617 // Comparison operators all count as conditions. Six comparisons
7618 // (`==`, `!=`, `<`, `>`, `<=`, `>=`) → C = 6.
7619 #[test]
7620 fn elixir_comparisons_are_conditions() {
7621 check_metrics::<ElixirParser>(
7622 "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",
7623 "foo.ex",
7624 |metric| {
7625 assert_eq!(metric.abc.conditions_sum(), 6);
7626 insta::assert_json_snapshot!(metric.abc);
7627 },
7628 );
7629 }
7630
7631 // Strict-equality operators `===` / `!==` count as conditions too.
7632 #[test]
7633 fn elixir_strict_equality_is_condition() {
7634 check_metrics::<ElixirParser>(
7635 "defmodule Foo do\n def f(a, b) do\n a === b or a !== b\n end\nend\n",
7636 "foo.ex",
7637 |metric| {
7638 assert_eq!(metric.abc.conditions_sum(), 2);
7639 insta::assert_json_snapshot!(metric.abc);
7640 },
7641 );
7642 }
7643
7644 // Guard `when` clause counts as a condition. One `when` → +1.
7645 // `def f(x) when x > 0` also has `>` → +1, totalling 2.
7646 #[test]
7647 fn elixir_guard_when_is_condition() {
7648 check_metrics::<ElixirParser>(
7649 "defmodule Foo do\n def f(x) when x > 0 do\n :pos\n end\nend\n",
7650 "foo.ex",
7651 |metric| {
7652 // when (+1) + > (+1) = 2
7653 assert_eq!(metric.abc.conditions_sum(), 2);
7654 insta::assert_json_snapshot!(metric.abc);
7655 },
7656 );
7657 }
7658
7659 // Keyword-shaped Calls (`case`, `cond`, `if`, `with`) each count
7660 // as one condition AND one branch. `case` here adds 1 condition
7661 // (the keyword Call) + 1 branch (the Call itself).
7662 #[test]
7663 fn elixir_case_is_condition_and_branch() {
7664 check_metrics::<ElixirParser>(
7665 "defmodule Foo do\n def f(x) do\n case x do\n 1 -> :one\n _ -> :other\n end\n end\nend\n",
7666 "foo.ex",
7667 |metric| {
7668 // conditions: case → 1
7669 assert_eq!(metric.abc.conditions_sum(), 1);
7670 insta::assert_json_snapshot!(metric.abc);
7671 },
7672 );
7673 }
7674
7675 // `cond` is structurally identical to `case` for Abc.
7676 #[test]
7677 fn elixir_cond_is_condition() {
7678 check_metrics::<ElixirParser>(
7679 "defmodule Foo do\n def f(x) do\n cond do\n x > 0 -> :pos\n true -> :other\n end\n end\nend\n",
7680 "foo.ex",
7681 |metric| {
7682 // conditions: cond (+1) + > (+1) = 2
7683 assert_eq!(metric.abc.conditions_sum(), 2);
7684 insta::assert_json_snapshot!(metric.abc);
7685 },
7686 );
7687 }
7688
7689 // `for` is a comprehension/loop, NOT in the issue's condition
7690 // list. It is still a Call so it contributes one branch, but no
7691 // condition.
7692 #[test]
7693 fn elixir_for_is_branch_not_condition() {
7694 check_metrics::<ElixirParser>(
7695 "defmodule Foo do\n def f(xs) do\n for x <- xs, do: x * 2\n end\nend\n",
7696 "foo.ex",
7697 |metric| {
7698 assert_eq!(metric.abc.conditions_sum(), 0);
7699 insta::assert_json_snapshot!(metric.abc);
7700 },
7701 );
7702 }
7703
7704 // Mixed shape, verified by hand: defmodule Call + def Call + if Call
7705 // + Call to side_effect/0 + assignment `x = 1` + comparison `x > 0`.
7706 // - Assignments: `x = 1` → A = 1.
7707 // - Branches: `defmodule` and `def` are declarative and excluded;
7708 // `if` Call + `side_effect()` Call → 2 Calls, plus 0 `|>` → B = 2.
7709 // - Conditions: `if` keyword → 1, `x > 0` → 1 → C = 2.
7710 #[test]
7711 fn elixir_mixed_abc() {
7712 check_metrics::<ElixirParser>(
7713 "defmodule Foo do\n def f do\n x = 1\n if x > 0 do\n side_effect()\n end\n end\nend\n",
7714 "foo.ex",
7715 |metric| {
7716 assert_eq!(metric.abc.assignments_sum(), 1);
7717 assert_eq!(metric.abc.branches_sum(), 2);
7718 assert_eq!(metric.abc.conditions_sum(), 2);
7719 insta::assert_json_snapshot!(metric.abc);
7720 },
7721 );
7722 }
7723
7724 #[test]
7725 fn elixir_unary_conditions_in_chain() {
7726 // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
7727 // `&&` / `||` chain is one condition. For `if a && b || c`: the
7728 // `if` keyword Call contributes 1 condition, and the walker adds
7729 // a, b, c → 3. expected: 4 conditions, consistent with the
7730 // function's cyclomatic complexity of 4 (base 1 + if + && + ||).
7731 check_metrics::<ElixirParser>(
7732 "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",
7733 "foo.ex",
7734 |metric| {
7735 assert_eq!(metric.abc.conditions_sum(), 4);
7736 },
7737 );
7738 }
7739
7740 #[test]
7741 fn elixir_comparison_operands_add_nothing() {
7742 // Isolation check: comparison operands of a `&&` chain are nested
7743 // `binary_operator` nodes, not bare boolean leaves, so the walker
7744 // adds nothing. expected: 3 = `if` (1) + `>` (1) + `>` (1); the
7745 // `&&` walker contributes 0.
7746 check_metrics::<ElixirParser>(
7747 "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",
7748 "foo.ex",
7749 |metric| {
7750 assert_eq!(metric.abc.conditions_sum(), 3);
7751 },
7752 );
7753 }
7754
7755 #[test]
7756 fn elixir_keyword_and_or_chain_counts_operands() {
7757 // The keyword forms `and` / `or` get the same Rule 9 treatment as
7758 // `&&` / `||`. expected: 4 = `if` (1) + operands a, b, c (3).
7759 check_metrics::<ElixirParser>(
7760 "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",
7761 "foo.ex",
7762 |metric| {
7763 assert_eq!(metric.abc.conditions_sum(), 4);
7764 },
7765 );
7766 }
7767
7768 // Sigil delimiter choice must not move ABC: `~s<hi>` and `~s(hi)`
7769 // are the same value spelled differently, but the `<` / `>`
7770 // delimiter tokens carry the comparison kind ids, so the unguarded
7771 // condition arm scored `~s<hi>` as 2 conditions and `~s(hi)` as 0.
7772 // The parent-is-`Sigil` guard (mirroring the Halstead getter's,
7773 // #1256) suppresses the delimiter case. expected, for each
7774 // spelling: A = 1 (the `x =` pattern match), B = 0 (a bare sigil
7775 // is not a `Call` node — verified by AST dump: `binary_operator`
7776 // wrapping `identifier`, `=`, `sigil`), C = 0 (no comparison, no
7777 // guard, no keyword Call).
7778 #[test]
7779 fn elixir_sigil_delimiter_choice_is_abc_invariant() {
7780 for src in ["x = ~s<hi>\n", "x = ~s(hi)\n"] {
7781 check_metrics::<ElixirParser>(src, "foo.ex", |metric| {
7782 assert_eq!(metric.abc.assignments_sum(), 1);
7783 assert_eq!(metric.abc.branches_sum(), 0);
7784 assert_eq!(metric.abc.conditions_sum(), 0);
7785 });
7786 }
7787 }
7788
7789 // Control for the guard above: `<` *outside* a sigil is a genuine
7790 // comparison and must keep counting even with a `<`-delimited
7791 // sigil in the same unit. expected: A = 2 (`x =`, `y =`), C = 1
7792 // (only `a < b`; the sigil's `<` / `>` delimiters are guarded).
7793 #[test]
7794 fn elixir_lt_comparison_still_counts_beside_sigil() {
7795 check_metrics::<ElixirParser>("x = ~s<hi>\ny = a < b\n", "foo.ex", |metric| {
7796 assert_eq!(metric.abc.assignments_sum(), 2);
7797 assert_eq!(metric.abc.conditions_sum(), 1);
7798 });
7799 }
7800
7801 // ----- C++ -----
7802
7803 #[test]
7804 fn cpp_empty_unit_zero() {
7805 // No code → A=B=C=0. Wires up the trait and exercises the
7806 // per-language compute reachability.
7807 check_metrics::<CppParser>("", "empty.cpp", |metric| {
7808 assert_eq!(metric.abc.assignments_sum(), 0);
7809 assert_eq!(metric.abc.branches_sum(), 0);
7810 assert_eq!(metric.abc.conditions_sum(), 0);
7811 insta::assert_json_snapshot!(metric.abc);
7812 });
7813 }
7814
7815 #[test]
7816 fn cpp_plain_and_compound_assignments_count() {
7817 // `int x = 0` is an `init_declarator` carrying an `=` token
7818 // and counts as 1 (post-#393: the literal Fitzpatrick rule
7819 // counts every `=` operator, matching the JS impl's
7820 // `let x = 5` treatment). `x = 5`, `x += 2`, `x = 7` all
7821 // parse as `assignment_expression` → 3. Total A = 4.
7822 check_metrics::<CppParser>(
7823 "void f() { int x = 0; x = 5; x += 2; x = 7; }",
7824 "foo.cpp",
7825 |metric| {
7826 assert_eq!(metric.abc.assignments_sum(), 4);
7827 assert_eq!(metric.abc.branches_sum(), 0);
7828 assert_eq!(metric.abc.conditions_sum(), 0);
7829 insta::assert_json_snapshot!(metric.abc);
7830 },
7831 );
7832 }
7833
7834 #[test]
7835 fn cpp_increment_and_decrement_count_as_assignment() {
7836 // `x++` / `--x` / prefix and postfix forms each parse as
7837 // `update_expression` and count as 1 assignment per
7838 // Fitzpatrick — 4. `int x = 0` (init_declarator with `=`)
7839 // adds 1 (post-#393). Total A = 5.
7840 check_metrics::<CppParser>(
7841 "void f() { int x = 0; x++; --x; ++x; x--; }",
7842 "foo.cpp",
7843 |metric| {
7844 assert_eq!(metric.abc.assignments_sum(), 5);
7845 insta::assert_json_snapshot!(metric.abc);
7846 },
7847 );
7848 }
7849
7850 #[test]
7851 fn cpp_init_declarators_count_as_assignments() {
7852 // Issue #393 regression: `int a=1;`, `int b=2;`, `int c=a+b;`,
7853 // `int d=0;` are all `init_declarator` nodes with `=` → 4
7854 // assignments. `d=5;` is one plain `assignment_expression`,
7855 // `d+=1;` is one compound. Total A = 4 + 1 + 1 = 6.
7856 check_metrics::<CppParser>(
7857 "void f() { int a=1; int b=2; int c=a+b; int d=0; d=5; d+=1; }",
7858 "foo.cpp",
7859 |metric| {
7860 assert_eq!(metric.abc.assignments_sum(), 6);
7861 insta::assert_json_snapshot!(metric.abc);
7862 },
7863 );
7864 }
7865
7866 #[test]
7867 fn cpp_declaration_without_initializer_does_not_count() {
7868 // `int a;` parses as a plain declarator inside `declaration`,
7869 // NOT an `init_declarator` (the latter only appears when an
7870 // initializer is present). Regression test for issue #393:
7871 // un-initialised declarations contribute zero to A.
7872 check_metrics::<CppParser>("void f() { int a; a = 5; }", "foo.cpp", |metric| {
7873 // Only `a = 5` (assignment_expression) → A = 1.
7874 assert_eq!(metric.abc.assignments_sum(), 1);
7875 insta::assert_json_snapshot!(metric.abc);
7876 });
7877 }
7878
7879 #[test]
7880 fn cpp_init_declarator_brace_paren_init_does_not_count() {
7881 // `init_declarator` has two grammar forms: `declarator = value`
7882 // (the `=` form) and `declarator argument_list_or_initializer_list`
7883 // (the `int x(5);` / `int x{5};` direct-init forms). Only the
7884 // first form contains an `=` token, so only it should count.
7885 // Regression test pinning that distinction so that
7886 // refactorings of the init_declarator arm don't accidentally
7887 // start counting direct-init too.
7888 check_metrics::<CppParser>(
7889 "void f() { int x(5); int y{7}; x = 1; }",
7890 "foo.cpp",
7891 |metric| {
7892 // Only `x = 1` (assignment_expression) → A = 1.
7893 assert_eq!(metric.abc.assignments_sum(), 1);
7894 insta::assert_json_snapshot!(metric.abc);
7895 },
7896 );
7897 }
7898
7899 #[test]
7900 fn cpp_calls_are_branches() {
7901 // Free call + member-fn call (parses as `call_expression` with
7902 // a `field_expression` callee) + `new` allocation. All three
7903 // are branches → B = 3. `auto* p = new int(5)` is also an
7904 // `init_declarator` with `=` so it contributes one assignment
7905 // (post-#393); the snapshot pins that magnitude.
7906 check_metrics::<CppParser>(
7907 "struct S { void m(); }; void g(); void f() { g(); S s; s.m(); auto* p = new int(5); }",
7908 "foo.cpp",
7909 |metric| {
7910 assert_eq!(metric.abc.branches_sum(), 3);
7911 assert_eq!(metric.abc.assignments_sum(), 1);
7912 insta::assert_json_snapshot!(metric.abc);
7913 },
7914 );
7915 }
7916
7917 #[test]
7918 fn cpp_comparisons_count_conditions() {
7919 // `<`, `>`, `<=`, `>=`, `==`, `!=`, and the C++20 spaceship
7920 // `<=>` each contribute one condition. The `||` short-
7921 // circuits add 0 (Fitzpatrick Rule 5, issue #395). Six
7922 // comparisons in the `||` chain plus `<=>` (1) plus the
7923 // outer `== 0` (1) → C = 8.
7924 check_metrics::<CppParser>(
7925 "#include <compare>\n\
7926 bool f(int a, int b) {\n\
7927 return a < b || a > b || a <= b || a >= b || a == b || a != b || (a <=> b) == 0;\n\
7928 }\n",
7929 "foo.cpp",
7930 |metric| {
7931 // `<`, `>`, `<=`, `>=`, `==`, `!=` → 6 comparisons
7932 // from the chained `||` expression. `(a <=> b) == 0`
7933 // adds the spaceship `<=>` (1) + the outer `== 0`
7934 // (1) → 8 total. The six `||` short-circuits add 0
7935 // (Fitzpatrick Rule 5; issue #395).
7936 assert_eq!(metric.abc.conditions_sum(), 8);
7937 insta::assert_json_snapshot!(metric.abc);
7938 },
7939 );
7940 }
7941
7942 #[test]
7943 fn cpp_short_circuit_ops_not_counted_directly() {
7944 // `&&` and `||` do NOT count on their own (see the
7945 // module-level `Stats` doc-comment; #395). Phase-2 walker
7946 // counts each operand of a logical chain once (#403), but
7947 // when every operand is itself a relational expression
7948 // (`a == b`, `a > 0`, `b < 0`) the walker doesn't add
7949 // anything on top of the existing comparison-token tally
7950 // — relational sub-expressions are not in
7951 // `cpp_bool_terminal_kinds!()` and `cpp_inspect_container`
7952 // does not recurse into them.
7953 check_metrics::<CppParser>(
7954 "bool f(int a, int b) { return a == b && a > 0 || b < 0; }",
7955 "foo.cpp",
7956 |metric| {
7957 // == 1, > 1, < 1; the walker on && and || finds
7958 // BinaryExpression operands (not terminal-bool) and
7959 // adds nothing. Total: 3.
7960 assert_eq!(metric.abc.conditions_sum(), 3);
7961 insta::assert_json_snapshot!(metric.abc);
7962 },
7963 );
7964 }
7965
7966 #[test]
7967 fn cpp_generic_brackets_not_conditions() {
7968 // `<` / `>` in `std::vector<int>` are `template_argument_list`
7969 // delimiters, NOT comparison operators. The `binary_expression`
7970 // parent check must filter them out → C = 0.
7971 check_metrics::<CppParser>(
7972 "#include <vector>\nstd::vector<int> f() { return std::vector<int>{}; }",
7973 "foo.cpp",
7974 |metric| {
7975 assert_eq!(metric.abc.conditions_sum(), 0);
7976 insta::assert_json_snapshot!(metric.abc);
7977 },
7978 );
7979 }
7980
7981 #[test]
7982 fn cpp_else_and_ternary_count_conditions() {
7983 // `if (cond) ... else ...` + ternary `cond ? a : b`. The
7984 // `if`-keyword is NOT a condition (its condition is the
7985 // comparison inside, which counts separately). `else` adds 1,
7986 // `?` adds 1. Two comparisons (`a > b`, `b < 0`) → 2. Total = 4.
7987 check_metrics::<CppParser>(
7988 "int f(int a, int b) {\n\
7989 if (a > b) { return a; } else { return b; }\n\
7990 return (b < 0) ? -b : b;\n\
7991 }\n",
7992 "foo.cpp",
7993 |metric| {
7994 assert_eq!(metric.abc.conditions_sum(), 4);
7995 insta::assert_json_snapshot!(metric.abc);
7996 },
7997 );
7998 }
7999
8000 // Issue #1102. A ternary's condition and both branch operands are
8001 // Fitzpatrick Rule 9 unary conditions, exactly as `java_walk_ternary`
8002 // has always counted them. Before the fix the C family scored
8003 // `a ? !b : !c` as 1 — the `?` token alone — against Java's 4.
8004 #[test]
8005 fn cpp_ternary_operand_slots_count_as_unary_conditions() {
8006 // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4.
8007 check_metrics::<CppParser>("void f() { x = a ? !b : !c; }", "foo.cpp", |metric| {
8008 assert_eq!(metric.abc.conditions_sum(), 4);
8009 });
8010 // No-double-count pin: `?` (1) + `>` (1) = 2, unchanged by the
8011 // fix. The parenthesised condition unwraps to a
8012 // `binary_expression`, which is not a boolean terminal, and
8013 // neither branch is negated — the `!` is the type-free proxy for
8014 // "this operand is boolean", so an unnegated branch contributes
8015 // nothing.
8016 check_metrics::<CppParser>("void f() { x = (a > 0) ? b : -b; }", "foo.cpp", |metric| {
8017 assert_eq!(metric.abc.conditions_sum(), 2);
8018 });
8019 // Nested: outer `?` (1) + outer condition `a` (1) + inner `?`
8020 // (1) + inner condition `b` (1) = 4. The outer consequence is
8021 // the inner ternary — neither a boolean terminal nor a
8022 // paren / `!` wrapper — so it adds nothing on its own and the
8023 // inner one is reached by the walk, not by descent.
8024 check_metrics::<CppParser>("void f() { x = a ? b ? c : d : e; }", "foo.cpp", |metric| {
8025 assert_eq!(metric.abc.conditions_sum(), 4);
8026 });
8027 // A negated *condition* is the only input that reaches the
8028 // walker's `else` fallback: `!a` is neither a boolean terminal
8029 // (so the terminal arm skips it) nor an operand slot (so
8030 // `cpp_inspect_container` is never called on it from anywhere
8031 // else). Every other condition fixture in this file wraps a
8032 // comparison, which the fallback resolves to 0 — delete the
8033 // fallback and only this case moves. `?` (1) + `!a` (1) = 2.
8034 check_metrics::<CppParser>("void f() { x = !a ? b : c; }", "foo.cpp", |metric| {
8035 assert_eq!(metric.abc.conditions_sum(), 2);
8036 });
8037 }
8038
8039 // The GNU short-ternary `a ?: b` elides the consequence, so the
8040 // C-family grammar marks that field optional and the alternative
8041 // lands at child(3) rather than child(4). Addressing the operand
8042 // slots by grammar field name — never by index — is what keeps `!b`
8043 // counted here; a fixed `child(4)` reads `None` and scores 2.
8044 #[test]
8045 fn cpp_elided_ternary_consequence_still_walks_the_alternative() {
8046 // `?` (1) + condition `a` (1) + `!b` (1) = 3.
8047 check_metrics::<CppParser>("void f() { x = a ?: !b; }", "foo.cpp", |metric| {
8048 assert_eq!(metric.abc.conditions_sum(), 3);
8049 });
8050 }
8051
8052 // `cpp_walk_ternary` is shared by the C, ObjC, and Mozcpp ABC impls
8053 // exactly as `cpp_inspect_container` is, so each needs its own
8054 // dispatcher arm. Mozcpp owns no file extension and so gets no
8055 // integration-snapshot coverage at all — this parity assertion is
8056 // its only guard.
8057 //
8058 // The expected value is *derived from the C++ run*, not hardcoded,
8059 // so the four languages cannot silently drift apart if the C++
8060 // expectation ever legitimately moves.
8061 #[test]
8062 fn c_family_ternary_operand_slots_agree_with_cpp() {
8063 const SRC: &str = "void f() { x = a ? !b : !c; }\n";
8064 let conditions = abc_conditions;
8065
8066 let cpp = conditions(LANG::Cpp, SRC);
8067 // Non-degenerate: a zeroed reference would make every
8068 // comparison below vacuous.
8069 assert_eq!(cpp, 4, "C++ reference value for `a ? !b : !c`");
8070
8071 assert_eq!(conditions(LANG::C, SRC), cpp, "C must match C++");
8072 assert_eq!(conditions(LANG::Mozcpp, SRC), cpp, "Mozcpp must match C++");
8073 assert_eq!(
8074 conditions(
8075 LANG::Objc,
8076 "@implementation Foo\n\
8077 - (void)bar {\n\
8078 x = a ? !b : !c;\n\
8079 }\n\
8080 @end\n",
8081 ),
8082 cpp,
8083 "ObjC must match C++"
8084 );
8085 }
8086
8087 // Issue #1276, C-family half. `cpp_walk_for_statement` is the
8088 // `for` header's counterpart to the `if` / `while` arms: the slot
8089 // is a bare expression rather than a `condition_clause`, so it
8090 // needs the top-level terminal check `cpp_walk_ternary` already
8091 // had. Every fixture is a shape only that walker can classify —
8092 // a comparison-shaped condition proves nothing, the `<` token arm
8093 // counts it either way (grammar-dispatch §11).
8094 #[test]
8095 fn cpp_for_condition_slot_counts_unary_conditions() {
8096 // Bare identifier: the whole condition, no operator token.
8097 check_metrics::<CppParser>("void f(int a) { for (; a; ) {} }", "foo.cpp", |metric| {
8098 assert_eq!(metric.abc.conditions_sum(), 1);
8099 });
8100 // Negation: reaches the terminal through
8101 // `cpp_inspect_container`'s `!` unwrap.
8102 check_metrics::<CppParser>("void f(int a) { for (; !a; ) {} }", "foo.cpp", |metric| {
8103 assert_eq!(metric.abc.conditions_sum(), 1);
8104 });
8105 // Parentheses: counts only because the `for_statement` parent
8106 // seeds `has_boolean_content` — the seed #1276 found dead.
8107 check_metrics::<CppParser>("void f(int a) { for (; (a); ) {} }", "foo.cpp", |metric| {
8108 assert_eq!(metric.abc.conditions_sum(), 1);
8109 });
8110 // No-double-count pin: the `<` token arm already counted this
8111 // shape before the fix and the walker must not add a second.
8112 // The two assignments confirm the header parsed as the
8113 // three-clause form rather than degenerating.
8114 check_metrics::<CppParser>(
8115 "void f(int n) { for (int i = 0; i < n; i++) {} }",
8116 "foo.cpp",
8117 |metric| {
8118 assert_eq!(metric.abc.conditions_sum(), 1);
8119 assert_eq!(metric.abc.assignments_sum(), 2);
8120 },
8121 );
8122 // Empty condition: no `condition` field, no decision, zero.
8123 check_metrics::<CppParser>("void f() { for (;;) { break; } }", "foo.cpp", |metric| {
8124 assert_eq!(metric.abc.conditions_sum(), 0);
8125 });
8126 }
8127
8128 // `cpp_walk_for_statement` is shared by the C, ObjC and Mozcpp ABC
8129 // impls the way `cpp_walk_ternary` is, so each needs its own
8130 // dispatcher arm — and Mozcpp, which owns no file extension, has no
8131 // integration-snapshot coverage at all, making this its only guard.
8132 // The expected value is derived from the C++ run rather than
8133 // hardcoded, so the four cannot silently drift apart.
8134 #[test]
8135 fn c_family_for_condition_slot_agrees_with_cpp() {
8136 const SRC: &str = "void f(int a) { for (; !a; ) {} }\n";
8137 let conditions = abc_conditions;
8138
8139 let cpp = conditions(LANG::Cpp, SRC);
8140 // Non-degenerate: a zeroed reference makes every comparison
8141 // below vacuous.
8142 assert_eq!(cpp, 1, "C++ reference value for `for (; !a; )`");
8143
8144 assert_eq!(conditions(LANG::C, SRC), cpp, "C must match C++");
8145 assert_eq!(conditions(LANG::Mozcpp, SRC), cpp, "Mozcpp must match C++");
8146 assert_eq!(
8147 conditions(
8148 LANG::Objc,
8149 "@implementation Foo\n\
8150 - (void)bar {\n\
8151 for (; !a; ) {}\n\
8152 }\n\
8153 @end\n",
8154 ),
8155 cpp,
8156 "ObjC must match C++"
8157 );
8158 }
8159
8160 #[test]
8161 fn cpp_switch_cases_count_default_excluded() {
8162 // `case 1`, `case 2` → 2 conditions. `default` is intentionally
8163 // excluded (the unconditional fallthrough, mirroring cyclomatic's
8164 // `Case`-only count). Since #469 every C-family language —
8165 // Java, C#, Groovy, JS, TS — agrees on this; C++ already did.
8166 // C = 2.
8167 check_metrics::<CppParser>(
8168 "void f(int x) {\n\
8169 switch (x) {\n\
8170 case 1: break;\n\
8171 case 2: break;\n\
8172 default: break;\n\
8173 }\n\
8174 }\n",
8175 "foo.cpp",
8176 |metric| {
8177 assert_eq!(metric.abc.conditions_sum(), 2);
8178 insta::assert_json_snapshot!(metric.abc);
8179 },
8180 );
8181 }
8182
8183 #[test]
8184 fn cpp_try_catch_count_conditions() {
8185 // `try` and `catch` each add one condition (Fitzpatrick's rule;
8186 // Java's impl above counts them too).
8187 check_metrics::<CppParser>(
8188 "void f() { try { } catch (int) { } catch (...) { } }",
8189 "foo.cpp",
8190 |metric| {
8191 // 1 `try` + 2 `catch` arms = 3.
8192 assert_eq!(metric.abc.conditions_sum(), 3);
8193 insta::assert_json_snapshot!(metric.abc);
8194 },
8195 );
8196 }
8197
8198 #[test]
8199 fn cpp_complex_function_abc() {
8200 // Mixed-shape regression: assignments, calls, conditions,
8201 // ternary, switch, new. Verified by hand:
8202 // - assignments: `int x = 0` (init_declarator with `=`),
8203 // `x = 5`, `x += 2`, `x++`, `x = (a > b) ? a : b`, `x = b`,
8204 // `auto* p = new int(5)` (init_declarator with `=`) → A = 7
8205 // (post-#393: every `=` in an init_declarator counts).
8206 // - branches: `f(a, b)` self-call + `new int(5)` → B = 2.
8207 // - conditions: `a == b` (1) + `a > 0` (1) inside the if;
8208 // `&&` itself is NOT a condition (Fitzpatrick Rule 5,
8209 // issue #395). `a > b` (1) + `?` (1) in the ternary.
8210 // `else` (1, from the `else if` keyword) + `a < b` (1)
8211 // in the else-if. `!x` contributes 1 via the unary-
8212 // conditional walker (Fitzpatrick Rule 9, issue #403):
8213 // the `||` walker treats `!x` as a unary boolean operand
8214 // and counts the wrapped Identifier once. `case 1`,
8215 // `case 2` → 2. `default` excluded. Total C = 9.
8216 check_metrics::<CppParser>(
8217 "int f(int a, int b) {\n\
8218 int x = 0;\n\
8219 x = 5;\n\
8220 x += 2;\n\
8221 x++;\n\
8222 if (a == b && a > 0) {\n\
8223 x = (a > b) ? a : b;\n\
8224 } else if (a < b || !x) {\n\
8225 x = b;\n\
8226 }\n\
8227 switch (x) {\n\
8228 case 1: break;\n\
8229 case 2: break;\n\
8230 default: break;\n\
8231 }\n\
8232 auto* p = new int(5);\n\
8233 return f(a, b);\n\
8234 }\n",
8235 "foo.cpp",
8236 |metric| {
8237 assert_eq!(metric.abc.assignments_sum(), 7);
8238 assert_eq!(metric.abc.branches_sum(), 2);
8239 assert_eq!(metric.abc.conditions_sum(), 9);
8240 insta::assert_json_snapshot!(metric.abc);
8241 },
8242 );
8243 }
8244
8245 #[test]
8246 fn cpp_if_multiple_conditions() {
8247 // Fitzpatrick Rule 9 walker (issue #403): each operand of a
8248 // `&&` / `||` chain is one condition.
8249 check_metrics::<CppParser>(
8250 "void f(bool a, bool b, bool c, bool d) {\n\
8251 \x20 if (a || b || c || d) {} // +4c\n\
8252 \x20 if (a && b && c) {} // +3c\n\
8253 \x20 if (!a && !b) {} // +2c\n\
8254 }\n",
8255 "foo.cpp",
8256 |metric| {
8257 assert_eq!(metric.abc.conditions_sum(), 9);
8258 insta::assert_json_snapshot!(metric.abc);
8259 },
8260 );
8261 }
8262
8263 #[test]
8264 fn cpp_while_and_do_while_conditions() {
8265 // Exercise both the WhileStatement and DoStatement arms via
8266 // the walker on the `&&` / `||` tokens inside their parens.
8267 check_metrics::<CppParser>(
8268 "void f(bool a, bool b) {\n\
8269 \x20 while (a || b) {} // +2c\n\
8270 \x20 do {} while (a && !b); // +2c\n\
8271 }\n",
8272 "foo.cpp",
8273 |metric| {
8274 assert_eq!(metric.abc.conditions_sum(), 4);
8275 insta::assert_json_snapshot!(metric.abc);
8276 },
8277 );
8278 }
8279
8280 #[test]
8281 fn cpp_if_constexpr_condition_counts() {
8282 // Regression for the code-review finding: C++ `if constexpr
8283 // (cond)` puts the `constexpr` keyword at child(1) and the
8284 // condition_clause at child(2). Pre-fix, the dispatcher used
8285 // child(1) and counted zero conditions for the `constexpr`
8286 // form. The fix uses `child_by_field_name("condition")`
8287 // which returns the condition_clause regardless of the
8288 // optional `constexpr` keyword.
8289 check_metrics::<CppParser>(
8290 "template <int N> void f() {\n\
8291 \x20 if constexpr (true) { } // +1c\n\
8292 \x20 if (false) { } // +1c\n\
8293 }\n",
8294 "foo.cpp",
8295 |metric| {
8296 assert_eq!(metric.abc.conditions_sum(), 2);
8297 insta::assert_json_snapshot!(metric.abc);
8298 },
8299 );
8300 }
8301
8302 #[test]
8303 fn cpp_cast_expression_in_logical_chain_counts() {
8304 // Regression for findings.md round-2 #1 (C++):
8305 // `if ((bool)ptr && ready) {}` had the `||` walker missing
8306 // the `(bool)ptr` operand because `CastExpression` was not
8307 // in `cpp_bool_terminal_kinds!()`. Mirrors C#'s
8308 // `csharp_bool_terminal_kinds!()` which lists
8309 // `CastExpression` (lesson 19, #372).
8310 check_metrics::<CppParser>(
8311 "void f(void* ptr, bool ready) { if ((bool)ptr && ready) { } }\n",
8312 "foo.cpp",
8313 |metric| {
8314 // `&&` walker counts both operands: `(bool)ptr` (1)
8315 // and `ready` (1). Total: 2.
8316 assert_eq!(metric.abc.conditions_sum(), 2);
8317 insta::assert_json_snapshot!(metric.abc);
8318 },
8319 );
8320 }
8321
8322 #[test]
8323 fn cpp_qualified_identifier_condition_counts() {
8324 // Regression for findings.md #3 (C++): tree-sitter-cpp emits
8325 // `qualified_identifier` under four kind_ids (573..576) per
8326 // the production-rule path; runtime kind for `ns::flag` is
8327 // 574 (`QualifiedIdentifier2`). Pre-fix the
8328 // `cpp_bool_terminal_kinds!()` macro listed neither the
8329 // primary nor any alias, so `if (n::flag) {}` reported zero
8330 // conditions. The macro now includes all four variants
8331 // (lesson #2).
8332 check_metrics::<CppParser>(
8333 "namespace n { extern bool flag; }\n\
8334 void f() { if (n::flag) { } }\n",
8335 "foo.cpp",
8336 |metric| {
8337 assert_eq!(metric.abc.conditions_sum(), 1);
8338 insta::assert_json_snapshot!(metric.abc);
8339 },
8340 );
8341 }
8342
8343 #[test]
8344 fn cpp_if_boolean_literal_condition() {
8345 check_metrics::<CppParser>(
8346 "void f() {\n\
8347 \x20 if (true) {} // +1c\n\
8348 \x20 if (!false) {} // +1c\n\
8349 \x20 while (true) {} // +1c\n\
8350 \x20 do {} while (false); // +1c\n\
8351 }\n",
8352 "foo.cpp",
8353 |metric| {
8354 assert_eq!(metric.abc.conditions_sum(), 4);
8355 insta::assert_json_snapshot!(metric.abc);
8356 },
8357 );
8358 }
8359
8360 #[test]
8361 fn cpp_methods_arguments_with_conditions() {
8362 check_metrics::<CppParser>(
8363 "void f(bool a, bool b) {\n\
8364 \x20 m(a, b); // +1b\n\
8365 \x20 m(!a, !b); // +1b +2c\n\
8366 }\n",
8367 "foo.cpp",
8368 |metric| {
8369 assert_eq!(metric.abc.branches_sum(), 2);
8370 assert_eq!(metric.abc.conditions_sum(), 2);
8371 insta::assert_json_snapshot!(metric.abc);
8372 },
8373 );
8374 }
8375
8376 #[test]
8377 fn cpp_return_with_conditions() {
8378 check_metrics::<CppParser>(
8379 "bool m1(int z) { return !(z >= 0); }\n\
8380 bool m2(bool x) { return (((!x))); }\n\
8381 bool m3(bool x, bool y) { return x && y; }\n",
8382 "foo.cpp",
8383 |metric| {
8384 // m1: !(z >= 0) → `>=` (1). `!` wraps a paren'd
8385 // BinaryExpression — inspect_container reaches
8386 // the inner BinaryExpression and stops, no
8387 // walker count. +1.
8388 // m2: (((!x))) → ReturnStatement → inspect_container
8389 // unwraps three parens + one unary → reaches `x`
8390 // in has_boolean_content=true (seeded by the
8391 // unary `!`). +1.
8392 // m3: x && y → `&&` walker counts both → +2.
8393 // Sum: 1 + 1 + 2 = 4.
8394 assert_eq!(metric.abc.conditions_sum(), 4);
8395 insta::assert_json_snapshot!(metric.abc);
8396 },
8397 );
8398 }
8399
8400 #[test]
8401 fn cpp_short_circuit_with_boolean_literal_operand() {
8402 // `a && true` reports 2 conditions: one for the identifier
8403 // operand, one for the `True` literal operand.
8404 check_metrics::<CppParser>(
8405 "bool f(bool a) { return a && true; }\n",
8406 "foo.cpp",
8407 |metric| {
8408 assert_eq!(metric.abc.conditions_sum(), 2);
8409 insta::assert_json_snapshot!(metric.abc);
8410 },
8411 );
8412 }
8413
8414 #[test]
8415 fn javascript_empty_unit_zero() {
8416 // No code → A=B=C=0. Wires up the trait and exercises the
8417 // per-language compute reachability.
8418 check_metrics::<JavascriptParser>("", "empty.js", |metric| {
8419 assert_eq!(metric.abc.assignments_sum(), 0);
8420 assert_eq!(metric.abc.branches_sum(), 0);
8421 assert_eq!(metric.abc.conditions_sum(), 0);
8422 insta::assert_json_snapshot!(metric.abc);
8423 });
8424 }
8425
8426 #[test]
8427 fn javascript_plain_and_compound_assignments_count() {
8428 // `let` / `var` declarations behave like TypeScript: only a
8429 // `const` initializer is suppressed. So `let x = 0` does count as
8430 // A=+1; only `const PI = 3.14` would be elided. Plain `x = 5`,
8431 // `x += 2`, `x = 7` all count → A = 4 total here.
8432 check_metrics::<JavascriptParser>(
8433 "function f() { let x = 0; x = 5; x += 2; x = 7; }",
8434 "foo.js",
8435 |metric| {
8436 assert_eq!(metric.abc.assignments_sum(), 4);
8437 assert_eq!(metric.abc.branches_sum(), 0);
8438 assert_eq!(metric.abc.conditions_sum(), 0);
8439 insta::assert_json_snapshot!(metric.abc);
8440 },
8441 );
8442 }
8443
8444 #[test]
8445 fn javascript_const_initializer_not_assignment() {
8446 // `const PI = 3.14` must NOT count as an assignment — its `=`
8447 // initialises a `const` binding. `let x = 1` and `var y = 2`
8448 // still count (matches the TS impl: only `const` suppresses).
8449 check_metrics::<JavascriptParser>(
8450 "function f() { const PI = 3.14; let x = 1; var y = 2; x = 9; }",
8451 "foo.js",
8452 |metric| {
8453 // `const PI` suppressed; `let x = 1`, `var y = 2`,
8454 // `x = 9` all count → A = 3.
8455 assert_eq!(metric.abc.assignments_sum(), 3);
8456 insta::assert_json_snapshot!(metric.abc);
8457 },
8458 );
8459 }
8460
8461 #[test]
8462 fn javascript_asi_const_does_not_suppress_later_assignments() {
8463 // The issue #1277 reproducer verbatim. JavaScript half of the
8464 // cluster documented at
8465 // `typescript_asi_const_does_not_suppress_later_assignments`.
8466 check_metrics::<JavascriptParser>(
8467 "function f() {
8468 const a = 1
8469 x = 2
8470 return x
8471 }",
8472 "foo.js",
8473 |metric| {
8474 // Pre-#1277 this reported 0; the semicolon-terminated
8475 // spelling reported 1.
8476 assert_eq!(metric.abc.assignments_sum(), 1);
8477 },
8478 );
8479 }
8480
8481 #[test]
8482 fn javascript_nested_arrow_const_does_not_leak() {
8483 // The ASI leak beside a nested space: the arrow body opens its
8484 // own space, so `y = 1` was always counted there, but
8485 // `const f = () => …` has no `;`, so `h`'s sentinel stayed live
8486 // and suppressed `z = 2`. The stack was per-space state that
8487 // `Stats::merge` never carried, so this is the terminator defect
8488 // and not a second route.
8489 check_metrics::<JavascriptParser>(
8490 "function h() {
8491 const f = () => { y = 1 }
8492 z = 2
8493 }",
8494 "foo.js",
8495 |metric| {
8496 // `y = 1` and `z = 2`; the `const` initializer is
8497 // suppressed. Pre-#1277: 1.
8498 assert_eq!(metric.abc.assignments_sum(), 2);
8499 },
8500 );
8501 }
8502
8503 #[test]
8504 fn javascript_non_declarator_equals_still_count() {
8505 // An `=` that does not belong to a `const` declarator is always
8506 // an assignment: a class `field_definition` initializer, a
8507 // default-parameter `assignment_pattern`, and a destructured
8508 // parameter's defaults, whose climb crosses the same pattern
8509 // layers as a `const` pattern's but ends at `formal_parameters`.
8510 // A predicate that accepted any pattern ancestor as a declarator
8511 // would zero the last three.
8512 check_metrics::<JavascriptParser>(
8513 "class K { f = 1; g(p = 2, {q = 3} = {}) { let d = 4 } }",
8514 "foo.js",
8515 |metric| {
8516 // `f = 1`, `p = 2`, `q = 3`, `= {}`, `let d = 4`.
8517 assert_eq!(metric.abc.assignments_sum(), 5);
8518 },
8519 );
8520 }
8521
8522 #[test]
8523 fn javascript_const_initializer_value_assignments_still_count() {
8524 // An `=` inside a `const` initializer's *value* is an
8525 // `assignment_expression`: its climb reaches no pattern layer and
8526 // no declarator, so it counts. The pre-#1277 sentinel
8527 // blanket-suppressed every `=` between `const` and `;`, which is
8528 // the shape that moved the pdf.js corpus snapshots
8529 // (`const bbox = (this.data.rect = …)`).
8530 check_metrics::<JavascriptParser>(
8531 "function m(o, a, b) { const x = (o.p = 1); const y = a || (b = 2); }",
8532 "foo.js",
8533 |metric| {
8534 // `o.p = 1` and `b = 2`; the two `const` initializers are
8535 // suppressed. Pre-#1277: 0.
8536 assert_eq!(metric.abc.assignments_sum(), 2);
8537 },
8538 );
8539 }
8540
8541 #[test]
8542 fn javascript_const_declarator_shapes_stay_suppressed() {
8543 // JavaScript half of
8544 // `typescript_const_declarator_shapes_stay_suppressed`.
8545 check_metrics::<JavascriptParser>(
8546 "function f(o, xs) {
8547 const a = 1, b = 2
8548 const {c = 5, d: {e = 6} = {}} = o
8549 const [g = 7, ...[h = 8]] = xs
8550 let i = 3
8551 var j = 4
8552 for (const x of xs) { k(x) }
8553 }",
8554 "foo.js",
8555 |metric| {
8556 assert_eq!(metric.abc.assignments_sum(), 2);
8557 },
8558 );
8559 }
8560
8561 #[test]
8562 fn javascript_increment_and_decrement_count_as_assignment() {
8563 // `x++` (post) and `--x` (pre) both update an lvalue and so
8564 // count as assignments. Combined with the `let x = 0`
8565 // initializer (which counts under the JS/TS rule — only `const`
8566 // suppresses), A = 3.
8567 check_metrics::<JavascriptParser>(
8568 "function f() { let x = 0; x++; --x; }",
8569 "foo.js",
8570 |metric| {
8571 assert_eq!(metric.abc.assignments_sum(), 3);
8572 insta::assert_json_snapshot!(metric.abc);
8573 },
8574 );
8575 }
8576
8577 #[test]
8578 fn javascript_calls_are_branches() {
8579 // `g(1)` is a `call_expression` → B = 1. `new Foo(2)` is a
8580 // `new_expression` → B = 1. Total B = 2.
8581 check_metrics::<JavascriptParser>(
8582 "function f() { g(1); new Foo(2); }",
8583 "foo.js",
8584 |metric| {
8585 assert_eq!(metric.abc.branches_sum(), 2);
8586 assert_eq!(metric.abc.conditions_sum(), 0);
8587 insta::assert_json_snapshot!(metric.abc);
8588 },
8589 );
8590 }
8591
8592 #[test]
8593 fn javascript_comparisons_count_conditions() {
8594 // `==`, `===`, `!=`, `!==`, `<`, `>`, `<=`, `>=` each count
8595 // once. The `&&` / `||` short-circuit operators are NOT
8596 // counted as conditions in this impl (matches the TS
8597 // precedent — short-circuit ops are folded into the
8598 // surrounding `if` / control-flow arm, not separately).
8599 // Total C = 8.
8600 check_metrics::<JavascriptParser>(
8601 "function f(a, b) { return a == b && a === b && a != b && a !== b && a < b && a > b && a <= b && a >= b; }",
8602 "foo.js",
8603 |metric| {
8604 assert_eq!(metric.abc.conditions_sum(), 8);
8605 insta::assert_json_snapshot!(metric.abc);
8606 },
8607 );
8608 }
8609
8610 #[test]
8611 fn javascript_number_truthy_condition_counts() {
8612 // Regression for #772: JS treats every non-zero number as
8613 // truthy, so `while (5)` and `x && 5` should each count their
8614 // numeric literal as a Fitzpatrick unary condition. Pre-fix
8615 // `javascript_bool_terminal_kinds!()` listed `True` / `False`
8616 // but omitted `Number`, so the walker dropped every numeric-
8617 // truthy operand (mirrors the Lua `Number` fix).
8618 check_metrics::<JavascriptParser>(
8619 "function f(x) { while (5) {} return x && 5; }",
8620 "foo.js",
8621 |metric| {
8622 // `while (5)` → Number literal (+1). `x && 5` → both
8623 // operands count: identifier `x` (+1), Number `5` (+1).
8624 // Total: 3.
8625 assert_eq!(metric.abc.conditions_sum(), 3);
8626 insta::assert_json_snapshot!(metric.abc);
8627 },
8628 );
8629 }
8630
8631 #[test]
8632 fn typescript_number_truthy_condition_counts() {
8633 // Regression for #772: TS shares the JS truthy semantics. The
8634 // numeric *literal* `5` (kind `Number`) counts; the type-keyword
8635 // `number` (kind `Number2`, the `predefined_type`) must not —
8636 // see `typescript_bool_terminal_kinds!`.
8637 check_metrics::<TypescriptParser>(
8638 "function f(x: number) { while (5) {} return x && 5; }",
8639 "foo.ts",
8640 |metric| {
8641 // `while (5)` → +1; `x && 5` → `x` (+1) + `5` (+1).
8642 // Total: 3. The `: number` annotation contributes 0.
8643 assert_eq!(metric.abc.conditions_sum(), 3);
8644 insta::assert_json_snapshot!(metric.abc);
8645 },
8646 );
8647 }
8648
8649 #[test]
8650 fn javascript_nullish_coalescing_counts_condition() {
8651 // `a ?? b` is one nullish-coalescing operator → C = 1.
8652 check_metrics::<JavascriptParser>(
8653 "function f(a, b) { return a ?? b; }",
8654 "foo.js",
8655 |metric| {
8656 assert_eq!(metric.abc.conditions_sum(), 1);
8657 insta::assert_json_snapshot!(metric.abc);
8658 },
8659 );
8660 }
8661
8662 #[test]
8663 fn javascript_else_ternary_case_default_try_catch() {
8664 // `else`, `?` (ternary), `case`, `try`, `catch` all count.
8665 // `default` is the unconditional fallthrough → +0 (#469).
8666 // With the comparisons:
8667 // - `a > 0` → 1
8668 // - `else` opens an else_clause → 1
8669 // - `?` ternary → 1
8670 // - the ternary's bare-identifier condition `a` → 1 (#1102)
8671 // - `case 1` → 1
8672 // - `default` → 0 (fallthrough, #469)
8673 // - `try` + `catch` → 2
8674 // Total C = 7.
8675 check_metrics::<JavascriptParser>(
8676 "function f(a) { if (a > 0) {} else {} let x = a ? 1 : 2; switch (x) { case 1: break; default: break; } try { } catch (e) { } }",
8677 "foo.js",
8678 |metric| {
8679 assert_eq!(metric.abc.conditions_sum(), 7);
8680 insta::assert_json_snapshot!(metric.abc);
8681 },
8682 );
8683 }
8684
8685 // Issue #1102, JS-family half. See
8686 // `cpp_ternary_operand_slots_count_as_unary_conditions` for the
8687 // rule; the two families were behind Java by the same three units.
8688 #[test]
8689 fn javascript_ternary_operand_slots_count_as_unary_conditions() {
8690 // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4.
8691 check_metrics::<JavascriptParser>(
8692 "function f() { x = a ? !b : !c; }",
8693 "foo.js",
8694 |metric| assert_eq!(metric.abc.conditions_sum(), 4),
8695 );
8696 // No-double-count pin: `?` (1) + `>` (1) = 2, unchanged by the
8697 // fix — the parenthesised condition unwraps to a
8698 // `binary_expression` (not a boolean terminal) and neither
8699 // branch is negated.
8700 check_metrics::<JavascriptParser>(
8701 "function f() { x = (a > 0) ? b : -b; }",
8702 "foo.js",
8703 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
8704 );
8705 // Nested: two `?` tokens plus the two bare-identifier
8706 // conditions = 4.
8707 check_metrics::<JavascriptParser>(
8708 "function f() { x = a ? b ? c : d : e; }",
8709 "foo.js",
8710 |metric| assert_eq!(metric.abc.conditions_sum(), 4),
8711 );
8712 // A negated condition is the only input reaching the walker's
8713 // `else` fallback — see the C++ sibling for why. `?` (1) +
8714 // `!a` (1) = 2.
8715 check_metrics::<JavascriptParser>("function f() { x = !a ? b : c; }", "foo.js", |metric| {
8716 assert_eq!(metric.abc.conditions_sum(), 2);
8717 });
8718 }
8719
8720 // TypeScript expands the same `ts_abc_compute!` arm from a separate
8721 // macro body than JavaScript's `js_abc_compute!`, so wiring one and
8722 // not the other is a live failure mode; TSX and Mozjs are clones of
8723 // these two.
8724 #[test]
8725 fn typescript_ternary_operand_slots_count_as_unary_conditions() {
8726 check_metrics::<TypescriptParser>(
8727 "function f() { x = a ? !b : !c; }",
8728 "foo.ts",
8729 |metric| assert_eq!(metric.abc.conditions_sum(), 4),
8730 );
8731 check_metrics::<TypescriptParser>(
8732 "function f() { x = (a > 0) ? b : -b; }",
8733 "foo.ts",
8734 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
8735 );
8736 }
8737
8738 // Issue #1276, JS-family half. See
8739 // `cpp_for_condition_slot_counts_unary_conditions` for the rule.
8740 // The JS grammar marks the `condition` field on both the expression
8741 // and the `;` closing it, so `child_by_field_name` is the only
8742 // addressing that lands on the expression for every header shape.
8743 #[test]
8744 fn javascript_for_condition_slot_counts_unary_conditions() {
8745 // Bare identifier: no operator token anywhere in the header.
8746 check_metrics::<JavascriptParser>("function f(a) { for (; a; ) {} }", "foo.js", |metric| {
8747 assert_eq!(metric.abc.conditions_sum(), 1);
8748 });
8749 // Negation, through the `!` unwrap.
8750 check_metrics::<JavascriptParser>(
8751 "function f(a) { for (; !a; ) {} }",
8752 "foo.js",
8753 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
8754 );
8755 // Parentheses: counts only via the `ForStatement`
8756 // boolean-context seed #1276 found dead.
8757 check_metrics::<JavascriptParser>(
8758 "function f(a) { for (; (a); ) {} }",
8759 "foo.js",
8760 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
8761 );
8762 // No-double-count pin: the `<` arm already counted this shape.
8763 // `let i = 0` and `i++` are the two assignments, which also
8764 // confirms the header parsed as the three-clause form.
8765 check_metrics::<JavascriptParser>(
8766 "function f(n) { for (let i = 0; i < n; i++) {} }",
8767 "foo.js",
8768 |metric| {
8769 assert_eq!(metric.abc.conditions_sum(), 1);
8770 assert_eq!(metric.abc.assignments_sum(), 2);
8771 },
8772 );
8773 // Empty condition: the slot holds an `empty_statement`, which
8774 // is neither a terminal nor a wrapper, so it counts nothing.
8775 check_metrics::<JavascriptParser>(
8776 "function f() { for (;;) { break; } }",
8777 "foo.js",
8778 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
8779 );
8780 }
8781
8782 // TypeScript expands the `ForStatement` arm from `ts_abc_compute!`,
8783 // a separate macro body from JavaScript's `js_abc_compute!`, so
8784 // wiring one and not the other is a live failure mode; TSX and
8785 // Mozjs are the clones of those two. The expected values are
8786 // derived from the JavaScript run rather than hardcoded.
8787 #[test]
8788 fn js_family_for_condition_slot_agrees_with_javascript() {
8789 const BARE: &str = "function f(a) { for (; a; ) {} }\n";
8790 const EMPTY: &str = "function f() { for (;;) { break; } }\n";
8791 let conditions = abc_conditions;
8792
8793 let bare = conditions(LANG::Javascript, BARE);
8794 // Non-degenerate: a zeroed reference makes the comparisons
8795 // below vacuous.
8796 assert_eq!(bare, 1, "JavaScript reference value for `for (; a; )`");
8797 assert_eq!(
8798 conditions(LANG::Javascript, EMPTY),
8799 0,
8800 "JavaScript `for (;;)`"
8801 );
8802
8803 for lang in [LANG::Mozjs, LANG::Typescript, LANG::Tsx] {
8804 assert_eq!(conditions(lang, BARE), bare, "{lang:?} bare for-condition");
8805 assert_eq!(conditions(lang, EMPTY), 0, "{lang:?} empty for-condition");
8806 }
8807 }
8808
8809 #[test]
8810 fn js_family_ternary_operand_slots_agree_with_javascript() {
8811 // `a ? !b : !c` is the one shape that tells the ternary walker
8812 // from the `for`-header walker: both read the `condition` field
8813 // and share the `(&Node, &mut f64)` signature, so a transposed
8814 // pair in a `ts_abc_compute!` / `js_abc_compute!` invocation
8815 // compiles, passes every condition-slot test, and drops only the
8816 // two branch operands. TypeScript alone pinned those before; the
8817 // other three expansions now do too.
8818 const SRC: &str = "function f(a, b, c) { x = a ? !b : !c; }\n";
8819 let javascript = abc_conditions(LANG::Javascript, SRC);
8820 // expected: 4 — the `?`, the `a` condition slot and both negated
8821 // branch operands; non-degenerate by construction.
8822 assert_eq!(
8823 javascript, 4,
8824 "JavaScript reference value for `a ? !b : !c`"
8825 );
8826 for lang in [LANG::Mozjs, LANG::Typescript, LANG::Tsx] {
8827 assert_eq!(
8828 abc_conditions(lang, SRC),
8829 javascript,
8830 "{lang:?} ternary operand slots"
8831 );
8832 }
8833 }
8834
8835 #[test]
8836 fn javascript_instanceof_counts_condition() {
8837 // `x instanceof Foo` is a binary expression whose operator is
8838 // the `instanceof` keyword token → C = 1.
8839 check_metrics::<JavascriptParser>(
8840 "function f(x) { return x instanceof Foo; }",
8841 "foo.js",
8842 |metric| {
8843 assert_eq!(metric.abc.conditions_sum(), 1);
8844 insta::assert_json_snapshot!(metric.abc);
8845 },
8846 );
8847 }
8848
8849 #[test]
8850 fn javascript_complex_function_abc() {
8851 // Mixed-shape regression. Verified by hand:
8852 // - assignments: `let x = 0` (a `let` initializer counts)
8853 // + `x = 5`, `x += 2`, `x++`, `x = (a>b)?a:b`, `x = b`,
8854 // `let p = ...` (likewise) → A = 7.
8855 // - branches: `f(a, b)` self-call + `new Bar()` → B = 2.
8856 // - conditions: `a == b`, `a > 0` → 2 inside the if header
8857 // (`&&` is not counted directly). `else` (1) + `a > b`,
8858 // `?` → 2 in the ternary. `a < b` → 1 in the else-if.
8859 // `!x` → 1 from the Fitzpatrick Rule 9 walker on `||`
8860 // (issue #403): the wrapped Identifier counts once.
8861 // `case 1` → 1 in the switch; `default` → 0 (fallthrough,
8862 // #469). Total C = 8.
8863 check_metrics::<JavascriptParser>(
8864 "function f(a, b) {\n\
8865 let x = 0;\n\
8866 x = 5;\n\
8867 x += 2;\n\
8868 x++;\n\
8869 if (a == b && a > 0) {\n\
8870 x = (a > b) ? a : b;\n\
8871 } else if (a < b || !x) {\n\
8872 x = b;\n\
8873 }\n\
8874 switch (x) {\n\
8875 case 1: break;\n\
8876 default: break;\n\
8877 }\n\
8878 let p = new Bar();\n\
8879 return f(a, b);\n\
8880 }\n",
8881 "foo.js",
8882 |metric| {
8883 assert_eq!(metric.abc.assignments_sum(), 7);
8884 assert_eq!(metric.abc.branches_sum(), 2);
8885 assert_eq!(metric.abc.conditions_sum(), 8);
8886 insta::assert_json_snapshot!(metric.abc);
8887 },
8888 );
8889 }
8890
8891 #[test]
8892 fn mozjs_asi_const_does_not_suppress_later_assignments() {
8893 // Mozjs half of the #1277 cluster; the fork carries its own
8894 // kind-id numbering, so it needs its own fixture. See
8895 // `typescript_asi_const_does_not_suppress_later_assignments`.
8896 check_metrics::<MozjsParser>(
8897 "function f() {
8898 const a = 1
8899 x = 2
8900 y = 3
8901 }",
8902 "foo.jsm",
8903 |metric| {
8904 assert_eq!(metric.abc.assignments_sum(), 2);
8905 },
8906 );
8907 }
8908
8909 #[test]
8910 fn mozjs_const_declarator_shapes_stay_suppressed() {
8911 // Mozjs half of
8912 // `javascript_const_declarator_shapes_stay_suppressed`.
8913 check_metrics::<MozjsParser>(
8914 "function f(o, xs) {
8915 const a = 1, b = 2
8916 const {c = 5, d: {e = 6} = {}} = o
8917 const [g = 7, ...[h = 8]] = xs
8918 let i = 3
8919 var j = 4
8920 for (const x of xs) { k(x) }
8921 }",
8922 "foo.jsm",
8923 |metric| {
8924 assert_eq!(metric.abc.assignments_sum(), 2);
8925 },
8926 );
8927 }
8928
8929 #[test]
8930 fn mozjs_complex_function_abc() {
8931 // Mozjs shares JavaScript's expression / statement vocabulary;
8932 // the `js_abc_compute!` macro expands identical token-level
8933 // rules for both. This test pins parity against the JS impl.
8934 check_metrics::<MozjsParser>(
8935 "function f(a, b) {\n\
8936 let x = 0;\n\
8937 x = 5;\n\
8938 x += 2;\n\
8939 x++;\n\
8940 if (a == b && a > 0) {\n\
8941 x = (a > b) ? a : b;\n\
8942 } else if (a < b || !x) {\n\
8943 x = b;\n\
8944 }\n\
8945 switch (x) {\n\
8946 case 1: break;\n\
8947 default: break;\n\
8948 }\n\
8949 let p = new Bar();\n\
8950 return f(a, b);\n\
8951 }\n",
8952 "foo.js",
8953 |metric| {
8954 assert_eq!(metric.abc.assignments_sum(), 7);
8955 assert_eq!(metric.abc.branches_sum(), 2);
8956 assert_eq!(metric.abc.conditions_sum(), 8);
8957 insta::assert_json_snapshot!(metric.abc);
8958 },
8959 );
8960 }
8961
8962 // ----- JS / TS / Tsx / Mozjs Phase-2B condition slots -----
8963
8964 #[test]
8965 fn javascript_await_expression_condition_counts() {
8966 // Regression for findings.md round-2 #2 (JS):
8967 // `if (await ready()) {}` parses with `await_expression` as
8968 // the condition node inside the `parenthesized_expression`.
8969 // `javascript_inspect_container` unwraps the paren but the
8970 // await child was not in the terminal-bool set, so the
8971 // walker broke without counting. Mirrors C# (lesson 19).
8972 check_metrics::<JavascriptParser>(
8973 "async function ready() { return true; }\n\
8974 async function f() { if (await ready()) { } }\n",
8975 "foo.js",
8976 |metric| {
8977 assert_eq!(metric.abc.branches_sum(), 1);
8978 assert_eq!(metric.abc.conditions_sum(), 1);
8979 insta::assert_json_snapshot!(metric.abc);
8980 },
8981 );
8982 }
8983
8984 #[test]
8985 fn javascript_member_expression_condition_counts() {
8986 // Regression for findings.md #3 (JS-family): tree-sitter-
8987 // javascript emits `member_expression` under three kind_ids
8988 // (191 primary, 208, 228 — `MemberExpression2/3`) depending
8989 // on the production rule path. The verifier in this audit
8990 // confirmed runtime kind for `o.x` is 208. Pre-fix the
8991 // shared `js_family_bool_terminal_kinds!()` macro listed
8992 // only the primary, so every `if (o.x) {}` / `o.x && o.y`
8993 // condition silently reported zero. The per-language macro
8994 // now includes all three aliases (lesson #2).
8995 check_metrics::<JavascriptParser>(
8996 "function f(o) {\n\
8997 \x20 if (o.x) {} // +1c\n\
8998 \x20 return o.x && o.y; // +2c (walker on &&)\n\
8999 }\n",
9000 "foo.js",
9001 |metric| {
9002 assert_eq!(metric.abc.conditions_sum(), 3);
9003 insta::assert_json_snapshot!(metric.abc);
9004 },
9005 );
9006 }
9007
9008 #[test]
9009 fn javascript_if_boolean_literal_condition() {
9010 check_metrics::<JavascriptParser>(
9011 "function f() {\n\
9012 \x20 if (true) {} // +1c\n\
9013 \x20 if (!false) {} // +1c\n\
9014 \x20 while (true) {} // +1c\n\
9015 \x20 do {} while (false); // +1c\n\
9016 }\n",
9017 "foo.js",
9018 |metric| {
9019 assert_eq!(metric.abc.conditions_sum(), 4);
9020 insta::assert_json_snapshot!(metric.abc);
9021 },
9022 );
9023 }
9024
9025 #[test]
9026 fn javascript_methods_arguments_with_conditions() {
9027 check_metrics::<JavascriptParser>(
9028 "function f(a, b) {\n\
9029 \x20 m(a, b); // +1b\n\
9030 \x20 m(!a, !b); // +1b +2c\n\
9031 }\n",
9032 "foo.js",
9033 |metric| {
9034 assert_eq!(metric.abc.branches_sum(), 2);
9035 assert_eq!(metric.abc.conditions_sum(), 2);
9036 insta::assert_json_snapshot!(metric.abc);
9037 },
9038 );
9039 }
9040
9041 #[test]
9042 fn javascript_return_with_conditions() {
9043 check_metrics::<JavascriptParser>(
9044 "function m1(z) { return !(z >= 0); }\n\
9045 function m2(x) { return (((!x))); }\n\
9046 function m3(x, y) { return x && y; }\n",
9047 "foo.js",
9048 |metric| {
9049 // m1: 1 (`>=`). m2: 1 (walker unwraps to `x`).
9050 // m3: 2 (`&&` walker counts both terminals).
9051 assert_eq!(metric.abc.conditions_sum(), 4);
9052 insta::assert_json_snapshot!(metric.abc);
9053 },
9054 );
9055 }
9056
9057 #[test]
9058 fn typescript_if_boolean_literal_condition() {
9059 check_metrics::<TypescriptParser>(
9060 "function f() {\n\
9061 \x20 if (true) {}\n\
9062 \x20 if (!false) {}\n\
9063 \x20 while (true) {}\n\
9064 \x20 do {} while (false);\n\
9065 }\n",
9066 "foo.ts",
9067 |metric| {
9068 assert_eq!(metric.abc.conditions_sum(), 4);
9069 insta::assert_json_snapshot!(metric.abc);
9070 },
9071 );
9072 }
9073
9074 #[test]
9075 fn typescript_methods_arguments_with_conditions() {
9076 check_metrics::<TypescriptParser>(
9077 "function f(a: boolean, b: boolean) {\n\
9078 \x20 m(a, b);\n\
9079 \x20 m(!a, !b);\n\
9080 }\n",
9081 "foo.ts",
9082 |metric| {
9083 assert_eq!(metric.abc.branches_sum(), 2);
9084 assert_eq!(metric.abc.conditions_sum(), 2);
9085 insta::assert_json_snapshot!(metric.abc);
9086 },
9087 );
9088 }
9089
9090 #[test]
9091 fn typescript_return_with_conditions() {
9092 check_metrics::<TypescriptParser>(
9093 "function m1(z: number): boolean { return !(z >= 0); }\n\
9094 function m2(x: boolean): boolean { return (((!x))); }\n\
9095 function m3(x: boolean, y: boolean): boolean { return x && y; }\n",
9096 "foo.ts",
9097 |metric| {
9098 assert_eq!(metric.abc.conditions_sum(), 4);
9099 insta::assert_json_snapshot!(metric.abc);
9100 },
9101 );
9102 }
9103
9104 #[test]
9105 fn tsx_if_boolean_literal_condition() {
9106 check_metrics::<TsxParser>(
9107 "function f() {\n\
9108 \x20 if (true) {}\n\
9109 \x20 if (!false) {}\n\
9110 \x20 while (true) {}\n\
9111 \x20 do {} while (false);\n\
9112 }\n",
9113 "foo.tsx",
9114 |metric| {
9115 assert_eq!(metric.abc.conditions_sum(), 4);
9116 insta::assert_json_snapshot!(metric.abc);
9117 },
9118 );
9119 }
9120
9121 #[test]
9122 fn tsx_methods_arguments_with_conditions() {
9123 check_metrics::<TsxParser>(
9124 "function f(a: boolean, b: boolean) {\n\
9125 \x20 m(a, b);\n\
9126 \x20 m(!a, !b);\n\
9127 }\n",
9128 "foo.tsx",
9129 |metric| {
9130 assert_eq!(metric.abc.branches_sum(), 2);
9131 assert_eq!(metric.abc.conditions_sum(), 2);
9132 insta::assert_json_snapshot!(metric.abc);
9133 },
9134 );
9135 }
9136
9137 #[test]
9138 fn tsx_return_with_conditions() {
9139 check_metrics::<TsxParser>(
9140 "function m1(z: number): boolean { return !(z >= 0); }\n\
9141 function m2(x: boolean): boolean { return (((!x))); }\n\
9142 function m3(x: boolean, y: boolean): boolean { return x && y; }\n",
9143 "foo.tsx",
9144 |metric| {
9145 assert_eq!(metric.abc.conditions_sum(), 4);
9146 insta::assert_json_snapshot!(metric.abc);
9147 },
9148 );
9149 }
9150
9151 #[test]
9152 fn mozjs_if_boolean_literal_condition() {
9153 check_metrics::<MozjsParser>(
9154 "function f() {\n\
9155 \x20 if (true) {}\n\
9156 \x20 if (!false) {}\n\
9157 \x20 while (true) {}\n\
9158 \x20 do {} while (false);\n\
9159 }\n",
9160 "foo.js",
9161 |metric| {
9162 assert_eq!(metric.abc.conditions_sum(), 4);
9163 insta::assert_json_snapshot!(metric.abc);
9164 },
9165 );
9166 }
9167
9168 #[test]
9169 fn mozjs_methods_arguments_with_conditions() {
9170 check_metrics::<MozjsParser>(
9171 "function f(a, b) {\n\
9172 \x20 m(a, b);\n\
9173 \x20 m(!a, !b);\n\
9174 }\n",
9175 "foo.js",
9176 |metric| {
9177 assert_eq!(metric.abc.branches_sum(), 2);
9178 assert_eq!(metric.abc.conditions_sum(), 2);
9179 insta::assert_json_snapshot!(metric.abc);
9180 },
9181 );
9182 }
9183
9184 #[test]
9185 fn mozjs_return_with_conditions() {
9186 check_metrics::<MozjsParser>(
9187 "function m1(z) { return !(z >= 0); }\n\
9188 function m2(x) { return (((!x))); }\n\
9189 function m3(x, y) { return x && y; }\n",
9190 "foo.js",
9191 |metric| {
9192 assert_eq!(metric.abc.conditions_sum(), 4);
9193 insta::assert_json_snapshot!(metric.abc);
9194 },
9195 );
9196 }
9197
9198 // ----- JS / TS / Tsx / Mozjs unary-conditional walker -----
9199
9200 #[test]
9201 fn javascript_if_multiple_conditions() {
9202 check_metrics::<JavascriptParser>(
9203 "function f(a, b, c, d) {\n\
9204 \x20 if (a || b || c || d) {} // +4c\n\
9205 \x20 if (a && b && c) {} // +3c\n\
9206 \x20 if (!a && !b) {} // +2c\n\
9207 }\n",
9208 "foo.js",
9209 |metric| {
9210 assert_eq!(metric.abc.conditions_sum(), 9);
9211 insta::assert_json_snapshot!(metric.abc);
9212 },
9213 );
9214 }
9215
9216 #[test]
9217 fn javascript_while_and_do_while_conditions() {
9218 check_metrics::<JavascriptParser>(
9219 "function f(a, b) {\n\
9220 \x20 while (a || b) {} // +2c\n\
9221 \x20 do {} while (a && !b); // +2c\n\
9222 }\n",
9223 "foo.js",
9224 |metric| {
9225 assert_eq!(metric.abc.conditions_sum(), 4);
9226 insta::assert_json_snapshot!(metric.abc);
9227 },
9228 );
9229 }
9230
9231 #[test]
9232 fn javascript_short_circuit_with_boolean_literal_operand() {
9233 check_metrics::<JavascriptParser>(
9234 "function f(a) { return a && true; }\n",
9235 "foo.js",
9236 |metric| {
9237 assert_eq!(metric.abc.conditions_sum(), 2);
9238 insta::assert_json_snapshot!(metric.abc);
9239 },
9240 );
9241 }
9242
9243 #[test]
9244 fn typescript_if_multiple_conditions() {
9245 check_metrics::<TypescriptParser>(
9246 "function f(a: boolean, b: boolean, c: boolean, d: boolean) {\n\
9247 \x20 if (a || b || c || d) {} // +4c\n\
9248 \x20 if (a && b && c) {} // +3c\n\
9249 \x20 if (!a && !b) {} // +2c\n\
9250 }\n",
9251 "foo.ts",
9252 |metric| {
9253 assert_eq!(metric.abc.conditions_sum(), 9);
9254 insta::assert_json_snapshot!(metric.abc);
9255 },
9256 );
9257 }
9258
9259 #[test]
9260 fn typescript_while_and_do_while_conditions() {
9261 check_metrics::<TypescriptParser>(
9262 "function f(a: boolean, b: boolean) {\n\
9263 \x20 while (a || b) {} // +2c\n\
9264 \x20 do {} while (a && !b); // +2c\n\
9265 }\n",
9266 "foo.ts",
9267 |metric| {
9268 assert_eq!(metric.abc.conditions_sum(), 4);
9269 insta::assert_json_snapshot!(metric.abc);
9270 },
9271 );
9272 }
9273
9274 #[test]
9275 fn typescript_short_circuit_with_boolean_literal_operand() {
9276 check_metrics::<TypescriptParser>(
9277 "function f(a: boolean): boolean { return a && true; }\n",
9278 "foo.ts",
9279 |metric| {
9280 assert_eq!(metric.abc.conditions_sum(), 2);
9281 insta::assert_json_snapshot!(metric.abc);
9282 },
9283 );
9284 }
9285
9286 #[test]
9287 fn tsx_if_multiple_conditions() {
9288 check_metrics::<TsxParser>(
9289 "function f(a: boolean, b: boolean, c: boolean, d: boolean) {\n\
9290 \x20 if (a || b || c || d) {} // +4c\n\
9291 \x20 if (a && b && c) {} // +3c\n\
9292 \x20 if (!a && !b) {} // +2c\n\
9293 }\n",
9294 "foo.tsx",
9295 |metric| {
9296 assert_eq!(metric.abc.conditions_sum(), 9);
9297 insta::assert_json_snapshot!(metric.abc);
9298 },
9299 );
9300 }
9301
9302 #[test]
9303 fn tsx_while_and_do_while_conditions() {
9304 check_metrics::<TsxParser>(
9305 "function f(a: boolean, b: boolean) {\n\
9306 \x20 while (a || b) {} // +2c\n\
9307 \x20 do {} while (a && !b); // +2c\n\
9308 }\n",
9309 "foo.tsx",
9310 |metric| {
9311 assert_eq!(metric.abc.conditions_sum(), 4);
9312 insta::assert_json_snapshot!(metric.abc);
9313 },
9314 );
9315 }
9316
9317 #[test]
9318 fn tsx_short_circuit_with_boolean_literal_operand() {
9319 check_metrics::<TsxParser>(
9320 "function f(a: boolean): boolean { return a && true; }\n",
9321 "foo.tsx",
9322 |metric| {
9323 assert_eq!(metric.abc.conditions_sum(), 2);
9324 insta::assert_json_snapshot!(metric.abc);
9325 },
9326 );
9327 }
9328
9329 #[test]
9330 fn mozjs_if_multiple_conditions() {
9331 check_metrics::<MozjsParser>(
9332 "function f(a, b, c, d) {\n\
9333 \x20 if (a || b || c || d) {} // +4c\n\
9334 \x20 if (a && b && c) {} // +3c\n\
9335 \x20 if (!a && !b) {} // +2c\n\
9336 }\n",
9337 "foo.js",
9338 |metric| {
9339 assert_eq!(metric.abc.conditions_sum(), 9);
9340 insta::assert_json_snapshot!(metric.abc);
9341 },
9342 );
9343 }
9344
9345 #[test]
9346 fn mozjs_while_and_do_while_conditions() {
9347 check_metrics::<MozjsParser>(
9348 "function f(a, b) {\n\
9349 \x20 while (a || b) {} // +2c\n\
9350 \x20 do {} while (a && !b); // +2c\n\
9351 }\n",
9352 "foo.js",
9353 |metric| {
9354 assert_eq!(metric.abc.conditions_sum(), 4);
9355 insta::assert_json_snapshot!(metric.abc);
9356 },
9357 );
9358 }
9359
9360 #[test]
9361 fn mozjs_short_circuit_with_boolean_literal_operand() {
9362 check_metrics::<MozjsParser>(
9363 "function f(a) { return a && true; }\n",
9364 "foo.js",
9365 |metric| {
9366 assert_eq!(metric.abc.conditions_sum(), 2);
9367 insta::assert_json_snapshot!(metric.abc);
9368 },
9369 );
9370 }
9371
9372 // ---------- Perl ABC tests ----------
9373
9374 #[test]
9375 fn perl_empty_unit_zero() {
9376 // Empty source produces zero ABC magnitude — pins the trait
9377 // wiring without exercising any compute branch.
9378 check_metrics::<PerlParser>("", "empty.pl", |metric| {
9379 assert_eq!(metric.abc.assignments_sum(), 0);
9380 assert_eq!(metric.abc.branches_sum(), 0);
9381 assert_eq!(metric.abc.conditions_sum(), 0);
9382 insta::assert_json_snapshot!(metric.abc);
9383 });
9384 }
9385
9386 #[test]
9387 fn perl_plain_and_compound_assignments_count() {
9388 // `my $x = 0` parses as a `binary_expression` with an `=`
9389 // token, so the initialiser counts (Perl has no equivalent of
9390 // the JS `const` initialiser-suppression rule). Each
9391 // assignment operator token contributes one assignment:
9392 // `=`, `=`, `+=`, `.=`, `**=` → A = 5. Two of those `=` come
9393 // from the `my $x = 0` initialiser and the later `$x = 5`
9394 // reassignment.
9395 check_metrics::<PerlParser>(
9396 "sub f { my $x = 0; $x = 5; $x += 2; $x .= \"a\"; $x **= 3; }",
9397 "foo.pl",
9398 |metric| {
9399 assert_eq!(metric.abc.assignments_sum(), 5);
9400 assert_eq!(metric.abc.branches_sum(), 0);
9401 assert_eq!(metric.abc.conditions_sum(), 0);
9402 insta::assert_json_snapshot!(metric.abc);
9403 },
9404 );
9405 }
9406
9407 #[test]
9408 fn perl_calls_are_branches() {
9409 // `foo()` parses as `call_expression_with_args_with_brackets`
9410 // wrapping an inner `call_expression_with_bareword(foo)`;
9411 // `bar 1, 2` wraps `bar` likewise under spaced-args; `shift`
9412 // appears as a standalone bareword. The bareword-inside-
9413 // wrapper case must NOT double-count — only the outer wrapper
9414 // contributes a branch. So B = 3 (foo, bar, shift), not 5.
9415 check_metrics::<PerlParser>(
9416 "sub f { foo(); bar 1, 2; my $a = shift; }",
9417 "foo.pl",
9418 |metric| {
9419 // shift's `my $a = shift` initialiser contributes one
9420 // assignment via the `=` token.
9421 assert_eq!(metric.abc.assignments_sum(), 1);
9422 assert_eq!(metric.abc.branches_sum(), 3);
9423 assert_eq!(metric.abc.conditions_sum(), 0);
9424 insta::assert_json_snapshot!(metric.abc);
9425 },
9426 );
9427 }
9428
9429 #[test]
9430 fn perl_method_invocation_counts_as_branch() {
9431 // `$obj->method(...)` parses as `method_invocation`. Any
9432 // arrow-dispatch counts as one branch regardless of how the
9433 // arguments are passed.
9434 check_metrics::<PerlParser>(
9435 "sub f { my $obj = shift; $obj->run($x); $obj->ping; }",
9436 "foo.pl",
9437 |metric| {
9438 // `my $obj = shift` → A=1, B=1 (shift bareword).
9439 // `$obj->run($x)` and `$obj->ping` → 2 more branches.
9440 assert_eq!(metric.abc.assignments_sum(), 1);
9441 assert_eq!(metric.abc.branches_sum(), 3);
9442 assert_eq!(metric.abc.conditions_sum(), 0);
9443 insta::assert_json_snapshot!(metric.abc);
9444 },
9445 );
9446 }
9447
9448 #[test]
9449 fn perl_numeric_and_string_comparisons_count_conditions() {
9450 // Numeric ops `==`, `!=`, `<`, `>`, `<=`, `>=`, `<=>` and
9451 // string ops `eq`, `ne`, `lt`, `gt`, `le`, `ge`, `cmp` each
9452 // fire once per token. The sample below uses one of each →
9453 // C = 14. No assignments, no branches.
9454 check_metrics::<PerlParser>(
9455 "sub f {\n\
9456 my $r;\n\
9457 $r = $a == $b;\n\
9458 $r = $a != $b;\n\
9459 $r = $a < $b;\n\
9460 $r = $a > $b;\n\
9461 $r = $a <= $b;\n\
9462 $r = $a >= $b;\n\
9463 $r = $a <=> $b;\n\
9464 $r = $a eq $b;\n\
9465 $r = $a ne $b;\n\
9466 $r = $a lt $b;\n\
9467 $r = $a gt $b;\n\
9468 $r = $a le $b;\n\
9469 $r = $a ge $b;\n\
9470 $r = $a cmp $b;\n\
9471 }",
9472 "foo.pl",
9473 |metric| {
9474 // 15 `=` tokens: one declaration `my $r` (no `=`),
9475 // then 14 `$r = …` plus there's no `=` in `my $r;`.
9476 // Actually: `my $r;` has no `=`; the 14 `$r = …` are
9477 // 14 `=` tokens. So A=14, C=14.
9478 assert_eq!(metric.abc.assignments_sum(), 14);
9479 assert_eq!(metric.abc.branches_sum(), 0);
9480 assert_eq!(metric.abc.conditions_sum(), 14);
9481 insta::assert_json_snapshot!(metric.abc);
9482 },
9483 );
9484 }
9485
9486 #[test]
9487 fn perl_short_circuit_not_counted_directly_ternary_counts() {
9488 // `&&`, `||`, `//`, low-precedence `and`, `or`, `xor` are
9489 // NOT counted as conditions on their own (Fitzpatrick Rule
9490 // 5; #395) — instead each operand is counted as a unary
9491 // conditional by the walker (Rule 9; #403). At the pinned
9492 // tree-sitter-perl grammar version, only the four
9493 // punctuation forms plus one keyword form parse under a
9494 // `binary_expression` parent that triggers the walker; the
9495 // other two keyword forms parse under a distinct grammar
9496 // node and contribute zero. Net: 4 walker-firing lines × 2
9497 // scalar-variable operands + 1 ternary node + 1 for the
9498 // ternary's bare `$a` condition operand (#1102) = 10. The
9499 // exact mix of "which two keyword forms are silent" is
9500 // grammar-version-dependent; a future grammar bump that
9501 // normalises the keyword forms' parent kind will shift this
9502 // count to 14. See follow-up note above the test name.
9503 check_metrics::<PerlParser>(
9504 "sub f {\n\
9505 my $r;\n\
9506 $r = $a && $b;\n\
9507 $r = $a || $b;\n\
9508 $r = $a // $b;\n\
9509 $r = $a and $b;\n\
9510 $r = $a or $b;\n\
9511 $r = $a xor $b;\n\
9512 $r = $a ? 1 : 2;\n\
9513 }",
9514 "foo.pl",
9515 |metric| {
9516 // 7 `=` tokens (one per reassignment line).
9517 assert_eq!(metric.abc.assignments_sum(), 7);
9518 assert_eq!(metric.abc.branches_sum(), 0);
9519 // 4 walker-triggered lines × 2 operands + 1 ternary
9520 // node + 1 for its bare `$a` condition operand = 10.
9521 // The two remaining low-precedence keyword forms (one
9522 // of `and`/`or`/`xor`) fall under a
9523 // non-binary_expression parent in this grammar
9524 // version and contribute zero via the walker.
9525 assert_eq!(metric.abc.conditions_sum(), 10);
9526 insta::assert_json_snapshot!(metric.abc);
9527 },
9528 );
9529 }
9530
9531 // Issue #1102, Perl half. See
9532 // `cpp_ternary_operand_slots_count_as_unary_conditions` for the
9533 // rule. Like PHP, Perl's ABC dispatcher has no `?`-token arm — the
9534 // grammar does emit the token, but the `ternary_expression` node is
9535 // what carries the tally's +1. tree-sitter-perl names the branch
9536 // fields `true` / `false` rather than the C-family `consequence` /
9537 // `alternative`, so a copied C-family gate would match nothing.
9538 #[test]
9539 fn perl_ternary_operand_slots_count_as_unary_conditions() {
9540 // ternary (1) + condition `$a` (1) + `!$b` (1) + `!$c` (1) = 4.
9541 check_metrics::<PerlParser>("sub f { my $x = $a ? !$b : !$c; }", "foo.pl", |metric| {
9542 assert_eq!(metric.abc.conditions_sum(), 4);
9543 });
9544 // No-double-count pin: ternary (1) + `>` (1) = 2, unchanged by
9545 // the fix.
9546 check_metrics::<PerlParser>(
9547 "sub f { my $x = ($a > 0) ? $b : -$b; }",
9548 "foo.pl",
9549 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
9550 );
9551 // A negated *condition* takes the walker's `else` fallback —
9552 // `!$a` is neither a boolean terminal nor a paren wrapper, so
9553 // only `perl_inspect_container` can classify it. Delete the
9554 // fallback and this reads 1. ternary (1) + `!$a` (1) = 2.
9555 check_metrics::<PerlParser>("sub f { my $x = !$a ? $b : $c; }", "foo.pl", |metric| {
9556 assert_eq!(metric.abc.conditions_sum(), 2);
9557 });
9558 // Nested: two ternary nodes plus the two bare-variable
9559 // conditions = 4.
9560 check_metrics::<PerlParser>(
9561 "sub f { my $x = $a ? ($b ? $c : $d) : $e; }",
9562 "foo.pl",
9563 |metric| assert_eq!(metric.abc.conditions_sum(), 4),
9564 );
9565 }
9566
9567 #[test]
9568 fn perl_elsif_and_else_count_conditions() {
9569 // `if (… == …) { … } elsif (… < …) { … } else { … }` →
9570 // 2 comparison tokens (`==`, `<`), plus `elsif_clause` and
9571 // `else_clause` each + 1 → C = 4. Branches: 0 (only
9572 // assignments). Assignments: just the `=` initialisers /
9573 // reassignments — there are 4 here (`$x` init plus three
9574 // `$x = …` reassigns).
9575 check_metrics::<PerlParser>(
9576 "sub f {\n\
9577 my $x = 0;\n\
9578 if ($a == $b) {\n\
9579 $x = 1;\n\
9580 } elsif ($a < $b) {\n\
9581 $x = 2;\n\
9582 } else {\n\
9583 $x = 3;\n\
9584 }\n\
9585 }",
9586 "foo.pl",
9587 |metric| {
9588 assert_eq!(metric.abc.assignments_sum(), 4);
9589 assert_eq!(metric.abc.branches_sum(), 0);
9590 assert_eq!(metric.abc.conditions_sum(), 4);
9591 insta::assert_json_snapshot!(metric.abc);
9592 },
9593 );
9594 }
9595
9596 #[test]
9597 fn perl_regex_match_operators_count_conditions() {
9598 // `=~` and `!~` are pattern-match operators; we count both
9599 // as conditions because they evaluate the regex match in a
9600 // boolean context.
9601 check_metrics::<PerlParser>(
9602 "sub f { my $s = shift; my $m = $s =~ /foo/; my $n = $s !~ /bar/; }",
9603 "foo.pl",
9604 |metric| {
9605 // 3 `=` tokens, 0 branches except `shift` bareword.
9606 assert_eq!(metric.abc.assignments_sum(), 3);
9607 assert_eq!(metric.abc.branches_sum(), 1);
9608 assert_eq!(metric.abc.conditions_sum(), 2);
9609 insta::assert_json_snapshot!(metric.abc);
9610 },
9611 );
9612 }
9613
9614 #[test]
9615 fn perl_complex_function_abc() {
9616 // Mixed program exercising every category. Computed
9617 // expected:
9618 // Assignments: `my $i = 0` (1), `$i++` is a unary
9619 // increment — Perl's grammar emits `PLUSPLUS` not an `=`
9620 // operator, so it does NOT count under the operator-
9621 // token rule. The for-loop's `$i++` is similarly
9622 // uncounted.
9623 // Total A: 1 from `my $i = 0`, 1 from `$total += $i`
9624 // (the `+=` token) → A = 2.
9625 // Branches: `do_work($i)` → 1; `print "done\n"` is a
9626 // call_expression_with_spaced_args → 1; `return $total`
9627 // uses the `return` keyword not a call → 0. B = 2.
9628 // Conditions: `$i < 10` (`<`) → 1; `$i % 2 == 0` (`==`) →
9629 // 1; `else_clause` → 1. C = 3.
9630 check_metrics::<PerlParser>(
9631 "sub run {\n\
9632 my $total = 0;\n\
9633 for (my $i = 0; $i < 10; $i++) {\n\
9634 if ($i % 2 == 0) {\n\
9635 do_work($i);\n\
9636 } else {\n\
9637 $total += $i;\n\
9638 }\n\
9639 }\n\
9640 print \"done\\n\";\n\
9641 return $total;\n\
9642 }",
9643 "foo.pl",
9644 |metric| {
9645 // `my $total = 0` is one `=`; `my $i = 0` is another
9646 // `=`; `$total += $i` is one `+=`. Total = 3.
9647 assert_eq!(metric.abc.assignments_sum(), 3);
9648 assert_eq!(metric.abc.branches_sum(), 2);
9649 assert_eq!(metric.abc.conditions_sum(), 3);
9650 insta::assert_json_snapshot!(metric.abc);
9651 },
9652 );
9653 }
9654
9655 #[test]
9656 fn perl_if_multiple_conditions() {
9657 // Fitzpatrick Rule 9 walker (issue #403): each operand of a
9658 // `&&` / `||` / `//` / `and` / `or` / `xor` chain is one
9659 // condition. ScalarVariable operands ($a, $b, …) qualify as
9660 // terminal-bool kinds for the walker.
9661 check_metrics::<PerlParser>(
9662 "sub f {\n\
9663 my ($a, $b, $c, $d) = @_;\n\
9664 if ($a || $b || $c || $d) { return 1; } # +4c\n\
9665 if ($a && $b && $c) { return 2; } # +3c\n\
9666 if (!$a && !$b) { return 3; } # +2c\n\
9667 return 0;\n\
9668 }",
9669 "foo.pl",
9670 |metric| {
9671 assert_eq!(metric.abc.conditions_sum(), 9);
9672 insta::assert_json_snapshot!(metric.abc);
9673 },
9674 );
9675 }
9676
9677 #[test]
9678 fn perl_while_and_until_conditions() {
9679 // Perl has no `do { ... } while(cond);` shape in this grammar
9680 // — `while` and `until` are the loop forms with a condition
9681 // slot. The walker fires on each `&&` / `||` token inside
9682 // those headers.
9683 check_metrics::<PerlParser>(
9684 "sub f {\n\
9685 my ($a, $b) = @_;\n\
9686 while ($a || $b) { last; } # +2c\n\
9687 until ($a && !$b) { last; } # +2c\n\
9688 }",
9689 "foo.pl",
9690 |metric| {
9691 assert_eq!(metric.abc.conditions_sum(), 4);
9692 insta::assert_json_snapshot!(metric.abc);
9693 },
9694 );
9695 }
9696
9697 #[test]
9698 fn perl_for_header_condition_slot_counts_unary_conditions() {
9699 // The Perl half of #1276. The C-style `for` header's condition
9700 // slot is a Rule 9 unary condition like the `if` slot: a bare
9701 // `$ok`, a negated `!$ok` and a parenthesised `($ok)` each count
9702 // one, an empty header and a `foreach` count zero, and a
9703 // comparison-shaped `$i < $n` stays at the one the `<` arm
9704 // already counts. Every C-style row is the three-clause spelling
9705 // because tree-sitter-perl does not parse an empty initializer.
9706 let cases = [
9707 ("for (my $i = 0; $ok; $i++) { }", 1),
9708 ("for (my $i = 0; !$ok; $i++) { }", 1),
9709 ("for (my $i = 0; ($ok); $i++) { }", 1),
9710 ("for (my $i = 0; $i < $n; $i++) { }", 1),
9711 ("for (my $i = 0; $ok && $j; $i++) { }", 2),
9712 ("for (;;) { last; }", 0),
9713 ("for my $x (@l) { }", 0),
9714 ];
9715 let mut ran = 0;
9716 for (body, expected) in cases {
9717 let src = format!("sub f {{ {body} }}\n");
9718 assert_eq!(abc_conditions(LANG::Perl, &src), expected, "`{body}`");
9719 ran += 1;
9720 }
9721 assert_eq!(ran, cases.len());
9722 assert!(cases.iter().any(|&(_, n)| n == 0));
9723 assert!(cases.iter().any(|&(_, n)| n == 2));
9724
9725 // The slot agrees with the `if` slot for every shape, which is
9726 // the property the fix restores; the reference is non-degenerate
9727 // by the table above.
9728 for condition in ["$ok", "!$ok", "($ok)", "$ok && $j"] {
9729 let in_for = format!("sub f {{ for (my $i = 0; {condition}; $i++) {{ }} }}\n");
9730 let in_if = format!("sub f {{ if ({condition}) {{ }} }}\n");
9731 assert_eq!(
9732 abc_conditions(LANG::Perl, &in_for),
9733 abc_conditions(LANG::Perl, &in_if),
9734 "`{condition}` must score alike in a `for` header and an `if`",
9735 );
9736 }
9737 }
9738
9739 #[test]
9740 fn perl_short_circuit_counts_scalar_variable_operands() {
9741 // `$a && $b` reports 2 conditions — one walker count per
9742 // `ScalarVariable` operand. Renamed from the cross-language
9743 // `_with_boolean_literal_operand` convention because Perl has
9744 // no readily-grammar-exposed boolean literal in an `&&`
9745 // operand slot at the pinned grammar version (the `Boolean`
9746 // kind only fires on the `boolean` pragma's named constants,
9747 // not bareword `1` / `0`). Two scalar variables are the
9748 // grammar-stable terminal-set witness for Perl.
9749 check_metrics::<PerlParser>(
9750 "sub f { my ($a) = @_; return $a && $b; }\n",
9751 "foo.pl",
9752 |metric| {
9753 assert_eq!(metric.abc.conditions_sum(), 2);
9754 insta::assert_json_snapshot!(metric.abc);
9755 },
9756 );
9757 }
9758
9759 #[test]
9760 fn perl_array_in_binary_operand_descends_to_scalar_context_value() {
9761 // Regression test for the code-review findings on the
9762 // Phase-2B Perl walker:
9763 // - Pre-fix-A: `perl_inspect_container` descended `Array`
9764 // via `node.child(1)` — the FIRST element — wrongly
9765 // attributing `$x` for `($x, $y)` (semantically `$y`
9766 // is the scalar-context value).
9767 // - Fix-A (the `array_is_paren` guard, 5db8078): dropped
9768 // Array-as-paren entirely in `BinaryExpression` operand
9769 // contexts to avoid the wrong attribution — but
9770 // regressed `$a || ($x)` (single paren-grouped operand)
9771 // to C=1 instead of 2.
9772 // - Fix-B (this change): keeps Array-as-paren unconditional
9773 // but descends via the LAST named child. `$a || ($x)`
9774 // reaches `$x` (count both operands → 2);
9775 // `$a || ($x, $y)` reaches `$y` (count `$a` + `$y` →
9776 // still 2, matching Fitzpatrick Rule 7 "one per
9777 // operand"); `if ($a)` still reaches `$a` (single-
9778 // element grouping → 1).
9779 check_metrics::<PerlParser>(
9780 "sub f { my ($a, $x, $y) = @_;\n\
9781 \x20 my $r = $a || ($x, $y); # +2c: $a + last-named $y\n\
9782 \x20 my $s = $a || ($x); # +2c: $a + only-named $x\n\
9783 \x20 $r + $s;\n\
9784 }\n",
9785 "foo.pl",
9786 |metric| {
9787 // 2 + 2 = 4 unary conditions from the two `||`s.
9788 assert_eq!(metric.abc.conditions_sum(), 4);
9789 insta::assert_json_snapshot!(metric.abc);
9790 },
9791 );
9792 }
9793
9794 #[test]
9795 fn perl_if_scalar_variable_condition() {
9796 // Renamed from the cross-language
9797 // `_if_boolean_literal_condition` convention because
9798 // Perl has no readily-grammar-exposed boolean literal in
9799 // an `if (cond)` slot at the pinned grammar version:
9800 // tree-sitter-perl's `Boolean` kind only fires for the
9801 // `boolean` pragma's named constants (not bareword `1` /
9802 // `0`, which surface as `Integer` / not in the
9803 // terminal-bool set). A scalar-variable condition is the
9804 // grammar-stable witness — `if ($a)` reaches
9805 // `scalar_variable` via the `Array` paren unwrap.
9806 check_metrics::<PerlParser>(
9807 "sub f { my ($a) = @_; if ($a) { return 1; } }\n",
9808 "foo.pl",
9809 |metric| {
9810 assert_eq!(metric.abc.conditions_sum(), 1);
9811 insta::assert_json_snapshot!(metric.abc);
9812 },
9813 );
9814 }
9815
9816 #[test]
9817 fn perl_methods_arguments_with_conditions() {
9818 // `call(!$a, !$b)` — argument list walker counts each
9819 // unary-conditional argument once. Cannot use `m(...)` as
9820 // the function name — tree-sitter-perl parses `m(...)` as
9821 // the regex-match operator, not a function call.
9822 check_metrics::<PerlParser>(
9823 "sub f { my ($a, $b) = @_; call($a, $b); call(!$a, !$b); }\n",
9824 "foo.pl",
9825 |metric| {
9826 // Two calls × 1 branch each = 2 branches.
9827 // `call(!$a, !$b)` contributes 2 walker conditions
9828 // (one per `!`-wrapped scalar-variable argument);
9829 // `call($a, $b)` contributes 0 (bare-args don't
9830 // count via the Arguments walker — list_kind !=
9831 // BinaryExpression).
9832 assert_eq!(metric.abc.branches_sum(), 2);
9833 assert_eq!(metric.abc.conditions_sum(), 2);
9834 insta::assert_json_snapshot!(metric.abc);
9835 },
9836 );
9837 }
9838
9839 #[test]
9840 fn perl_return_with_conditions() {
9841 // `return !$a` reports 1 condition via the walker (unary
9842 // unwrap to scalar-variable terminal). `return $a` reports
9843 // 0 (no paren / unary wrap, has_boolean_content stays
9844 // false from ReturnExpression parent).
9845 check_metrics::<PerlParser>(
9846 "sub m1 { my ($z) = @_; return !($z); }\n\
9847 sub m2 { my ($x) = @_; return (((!$x))); }\n\
9848 sub m3 { my ($x, $y) = @_; return $x && $y; }\n",
9849 "foo.pl",
9850 |metric| {
9851 // m1: !($z) → walker on `!` unwraps paren to $z (1).
9852 // m2: (((!$x))) → walker unwraps three parens + one
9853 // unary to $x (1).
9854 // m3: $x && $y → walker on `&&` counts both (2).
9855 // Sum: 4.
9856 assert_eq!(metric.abc.conditions_sum(), 4);
9857 insta::assert_json_snapshot!(metric.abc);
9858 },
9859 );
9860 }
9861
9862 // ---------- Lua ABC tests ----------
9863
9864 #[test]
9865 fn lua_empty_unit_zero() {
9866 check_metrics::<LuaParser>("", "empty.lua", |metric| {
9867 assert_eq!(metric.abc.assignments_sum(), 0);
9868 assert_eq!(metric.abc.branches_sum(), 0);
9869 assert_eq!(metric.abc.conditions_sum(), 0);
9870 insta::assert_json_snapshot!(metric.abc);
9871 });
9872 }
9873
9874 #[test]
9875 fn lua_assignments_count_locals_and_plain() {
9876 // `local x = 0` wraps an `assignment_statement` under a
9877 // `variable_declaration`; the inner wrapper still counts.
9878 // Multi-target assignment `a, b = 1, 2` is a single
9879 // `assignment_statement` and contributes 1, NOT 2 — the
9880 // wrapper is the unit of counting (matches the Python rule:
9881 // one `Assignment` node, one assignment).
9882 check_metrics::<LuaParser>(
9883 "function f()\n\
9884 local x = 0\n\
9885 x = 1\n\
9886 local a, b = 1, 2\n\
9887 a, b = b, a\n\
9888 end",
9889 "foo.lua",
9890 |metric| {
9891 assert_eq!(metric.abc.assignments_sum(), 4);
9892 assert_eq!(metric.abc.branches_sum(), 0);
9893 assert_eq!(metric.abc.conditions_sum(), 0);
9894 insta::assert_json_snapshot!(metric.abc);
9895 },
9896 );
9897 }
9898
9899 #[test]
9900 fn lua_calls_are_branches() {
9901 // `print(x)`, `obj.m(x)`, `obj:m(x)`, `f(g(1))` — every
9902 // call form is a `function_call` node. The nested
9903 // `f(g(1))` counts as 2 branches (one per dispatch).
9904 check_metrics::<LuaParser>(
9905 "function r(x)\n\
9906 print(x)\n\
9907 obj.m(x)\n\
9908 obj:m(x)\n\
9909 return f(g(1))\n\
9910 end",
9911 "foo.lua",
9912 |metric| {
9913 assert_eq!(metric.abc.assignments_sum(), 0);
9914 assert_eq!(metric.abc.branches_sum(), 5);
9915 assert_eq!(metric.abc.conditions_sum(), 0);
9916 insta::assert_json_snapshot!(metric.abc);
9917 },
9918 );
9919 }
9920
9921 #[test]
9922 fn lua_comparisons_count_logical_ops_do_not() {
9923 // Each comparison token contributes one condition; `and` /
9924 // `or` are NOT counted on their own (Fitzpatrick Rule 5;
9925 // #395) — instead each operand is counted as a unary
9926 // conditional by the walker (Rule 9; #403). The two
9927 // `a and b` / `a or b` lines add 2 walker conditions each.
9928 check_metrics::<LuaParser>(
9929 "function f(a, b)\n\
9930 local r\n\
9931 r = a == b\n\
9932 r = a ~= b\n\
9933 r = a < b\n\
9934 r = a > b\n\
9935 r = a <= b\n\
9936 r = a >= b\n\
9937 r = a and b\n\
9938 r = a or b\n\
9939 end",
9940 "foo.lua",
9941 |metric| {
9942 // 8 `r = …` reassignments, plus `local r` (no `=`).
9943 assert_eq!(metric.abc.assignments_sum(), 8);
9944 assert_eq!(metric.abc.branches_sum(), 0);
9945 // 6 comparisons (+6) + 2 logical lines × 2 walker
9946 // operands (+4) = 10.
9947 assert_eq!(metric.abc.conditions_sum(), 10);
9948 insta::assert_json_snapshot!(metric.abc);
9949 },
9950 );
9951 }
9952
9953 #[test]
9954 fn lua_elseif_and_else_count_conditions() {
9955 // Each elseif / else arm of the if contributes one
9956 // condition, mirroring the Python rule.
9957 check_metrics::<LuaParser>(
9958 "function f(x)\n\
9959 if x > 0 then\n\
9960 return 1\n\
9961 elseif x < 0 then\n\
9962 return -1\n\
9963 else\n\
9964 return 0\n\
9965 end\n\
9966 end",
9967 "foo.lua",
9968 |metric| {
9969 // Comparisons: `>`, `<` → 2; elseif_statement → 1;
9970 // else_statement → 1. C = 4. No branches (no calls).
9971 assert_eq!(metric.abc.assignments_sum(), 0);
9972 assert_eq!(metric.abc.branches_sum(), 0);
9973 assert_eq!(metric.abc.conditions_sum(), 4);
9974 insta::assert_json_snapshot!(metric.abc);
9975 },
9976 );
9977 }
9978
9979 #[test]
9980 fn lua_complex_function_abc() {
9981 // Combines every category to pin the metric.
9982 check_metrics::<LuaParser>(
9983 "function run(n)\n\
9984 local total = 0\n\
9985 for i = 1, n do\n\
9986 if i % 2 == 0 then\n\
9987 do_work(i)\n\
9988 else\n\
9989 total = total + i\n\
9990 end\n\
9991 end\n\
9992 print(\"done\")\n\
9993 return total\n\
9994 end",
9995 "foo.lua",
9996 |metric| {
9997 // Assignments: `local total = 0` (1), `total = total + i` (1) → 2.
9998 // Branches: `do_work(i)` (1), `print(\"done\")` (1) → 2.
9999 // Conditions: `==` (1), `else_statement` (1) → 2.
10000 assert_eq!(metric.abc.assignments_sum(), 2);
10001 assert_eq!(metric.abc.branches_sum(), 2);
10002 assert_eq!(metric.abc.conditions_sum(), 2);
10003 insta::assert_json_snapshot!(metric.abc);
10004 },
10005 );
10006 }
10007
10008 #[test]
10009 fn lua_if_multiple_conditions() {
10010 // Fitzpatrick Rule 9 walker (issue #403). Lua's `and` / `or`
10011 // are keyword tokens inside a `binary_expression`.
10012 check_metrics::<LuaParser>(
10013 "function f(a, b, c, d)\n\
10014 if a or b or c or d then return 1 end -- +4c\n\
10015 if a and b and c then return 2 end -- +3c\n\
10016 if not a and not b then return 3 end -- +2c\n\
10017 return 0\n\
10018 end",
10019 "foo.lua",
10020 |metric| {
10021 assert_eq!(metric.abc.conditions_sum(), 9);
10022 insta::assert_json_snapshot!(metric.abc);
10023 },
10024 );
10025 }
10026
10027 #[test]
10028 fn lua_while_conditions() {
10029 // Lua has no `do { ... } while(cond);` — `while cond do …
10030 // end` and `repeat … until cond` are the loop forms.
10031 check_metrics::<LuaParser>(
10032 "function f(a, b)\n\
10033 while a or b do break end -- +2c\n\
10034 repeat break until a and not b -- +2c\n\
10035 end",
10036 "foo.lua",
10037 |metric| {
10038 assert_eq!(metric.abc.conditions_sum(), 4);
10039 insta::assert_json_snapshot!(metric.abc);
10040 },
10041 );
10042 }
10043
10044 #[test]
10045 fn lua_short_circuit_with_boolean_literal_operand() {
10046 // `a and true` reports 2 conditions: one Identifier, one
10047 // True keyword literal.
10048 check_metrics::<LuaParser>("function f(a) return a and true end", "foo.lua", |metric| {
10049 assert_eq!(metric.abc.conditions_sum(), 2);
10050 insta::assert_json_snapshot!(metric.abc);
10051 });
10052 }
10053
10054 #[test]
10055 fn lua_number_truthy_condition_counts() {
10056 // Regression for findings.md #2: Lua treats every non-nil,
10057 // non-false value as truthy, so `if 1 then ... end` and
10058 // `return a and 2` should each count their numeric literal
10059 // as a Fitzpatrick Rule 6 / 7 unary condition. Pre-fix,
10060 // `lua_bool_terminal_kinds!()` listed `True` / `False` /
10061 // `Nil` but omitted `Number`, so the walker dropped every
10062 // numeric-truthy operand. The walker comment at the top of
10063 // `lua_inspect_container` already promised numbers were
10064 // terminal-bool kinds; this commit closes the gap.
10065 check_metrics::<LuaParser>(
10066 "function f(a)\n\
10067 \x20 if 1 then return 1 end\n\
10068 \x20 return a and 2\n\
10069 end",
10070 "foo.lua",
10071 |metric| {
10072 // `if 1 then` → walker counts the Number literal (+1).
10073 // `a and 2` → `and` walker counts both operands:
10074 // identifier `a` (+1), Number `2` (+1).
10075 // Total: 3.
10076 assert_eq!(metric.abc.conditions_sum(), 3);
10077 insta::assert_json_snapshot!(metric.abc);
10078 },
10079 );
10080 }
10081
10082 #[test]
10083 fn lua_if_boolean_literal_condition() {
10084 check_metrics::<LuaParser>(
10085 "function f()\n\
10086 if true then end -- +1c\n\
10087 if not false then end -- +1c\n\
10088 while true do break end -- +1c\n\
10089 repeat break until false -- +1c\n\
10090 end",
10091 "foo.lua",
10092 |metric| {
10093 assert_eq!(metric.abc.conditions_sum(), 4);
10094 insta::assert_json_snapshot!(metric.abc);
10095 },
10096 );
10097 }
10098
10099 #[test]
10100 fn lua_methods_arguments_with_conditions() {
10101 // `m(not a, not b)` — argument list walker counts each
10102 // unary-conditional argument once. Bare-identifier args
10103 // (`m(a, b)`) do not count (list_kind != BinaryExpression).
10104 check_metrics::<LuaParser>(
10105 "function f(a, b) m(a, b); m(not a, not b) end",
10106 "foo.lua",
10107 |metric| {
10108 assert_eq!(metric.abc.branches_sum(), 2);
10109 assert_eq!(metric.abc.conditions_sum(), 2);
10110 insta::assert_json_snapshot!(metric.abc);
10111 },
10112 );
10113 }
10114
10115 #[test]
10116 fn lua_return_with_conditions() {
10117 // `return not (z >= 0)` → walker on `not` unwraps the paren
10118 // chain and reaches the inner BinaryExpression; the inner
10119 // `>=` comparison is the actual Fitzpatrick condition.
10120 check_metrics::<LuaParser>(
10121 "function m1(z) return not (z >= 0) end\n\
10122 function m2(x) return (((not x))) end\n\
10123 function m3(x, y) return x and y end",
10124 "foo.lua",
10125 |metric| {
10126 // m1: `>=` (1). `not` wraps a paren'd
10127 // BinaryExpression — Lua's lua_inspect_container
10128 // reaches the inner BinaryExpression and stops,
10129 // no walker count. +1.
10130 // m2: ReturnStatement → iterate expression_list →
10131 // inspect_container on the outermost paren →
10132 // unwraps to `x` in has_boolean_content-true
10133 // (seeded by the `not`). +1.
10134 // m3: x and y → `and` walker counts both → +2.
10135 // Sum: 4.
10136 assert_eq!(metric.abc.conditions_sum(), 4);
10137 insta::assert_json_snapshot!(metric.abc);
10138 },
10139 );
10140 }
10141
10142 // ---------- Tcl ABC tests ----------
10143
10144 #[test]
10145 fn tcl_empty_unit_zero() {
10146 check_metrics::<TclParser>("", "empty.tcl", |metric| {
10147 assert_eq!(metric.abc.assignments_sum(), 0);
10148 assert_eq!(metric.abc.branches_sum(), 0);
10149 assert_eq!(metric.abc.conditions_sum(), 0);
10150 insta::assert_json_snapshot!(metric.abc);
10151 });
10152 }
10153
10154 #[test]
10155 fn tcl_set_command_counts_assignment() {
10156 // `set` has its own grammar production; each invocation is
10157 // one assignment.
10158 check_metrics::<TclParser>(
10159 "proc f {} {\n\
10160 set x 1\n\
10161 set y 2\n\
10162 set x [expr {$x + $y}]\n\
10163 }",
10164 "foo.tcl",
10165 |metric| {
10166 // 3 `set` invocations → A=3. The inner `expr` is a
10167 // sub-command (`command_substitution` + `expr_cmd`),
10168 // not a `command` node, so it doesn't add a branch.
10169 assert_eq!(metric.abc.assignments_sum(), 3);
10170 assert_eq!(metric.abc.branches_sum(), 0);
10171 assert_eq!(metric.abc.conditions_sum(), 0);
10172 insta::assert_json_snapshot!(metric.abc);
10173 },
10174 );
10175 }
10176
10177 #[test]
10178 fn tcl_incr_append_lappend_count_assignment() {
10179 // Variable-mutation commands (`incr`, `append`, `lappend`)
10180 // are recognised by name and count as assignments, not
10181 // branches.
10182 check_metrics::<TclParser>(
10183 "proc f {} {\n\
10184 set x 0\n\
10185 incr x\n\
10186 append s \"hi\"\n\
10187 lappend lst 1\n\
10188 }",
10189 "foo.tcl",
10190 |metric| {
10191 // `set` (1) + `incr` (1) + `append` (1) + `lappend`
10192 // (1) → A=4. No branches, no conditions.
10193 assert_eq!(metric.abc.assignments_sum(), 4);
10194 assert_eq!(metric.abc.branches_sum(), 0);
10195 assert_eq!(metric.abc.conditions_sum(), 0);
10196 insta::assert_json_snapshot!(metric.abc);
10197 },
10198 );
10199 }
10200
10201 #[test]
10202 fn tcl_computed_command_name_is_not_an_assignment() {
10203 // A command whose leading word is computed (`$cmd args`) names no
10204 // builtin the parser can resolve, so it stays a branch. Pins the
10205 // field-addressed, `simple_word`-gated read the classifier shares
10206 // with the Cognitive / Cyclomatic detectors (grammar-dispatch §3):
10207 // the earlier `child(0)` byte-slice addressed the slot by position
10208 // and compared whatever literal text sat there, which agrees with
10209 // the gated read on today's grammar only because no mutator name is
10210 // spelled with a leading `$`. A grammar that moved the name out of
10211 // `child(0)`, or a mutator list that grew a computed-looking entry,
10212 // would diverge — this fixture is where that surfaces.
10213 check_metrics::<TclParser>(
10214 "proc f {cmd x} {\n\
10215 $cmd $x\n\
10216 incr x\n\
10217 }",
10218 "foo.tcl",
10219 |metric| {
10220 // `incr x` is the only assignment; `$cmd $x` is a branch.
10221 assert_eq!(metric.abc.assignments_sum(), 1);
10222 assert_eq!(metric.abc.branches_sum(), 1);
10223 assert_eq!(metric.abc.conditions_sum(), 0);
10224 },
10225 );
10226 }
10227
10228 #[test]
10229 fn tcl_generic_commands_are_branches() {
10230 // Anything that isn't `set` or a known mutator command
10231 // counts as a branch — including builtins like `puts` and
10232 // `return`.
10233 check_metrics::<TclParser>(
10234 "proc f {} {\n\
10235 puts \"hello\"\n\
10236 do_work 1 2\n\
10237 return 0\n\
10238 }",
10239 "foo.tcl",
10240 |metric| {
10241 // 3 commands, all branches.
10242 assert_eq!(metric.abc.assignments_sum(), 0);
10243 assert_eq!(metric.abc.branches_sum(), 3);
10244 assert_eq!(metric.abc.conditions_sum(), 0);
10245 insta::assert_json_snapshot!(metric.abc);
10246 },
10247 );
10248 }
10249
10250 #[test]
10251 fn tcl_comparisons_count_logical_ops_do_not() {
10252 // `expr` predicates expose comparison / logical tokens at
10253 // the leaf level. Each comparison token contributes one
10254 // condition; `&&` and `||` are NOT counted on their own
10255 // (Fitzpatrick Rule 5; #395) — instead each operand is
10256 // counted as a unary conditional by the walker (Rule 9;
10257 // #403). The two logical lines add 2 walker conditions
10258 // each (variable-substitution operands).
10259 check_metrics::<TclParser>(
10260 "proc f {a b} {\n\
10261 set r [expr {$a == $b}]\n\
10262 set r [expr {$a != $b}]\n\
10263 set r [expr {$a < $b}]\n\
10264 set r [expr {$a > $b}]\n\
10265 set r [expr {$a <= $b}]\n\
10266 set r [expr {$a >= $b}]\n\
10267 set r [expr {$a eq $b}]\n\
10268 set r [expr {$a ne $b}]\n\
10269 set r [expr {$a && $b}]\n\
10270 set r [expr {$a || $b}]\n\
10271 }",
10272 "foo.tcl",
10273 |metric| {
10274 // 10 `set` assignments.
10275 assert_eq!(metric.abc.assignments_sum(), 10);
10276 assert_eq!(metric.abc.branches_sum(), 0);
10277 // 8 comparisons (+8) + 2 logical lines × 2 walker
10278 // operands (+4) = 12.
10279 assert_eq!(metric.abc.conditions_sum(), 12);
10280 insta::assert_json_snapshot!(metric.abc);
10281 },
10282 );
10283 }
10284
10285 #[test]
10286 fn tcl_ternary_counts_condition() {
10287 // The `ternary_expr` node is one condition and its condition
10288 // slot `$a` — a bare truthy test — is another, matching C++'s
10289 // `int r = a ? b : c;` (also 2). The two branch operands are
10290 // unnegated and so contribute nothing (#1180).
10291 check_metrics::<TclParser>(
10292 "proc f {a b c} {\n\
10293 set r [expr {$a ? $b : $c}]\n\
10294 }",
10295 "foo.tcl",
10296 |metric| {
10297 assert_eq!(metric.abc.assignments_sum(), 1);
10298 assert_eq!(metric.abc.branches_sum(), 0);
10299 assert_eq!(metric.abc.conditions_sum(), 2);
10300 insta::assert_json_snapshot!(metric.abc);
10301 },
10302 );
10303 }
10304
10305 /// The Tcl half of the ternary slot-location guard (#1180).
10306 ///
10307 /// `ternary_expr` exposes no grammar fields, so the slots are found
10308 /// relative to the `?` and `:` tokens. A *parenthesised* condition is
10309 /// the input that discriminates that from a fixed-index reading:
10310 /// `_expr` inlines `( … )` as anonymous children of `ternary_expr`,
10311 /// so `($a) ? !$b : !$c` shifts every operand right by one and
10312 /// `child(0)` / `child(2)` / `child(4)` land on `(`, `)` and `?`.
10313 /// Without this case the whole fixed-index revert passes.
10314 #[test]
10315 fn tcl_parenthesised_ternary_condition_matches_the_bare_form() {
10316 let conditions = |source: &str| {
10317 crate::test_support::metrics_verbatim(
10318 crate::LANG::Tcl,
10319 source.as_bytes(),
10320 crate::MetricsOptions::default(),
10321 )
10322 .abc
10323 .conditions_sum()
10324 };
10325 let bare = conditions("proc f {a b c} {\n set r [expr {$a ? !$b : !$c}]\n}");
10326 assert_eq!(bare, 4, "the bare form is the documented reference value");
10327 assert_eq!(
10328 conditions("proc f {a b c} {\n set r [expr {($a) ? !$b : !$c}]\n}"),
10329 bare,
10330 "parenthesising the condition must not change the count"
10331 );
10332 }
10333
10334 /// A parenthesised operand under `!` counts the same as a bare one.
10335 ///
10336 /// `_expr` inlines `( … )` as anonymous children, so `!($a)` puts
10337 /// `(` where a positional read expects the operand. The walker's
10338 /// negation branch kept a fixed `child(1)` through the first draft of
10339 /// #1180 and scored these 0 while the unparenthesised forms scored 1
10340 /// — an inconsistency the fix itself introduced, since before it
10341 /// neither form counted. Found in review, not by the tests: the
10342 /// parenthesised fixtures added with #1180 covered the ternary
10343 /// *condition* slot only.
10344 #[test]
10345 fn tcl_parenthesised_negated_operands_match_the_bare_form() {
10346 let conditions = |source: &str| {
10347 crate::test_support::metrics_verbatim(
10348 crate::LANG::Tcl,
10349 source.as_bytes(),
10350 crate::MetricsOptions::default(),
10351 )
10352 .abc
10353 .conditions_sum()
10354 };
10355 for (bare, parenthesised) in [
10356 (
10357 "proc f {a} {\n if {!$a} { puts x }\n}",
10358 "proc f {a} {\n if {!($a)} { puts x }\n}",
10359 ),
10360 (
10361 "proc f {a b} {\n if {$a && !$b} { puts x }\n}",
10362 "proc f {a b} {\n if {$a && !($b)} { puts x }\n}",
10363 ),
10364 (
10365 "proc f {a b c} {\n set r [expr {$a ? !$b : !$c}]\n}",
10366 "proc f {a b c} {\n set r [expr {$a ? !($b) : !$c}]\n}",
10367 ),
10368 ] {
10369 let want = conditions(bare);
10370 assert!(want > 0, "the bare form must count something: {bare}");
10371 assert_eq!(
10372 conditions(parenthesised),
10373 want,
10374 "parenthesising the negated operand changed the count\n bare: {bare}\n paren: {parenthesised}"
10375 );
10376 }
10377 }
10378
10379 #[test]
10380 fn irules_abc_parenthesised_negated_operands_match_the_bare_form() {
10381 let conditions = |source: &str| {
10382 crate::test_support::metrics_verbatim(
10383 crate::LANG::Irules,
10384 source.as_bytes(),
10385 crate::MetricsOptions::default(),
10386 )
10387 .abc
10388 .conditions_sum()
10389 };
10390 let want = conditions("when X {\n if { !$a } { log local0. hi }\n}\n");
10391 assert_eq!(want, 1);
10392 assert_eq!(
10393 conditions("when X {\n if { !($a) } { log local0. hi }\n}\n"),
10394 want
10395 );
10396 }
10397
10398 #[test]
10399 fn tcl_bare_truthy_and_negated_predicates_count_one_condition() {
10400 // The headline #1180 fix, on the Tcl side: both were 0 before.
10401 let conditions = |source: &str| {
10402 crate::test_support::metrics_verbatim(
10403 crate::LANG::Tcl,
10404 source.as_bytes(),
10405 crate::MetricsOptions::default(),
10406 )
10407 .abc
10408 .conditions_sum()
10409 };
10410 assert_eq!(conditions("proc f {a} {\n if {$a} { puts x }\n}"), 1);
10411 assert_eq!(conditions("proc f {a} {\n if {!$a} { puts x }\n}"), 1);
10412 assert_eq!(conditions("proc f {a} {\n while {$a} { puts x }\n}"), 1);
10413 assert_eq!(conditions("proc f {a} {\n while {!$a} { puts x }\n}"), 1);
10414 }
10415
10416 #[test]
10417 fn tcl_bare_truthy_elseif_predicate_counts_one_condition() {
10418 // The `Tcl::Elseif` arm routes its predicate through
10419 // `tcl_condition_expr` exactly as `If` / `While` do (#1180), but
10420 // a comparison predicate is counted by its leaf operator, so it
10421 // cannot tell the routing from its absence. Only a bare truthy
10422 // predicate can: `$b` scores through the routed walker or not at
10423 // all.
10424 let conditions = |source: &str| {
10425 crate::test_support::metrics_verbatim(
10426 crate::LANG::Tcl,
10427 source.as_bytes(),
10428 crate::MetricsOptions::default(),
10429 )
10430 .abc
10431 .conditions_sum()
10432 };
10433 // `$a` truthy (1) + `elseif` clause (1) + `$b` truthy (1).
10434 assert_eq!(
10435 conditions("proc f {a b} {\n if {$a} { puts x } elseif {$b} { puts y }\n}"),
10436 3
10437 );
10438 }
10439
10440 #[test]
10441 fn tcl_elseif_and_else_count_conditions() {
10442 // `if` / `elseif` / `else` clause productions each
10443 // contribute one condition. The leaf comparison inside the
10444 // predicate is counted independently.
10445 check_metrics::<TclParser>(
10446 "proc f {x} {\n\
10447 if {$x > 0} {\n\
10448 return 1\n\
10449 } elseif {$x < 0} {\n\
10450 return -1\n\
10451 } else {\n\
10452 return 0\n\
10453 }\n\
10454 }",
10455 "foo.tcl",
10456 |metric| {
10457 // Branches: three `return` commands → 3.
10458 // Conditions: `>` (1), `<` (1), `elseif` (1), `else`
10459 // (1) → 4.
10460 assert_eq!(metric.abc.assignments_sum(), 0);
10461 assert_eq!(metric.abc.branches_sum(), 3);
10462 assert_eq!(metric.abc.conditions_sum(), 4);
10463 insta::assert_json_snapshot!(metric.abc);
10464 },
10465 );
10466 }
10467
10468 #[test]
10469 fn tcl_if_multiple_conditions() {
10470 // Fitzpatrick Rule 9 walker (issue #403). Tcl's `expr` slot
10471 // exposes `&&` / `||` operands as variable substitutions
10472 // (`$a`, `$b`, …) inside a `binop_expr`.
10473 check_metrics::<TclParser>(
10474 "proc f {a b c d} {\n\
10475 if {[expr {$a || $b || $c || $d}]} { return 1 } \n\
10476 if {[expr {$a && $b && $c}]} { return 2 } \n\
10477 return 0\n\
10478 }",
10479 "foo.tcl",
10480 |metric| {
10481 // The two chains feed the walker: 4 + 3 = 7. Each `if`
10482 // predicate is additionally a bare truthy test of a
10483 // command substitution — `{[expr {…}]}` is structurally
10484 // `if {[somecmd]}`, which counts 1 exactly as `if {$a}`
10485 // does — so 7 + 2 = 9 (#1180). Written without the
10486 // redundant `[expr …]` wrapper, `if {$a || $b}` scores
10487 // 2, matching C++'s `if (a || b)`.
10488 assert_eq!(metric.abc.conditions_sum(), 9);
10489 insta::assert_json_snapshot!(metric.abc);
10490 },
10491 );
10492 }
10493
10494 #[test]
10495 fn tcl_while_conditions() {
10496 // Tcl has no `do { ... } while(cond);` — `while {…} {…}` is
10497 // the standard loop. The walker fires on `&&` / `||` tokens
10498 // inside the `expr` predicate.
10499 check_metrics::<TclParser>(
10500 "proc f {a b} {\n\
10501 while {[expr {$a || $b}]} { break } \n\
10502 while {[expr {$a && $b}]} { break } \n\
10503 }",
10504 "foo.tcl",
10505 |metric| {
10506 // 2 + 2 from the chains, plus one bare truthy test per
10507 // `while` predicate — see `tcl_if_multiple_conditions`
10508 // (#1180).
10509 assert_eq!(metric.abc.conditions_sum(), 6);
10510 insta::assert_json_snapshot!(metric.abc);
10511 },
10512 );
10513 }
10514
10515 #[test]
10516 fn tcl_short_circuit_with_boolean_literal_operand() {
10517 // `$a && 1` reports 2 conditions: a VariableSubstitution
10518 // operand plus a Number-literal operand. Confirms `Number`
10519 // is in the walker terminal set. `true` / `false` Tcl
10520 // keywords are not literal tokens in tree-sitter-tcl —
10521 // they're emitted as the operator-context word, which is
10522 // captured separately by the `Tcl::Boolean` kind for
10523 // dedicated `expr {true}` predicates but not as a `&&`
10524 // operand at this iteration; using a numeric literal keeps
10525 // the assertion grammar-stable.
10526 check_metrics::<TclParser>(
10527 "proc f {a} { return [expr {$a && 1}] }\n",
10528 "foo.tcl",
10529 |metric| {
10530 assert_eq!(metric.abc.conditions_sum(), 2);
10531 insta::assert_json_snapshot!(metric.abc);
10532 },
10533 );
10534 }
10535
10536 #[test]
10537 fn tcl_complex_function_abc() {
10538 // Mixed program covering every category. Tcl's grammar
10539 // re-parses braced content that looks command-shaped as a
10540 // nested `command` node, which inflates the branch count
10541 // relative to a naive read of the source — see breakdown.
10542 check_metrics::<TclParser>(
10543 "proc run {n} {\n\
10544 set total 0\n\
10545 for {set i 0} {$i < $n} {incr i} {\n\
10546 if {$i % 2 == 0} {\n\
10547 do_work $i\n\
10548 } else {\n\
10549 incr total $i\n\
10550 }\n\
10551 }\n\
10552 puts \"done\"\n\
10553 return $total\n\
10554 }",
10555 "foo.tcl",
10556 |metric| {
10557 // Assignments: `set total 0` (1), `set i 0` (1),
10558 // `incr i` (1), `incr total $i` (1) → A = 4.
10559 // Branches: the outer `for …` is one `command` node;
10560 // the `{$i < $n}` predicate ALSO re-parses as a
10561 // `command` node (tree-sitter-tcl treats braced
10562 // predicates as nested commands at the pinned
10563 // grammar version); plus `do_work $i`, `puts
10564 // "done"`, and `return $total`. The for-loop body's
10565 // `incr` and `incr total $i` are assignment commands
10566 // and don't add branches. Total B = 5.
10567 // Conditions: `==` (1) and `else` (1) → C = 2. The
10568 // `<` inside `{$i < $n}` is NOT `Tcl::LT`: because
10569 // that predicate re-parses as a `command`, the `<`
10570 // is emitted as `simple_word`. Only `<` inside a
10571 // real `expr` production becomes `Tcl::LT`.
10572 assert_eq!(metric.abc.assignments_sum(), 4);
10573 assert_eq!(metric.abc.branches_sum(), 5);
10574 assert_eq!(metric.abc.conditions_sum(), 2);
10575 insta::assert_json_snapshot!(metric.abc);
10576 },
10577 );
10578 }
10579
10580 /// The dedicated `set name value` production counts as one assignment.
10581 #[test]
10582 fn irules_abc_set_assignment() {
10583 check_metrics::<IrulesParser>("when X {\n set x 1\n}\n", "foo.irule", |metric| {
10584 assert_eq!(metric.abc.assignments_sum(), 1);
10585 assert_eq!(metric.abc.branches_sum(), 0);
10586 assert_eq!(metric.abc.conditions_sum(), 0);
10587 });
10588 }
10589
10590 /// Mutator commands (`incr` / `append` / `lappend`) count as
10591 /// assignments, not branches — iRules has no assignment operators, so
10592 /// mutation is always a command invocation.
10593 #[test]
10594 fn irules_abc_mutator_commands() {
10595 check_metrics::<IrulesParser>(
10596 "when X {\n incr x\n append s \"y\"\n lappend l 1\n}\n",
10597 "foo.irule",
10598 |metric| {
10599 assert_eq!(metric.abc.assignments_sum(), 3);
10600 assert_eq!(metric.abc.branches_sum(), 0);
10601 },
10602 );
10603 }
10604
10605 /// Generic (non-mutator) commands count as branches.
10606 #[test]
10607 fn irules_abc_branch_commands() {
10608 check_metrics::<IrulesParser>(
10609 "when X {\n log local0. hi\n pool p1\n}\n",
10610 "foo.irule",
10611 |metric| {
10612 assert_eq!(metric.abc.assignments_sum(), 0);
10613 assert_eq!(metric.abc.branches_sum(), 2);
10614 assert_eq!(metric.abc.conditions_sum(), 0);
10615 },
10616 );
10617 }
10618
10619 /// A numeric comparison (`==`) is one condition; the `log` inside the
10620 /// `if` body is one branch.
10621 #[test]
10622 fn irules_abc_comparison_condition() {
10623 check_metrics::<IrulesParser>(
10624 "when X {\n if { $a == 1 } { log local0. hi }\n}\n",
10625 "foo.irule",
10626 |metric| {
10627 assert_eq!(metric.abc.branches_sum(), 1);
10628 assert_eq!(metric.abc.conditions_sum(), 1);
10629 },
10630 );
10631 }
10632
10633 /// A word-form string comparator (`contains`) is a condition just like
10634 /// `==` — iRules-specific (Tcl has only `eq`/`ne`/`in`/`ni`). If
10635 /// `contains` were dropped from the condition set this would report 0.
10636 #[test]
10637 fn irules_abc_string_op_condition() {
10638 check_metrics::<IrulesParser>(
10639 "when X {\n if { $a contains \"x\" } { log local0. hi }\n}\n",
10640 "foo.irule",
10641 |metric| {
10642 assert_eq!(metric.abc.branches_sum(), 1);
10643 assert_eq!(metric.abc.conditions_sum(), 1);
10644 },
10645 );
10646 }
10647
10648 /// Each `elseif` / `else` clause is one condition; the three `set`s are
10649 /// assignments. The leading `if` is not itself a condition.
10650 #[test]
10651 fn irules_abc_elseif_else_conditions() {
10652 check_metrics::<IrulesParser>(
10653 "when X {\n if { $a } { set r 1 } elseif { $b } { set r 2 } else { set r 3 }\n}\n",
10654 "foo.irule",
10655 |metric| {
10656 assert_eq!(metric.abc.assignments_sum(), 3);
10657 // The `elseif` and `else` clauses are one condition each,
10658 // as before; #1180 adds the two bare truthy predicates
10659 // (`{ $a }`, `{ $b }`). C++'s
10660 // `if(a){} else if(b){} else {}` also scores 4.
10661 assert_eq!(metric.abc.conditions_sum(), 4);
10662 },
10663 );
10664 }
10665
10666 /// A ternary contributes its own condition plus the `>` comparison in
10667 /// its test: conditions 2; the `set` is one assignment.
10668 #[test]
10669 fn irules_abc_ternary_condition() {
10670 check_metrics::<IrulesParser>(
10671 "when X {\n set y [expr { $a > 0 ? 1 : 0 }]\n}\n",
10672 "foo.irule",
10673 |metric| {
10674 assert_eq!(metric.abc.assignments_sum(), 1);
10675 assert_eq!(metric.abc.conditions_sum(), 2);
10676 },
10677 );
10678 }
10679
10680 /// Fitzpatrick Rule 9: the short-circuit `&&` is not itself a condition,
10681 /// but each negated bare operand (`!$a`, `!$b`) in the chain is. Guards
10682 /// the `irules_count_unary_conditions` / `irules_inspect_container`
10683 /// walker — conditions 2.
10684 #[test]
10685 fn irules_abc_negated_operands_in_chain() {
10686 check_metrics::<IrulesParser>(
10687 "when X {\n if { !$a && !$b } { log local0. hi }\n}\n",
10688 "foo.irule",
10689 |metric| {
10690 assert_eq!(metric.abc.branches_sum(), 1);
10691 assert_eq!(metric.abc.conditions_sum(), 2);
10692 },
10693 );
10694 }
10695
10696 /// A bare-truthy `if {$a}` predicate is one condition (#1180).
10697 ///
10698 /// It carries no comparison, ternary or short-circuit operator, so
10699 /// before the Phase 2B slot routing landed nothing invoked the
10700 /// unary-conditional walker and the whole predicate scored 0 — this
10701 /// test previously pinned that absence, and the metrics book's ABC
10702 /// deviation table said so. Now the `if` node routes its `expr`
10703 /// predicate and the count matches C++'s `if (a)`, which is also 1.
10704 /// The `log` command remains the single branch.
10705 #[test]
10706 fn irules_abc_bare_truthy_counts_one_condition() {
10707 check_metrics::<IrulesParser>(
10708 "when X {\n if { $a } { log local0. hi }\n}\n",
10709 "foo.irule",
10710 |metric| {
10711 assert_eq!(metric.abc.branches_sum(), 1);
10712 assert_eq!(metric.abc.conditions_sum(), 1);
10713 },
10714 );
10715 }
10716
10717 /// The negated form of the same predicate, which was *also* 0 before
10718 /// #1180: `!$a` reached the walker but no parent seeded boolean
10719 /// context, so the terminal operand was never counted. Distinct from
10720 /// `irules_abc_negated_operands_in_chain`, whose `&&` supplied the
10721 /// seed the bare form lacked.
10722 #[test]
10723 fn irules_abc_negated_bare_truthy_counts_one_condition() {
10724 check_metrics::<IrulesParser>(
10725 "when X {\n if { !$a } { log local0. hi }\n}\n",
10726 "foo.irule",
10727 |metric| {
10728 assert_eq!(metric.abc.branches_sum(), 1);
10729 assert_eq!(metric.abc.conditions_sum(), 1);
10730 },
10731 );
10732 }
10733
10734 /// The ternary's three operand slots, located relative to the `?`
10735 /// and `:` tokens because the grammar exposes no fields (#1180).
10736 ///
10737 /// `$a ? !$b : !$c` is four: the `ternary_expr` node, the bare
10738 /// truthy condition, and one per negated branch — the same value
10739 /// Java, C#, Groovy, the C family, the JS family, PHP, Perl, Ruby
10740 /// and Python report for the identical expression.
10741 #[test]
10742 fn irules_abc_ternary_routes_its_operand_slots() {
10743 check_metrics::<IrulesParser>(
10744 "when X {\n set y [expr { $a ? !$b : !$c }]\n}\n",
10745 "foo.irule",
10746 |metric| {
10747 assert_eq!(metric.abc.conditions_sum(), 4);
10748 },
10749 );
10750 }
10751
10752 /// The #1161 control: a ternary whose condition is a *comparison*
10753 /// must not move. The `>` already supplied its condition and the
10754 /// branches are unnegated, so routing the slots adds nothing.
10755 #[test]
10756 fn irules_abc_comparison_ternary_is_unchanged_by_slot_routing() {
10757 check_metrics::<IrulesParser>(
10758 "when X {\n set y [expr { $a > 0 ? 1 : 0 }]\n}\n",
10759 "foo.irule",
10760 |metric| {
10761 assert_eq!(metric.abc.conditions_sum(), 2);
10762 },
10763 );
10764 }
10765
10766 /// A parenthesised ternary condition scores the same as a bare one.
10767 ///
10768 /// The grammar inlines `( … )` as anonymous children of
10769 /// `ternary_expr` rather than wrapping them in a node, so a
10770 /// fixed-index reading of the slots would shift right by one and
10771 /// mis-assign every operand. This is the input that discriminates
10772 /// the token-relative location the fix uses.
10773 #[test]
10774 fn irules_abc_parenthesised_ternary_condition_matches_the_bare_form() {
10775 // `check_metrics` takes a bare `fn`, so it cannot carry the
10776 // first measurement into the second comparison; `metrics_verbatim`
10777 // returns a value instead.
10778 let conditions = |source: &str| {
10779 crate::test_support::metrics_verbatim(
10780 crate::LANG::Irules,
10781 source.as_bytes(),
10782 crate::MetricsOptions::default(),
10783 )
10784 .abc
10785 .conditions_sum()
10786 };
10787 let bare = conditions("when X {\n set y [expr { $a ? !$b : !$c }]\n}\n");
10788 assert_eq!(bare, 4, "the bare form is the documented reference value");
10789 assert_eq!(
10790 conditions("when X {\n set y [expr { ($a) ? !$b : !$c }]\n}\n"),
10791 bare,
10792 "parenthesising the condition must not change the count"
10793 );
10794 }
10795}
10796
10797/// A comment inside a ternary must not change its ABC conditions
10798/// (#1181).
10799///
10800/// Two opposite defects, one cause: tree-sitter counts a comment among a
10801/// node's children, so it is the operand's previous sibling *and* it
10802/// shifts every positional index.
10803///
10804/// * Languages whose seed asked "is my previous sibling `?` or `:`"
10805/// (C family, PHP, Perl, JS family) read the comment as "not a
10806/// ternary token", flipped the boolean-context seed on for a *branch*
10807/// slot, and **over**-counted: `a ? /*n*/ (b) : c` scored 3 where
10808/// `a ? (b) : c` scores 2.
10809/// * Languages whose branch walk read `child(2)` / `child(4)` (Java,
10810/// C#, Groovy) landed on the comment instead of the operand, never
10811/// inspected it, and **under**-counted: `a ? /*n*/ !b : c` scored 2
10812/// where `a ? !b : c` scores 3.
10813///
10814/// Both slots are now addressed by grammar field. The parenthesised
10815/// operand is the only input that discriminates the first defect and
10816/// the negated operand the only one that discriminates the second —
10817/// existing ternary fixtures use neither.
10818#[cfg(test)]
10819mod ternary_comment_invariance {
10820 use crate::test_support::metrics_verbatim;
10821 use crate::{LANG, MetricsOptions};
10822
10823 fn conditions(lang: LANG, source: &str) -> u64 {
10824 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
10825 .abc
10826 .conditions_sum()
10827 }
10828
10829 /// `(base, with_comment)` for a parenthesised and a negated branch
10830 /// operand, per language.
10831 fn cases(lang: LANG) -> Option<[(String, String); 2]> {
10832 // `{}` marks the consequence slot; `/*n*/` the inserted comment.
10833 let (template, paren, negated, comment) = match lang {
10834 LANG::Cpp | LANG::C | LANG::Objc | LANG::Mozcpp => {
10835 ("int f(){ int x = a ? {} : c; }", "(b)", "!b", "/*n*/ ")
10836 }
10837 LANG::Java => (
10838 "class K{ void f(){ int x = a ? {} : c; } }",
10839 "(b)",
10840 "!b",
10841 "/*n*/ ",
10842 ),
10843 LANG::Csharp => (
10844 "class K{ void f(){ var x = a ? {} : c; } }",
10845 "(b)",
10846 "!b",
10847 "/*n*/ ",
10848 ),
10849 LANG::Groovy => ("def f(){ def x = a ? {} : c }", "(b)", "!b", "/*n*/ "),
10850 LANG::Javascript | LANG::Typescript | LANG::Tsx | LANG::Mozjs => {
10851 ("function f(){ var x = a ? {} : c; }", "(b)", "!b", "/*n*/ ")
10852 }
10853 LANG::Php => (
10854 "<?php function f(){ $x = $a ? {} : $c; }",
10855 "($b)",
10856 "!$b",
10857 "/*n*/ ",
10858 ),
10859 // Perl has no block comment: `#` runs to end of line, so the
10860 // comment must carry its own newline.
10861 LANG::Perl => ("sub f { my $x = $a ? {} : $c; }", "($b)", "!$b", "# n\n "),
10862 _ => return None,
10863 };
10864 let build = |operand: &str, with_comment: bool| {
10865 let slot = if with_comment {
10866 format!("{comment}{operand}")
10867 } else {
10868 operand.to_owned()
10869 };
10870 template.replace("{}", &slot)
10871 };
10872 Some([
10873 (build(paren, false), build(paren, true)),
10874 (build(negated, false), build(negated, true)),
10875 ])
10876 }
10877
10878 #[test]
10879 fn a_comment_before_a_branch_operand_changes_nothing() {
10880 let mut checked = 0;
10881 for lang in LANG::into_enum_iter() {
10882 if !lang.is_enabled() {
10883 continue;
10884 }
10885 let Some(pairs) = cases(lang) else { continue };
10886 checked += 1;
10887 for (base, commented) in pairs {
10888 assert_eq!(
10889 conditions(lang, &commented),
10890 conditions(lang, &base),
10891 "{lang:?}: a comment changed the ABC conditions of a ternary\n \
10892 without: {base}\n with: {commented}"
10893 );
10894 }
10895 }
10896 assert!(
10897 checked > 0,
10898 "no ternary language enabled; this test asserted nothing"
10899 );
10900 }
10901
10902 /// The absolute values the invariance test compares against, so a
10903 /// regression that moved *both* sides equally still fails.
10904 ///
10905 /// expected: `a ? (b) : c` counts the `?` marker plus the condition
10906 /// `a` in boolean context = 2. Negating the consequence adds one
10907 /// more, since `!b` establishes boolean content for that slot = 3.
10908 #[test]
10909 fn the_baseline_values_are_two_and_three() {
10910 let mut checked = 0;
10911 for lang in LANG::into_enum_iter() {
10912 if !lang.is_enabled() {
10913 continue;
10914 }
10915 let Some([(paren, _), (negated, _)]) = cases(lang) else {
10916 continue;
10917 };
10918 assert_eq!(
10919 conditions(lang, &paren),
10920 2,
10921 "{lang:?}: parenthesised branch"
10922 );
10923 assert_eq!(conditions(lang, &negated), 3, "{lang:?}: negated branch");
10924 checked += 1;
10925 }
10926 assert!(
10927 checked > 0,
10928 "no ternary language enabled; this test asserted nothing"
10929 );
10930 }
10931}
10932
10933/// A keyword negation must score like its symbolic twin (#1182).
10934///
10935/// `not b` and `!b` are the same negation — they differ in precedence,
10936/// not in meaning, and ABC counts the negation rather than the parse.
10937/// Ruby and Perl tested only the `!` token, so `if not b` scored 0
10938/// against `if !b`'s 1, and a `not` ternary scored 2 against the `!`
10939/// form's 4.
10940///
10941/// Lua and Elixir were checked in the same sweep and were already
10942/// correct: Lua's only negation keyword *is* `not` and it was the token
10943/// being tested, and Elixir reaches the same count by another path.
10944/// They are exercised here so a future edit cannot regress them
10945/// silently. Python counts `not` through its own dispatcher arm and has
10946/// no `!` spelling to compare against.
10947#[cfg(test)]
10948mod keyword_negation_parity {
10949 use crate::test_support::metrics_verbatim;
10950 use crate::{LANG, MetricsOptions};
10951
10952 fn conditions(lang: LANG, source: &str) -> u64 {
10953 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
10954 .abc
10955 .conditions_sum()
10956 }
10957
10958 /// `(bang_form, keyword_form)` pairs that must score identically.
10959 fn pairs(lang: LANG) -> Option<Vec<(String, String)>> {
10960 let build =
10961 |t: &str| -> (String, String) { (t.replace("{NOT}", "!"), t.replace("{NOT}", "not ")) };
10962 let templates: &[&str] = match lang {
10963 LANG::Ruby => &[
10964 "def f(b)\n if {NOT}b\n 1\n end\nend\n",
10965 "def f(a, b, c)\n x = a ? ({NOT}b) : ({NOT}c)\nend\n",
10966 "def f(a, b, c)\n x = a ? b : ({NOT}c)\nend\n",
10967 ],
10968 LANG::Perl => &[
10969 "sub f { if ({NOT}$b) { 1; } }",
10970 "sub f { my $x = $a ? ({NOT}$b) : ({NOT}$c); }",
10971 "sub f { my $x = $a ? $b : ({NOT}$c); }",
10972 ],
10973 // Already correct before #1182; pinned so they stay that way.
10974 LANG::Elixir => &["def f(b) do\n if {NOT}b do\n 1\n end\nend\n"],
10975 _ => return None,
10976 };
10977 Some(templates.iter().map(|t| build(t)).collect())
10978 }
10979
10980 #[test]
10981 fn the_not_keyword_scores_like_bang() {
10982 let mut checked = 0;
10983 for lang in LANG::into_enum_iter() {
10984 if !lang.is_enabled() {
10985 continue;
10986 }
10987 let Some(pairs) = pairs(lang) else { continue };
10988 checked += 1;
10989 for (bang, keyword) in pairs {
10990 assert_eq!(
10991 conditions(lang, &keyword),
10992 conditions(lang, &bang),
10993 "{lang:?}: `not` and `!` scored differently\n bang: {bang}\n keyword: {keyword}"
10994 );
10995 }
10996 }
10997 assert!(
10998 checked > 0,
10999 "no language enabled; this test asserted nothing"
11000 );
11001 }
11002
11003 /// The absolute values, so a regression that moved both spellings
11004 /// equally still fails.
11005 ///
11006 /// expected: `if !b` is one condition — the negated bare operand.
11007 /// `a ? (!b) : (!c)` is four: the `?` marker, the condition `a` in
11008 /// boolean context, and one per negated branch operand.
11009 #[test]
11010 fn the_baseline_values_are_one_and_four() {
11011 let mut checked = 0;
11012 for (lang, guard, ternary) in [
11013 (
11014 LANG::Ruby,
11015 "def f(b)\n if not b\n 1\n end\nend\n",
11016 "def f(a, b, c)\n x = a ? (not b) : (not c)\nend\n",
11017 ),
11018 (
11019 LANG::Perl,
11020 "sub f { if (not $b) { 1; } }",
11021 "sub f { my $x = $a ? (not $b) : (not $c); }",
11022 ),
11023 ] {
11024 if !lang.is_enabled() {
11025 continue;
11026 }
11027 assert_eq!(conditions(lang, guard), 1, "{lang:?}: `if not b`");
11028 assert_eq!(conditions(lang, ternary), 4, "{lang:?}: `not` ternary");
11029 checked += 1;
11030 }
11031 assert!(
11032 checked > 0,
11033 "no language enabled; this test asserted nothing"
11034 );
11035 }
11036
11037 /// Lua's only negation keyword is `not`, so it has no `!` twin to
11038 /// compare against — its guard is that the keyword counts at all.
11039 #[test]
11040 fn lua_counts_its_only_negation_keyword() {
11041 if !LANG::Lua.is_enabled() {
11042 return;
11043 }
11044 assert_eq!(
11045 conditions(
11046 LANG::Lua,
11047 "function f(b)\n if not b then return 1 end\nend\n"
11048 ),
11049 1
11050 );
11051 }
11052}